How to Dynamically Instantiate QML items in Pyside - python

Building off this Pyside tutorial:
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_1
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_2
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_3
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_4
I am attempting to do everything in Python and not have any java script.
The only difficulty I've run into is when calling the createObject() method of a QDeclarativeComponent which is described nicely as a "Dynamic Object Management" here:
http://qt-project.org/doc/qt-4.8/qdeclarativedynamicobjects.html
So here is a bare bones example that causes the error:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtDeclarative import *
class MainWindow(QDeclarativeView):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowTitle("Main Window")
# Renders game screen
self.setSource(QUrl.fromLocalFile('game2.qml'))
# QML resizes to main window
self.setResizeMode(QDeclarativeView.SizeRootObjectToView)
# a qml object I'd like to add dynamically
self.component = QDeclarativeComponent(QDeclarativeEngine(), QUrl.fromLocalFile("Block2.qml"))
# check if were ready to construct the object
if self.component.isReady():
# create the qml object dynamically
dynamicObject = self.component.createObject(self.rootObject())
if __name__ == '__main__':
# Create the Qt Application
app = QApplication(sys.argv)
# Create and show the main window
window = MainWindow()
window.show()
# Run the main Qt loop
sys.exit(app.exec_())
With main window QML file contents ("game2.qml"):
import QtQuick 1.0
Rectangle {
id: screen
width: 490; height: 720
SystemPalette { id: activePalette }
}
And QML object I'd like to dynamically construct ("Block2.qml"):
import QtQuick 1.0
Rectangle {
id: block
}
When I run this code, it crashes at:
dynamicObject = self.component.createObject(self.rootObject())
with:
TypeError: Unknown type used to call meta function (that may be a signal): QScriptValue
I understand the parent must be a QObject but otherwise I'm not entirely sure from the docs what more it should constitute:
http://srinikom.github.io/pyside-docs/PySide/QtDeclarative/QDeclarativeComponent.html
This isn't an issue in C++ according to:
https://qt-project.org/forums/viewthread/7717
It is clearly only an issue in Pyside currently.
Any idea what might be causing this issue? Potential bug?

A work around is to rely on javascript for object creation while everything else is python. In this implementation you pass the qml file of the component and its parent to the javascript implementation that creates the component. It does a basic construction of the object. Would be ideal having pure python solution though for brevity.
import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtDeclarative import *
class MainWindow(QDeclarativeView):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowTitle("Main Window")
# Renders game screen
self.setSource(QUrl.fromLocalFile('game2.qml'))
# QML resizes to main window
self.setResizeMode(QDeclarativeView.SizeRootObjectToView)
# a qml object I'd like to add dynamically
parent = self.rootObject()
view = QDeclarativeView()
view.setSource(QUrl.fromLocalFile("comp_create.qml"))
block = view.rootObject().create("Block2.qml", parent)
print block
block.x = 100
block.y = 200
# prove that we created the object
print block.x, block.y
if __name__ == '__main__':
# Create the Qt Application
app = QApplication(sys.argv)
# Create and show the main window
window = MainWindow()
window.show()
# Run the main Qt loop
sys.exit(app.exec_())
The only added QML is a component creator that uses javascript to create objects since Pyside currently wont work ("comp_create.qml"):
import QtQuick 1.0
Item {
id: creator
function create(qml_fname, parent) {
// for a given qml file and parent, create component
var comp = Qt.createComponent(qml_fname);
if (comp.status == Component.Ready) {
// create the object with given parent
var ob = comp.createObject(parent);
if (ob == null) {
// Error Handling
console.log("Error creating object");
}
return ob
} else if (component.status == Component.Error) {
// Error Handling
console.log("Error loading component:", component.errorString());
}
else {
component.statusChanged.connect(finishCreation);
}
return null
}
}
Note, borrowed this code mostly from:
http://qt-project.org/doc/qt-4.8/qdeclarativedynamicobjects.html

Related

closeEvent for Window Python

I am creating an application on Qt Creator with python as my backend and I have a problem that i can't seem to pin point.
So what i want to do is have a function called when the user exits the application but the function
closeEvent(self, event:QCloseEvent) is never called.
The Constructor that the class inheriting the QMainWindow has is called but the over ridden methods aren't called.
Main.qml
import QtQuick
import QtQuick.Window
Window {
id: mainwindow
width: 640
height: 480
visible: true
color: "#000000"
flags: Qt.Window
title: qsTr("Hello World")
Connections{
target: backend
}
}
Main.py
# This Python file uses the following encoding: utf-8
import os
from pathlib import Path
import sys
from PySide6.QtGui import QCloseEvent
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtWidgets import QApplication,QMainWindow
class DetectQuit(QMainWindow):
def __init__(self):
print("From Constructor")
super().__init__()
print("End Constructor")
def hideEvent(self, event):
print("Minimizing Application")
def closeEvent(self, event:QCloseEvent) :
return super().closeEvent(event)
def closeEvent(self, event):
print("Exiting Application")
if __name__ == "__main__":
app = QApplication(sys.argv)
engine = QQmlApplicationEngine()
mainwindow=DetectQuit()
engine.rootContext().setContextProperty("backend",mainwindow)
engine.load(os.fspath(Path(__file__).resolve().parent / "main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
So what happens is that the DetectQuit class' constructor is called but the over-ridden functions are not called. If I add a self.show() in the constructor, there appears a second window titled "python" that calls the events accordingly.
Images for Visual assistance :
Running the Application Prints the Constructor part but not the Closing and hiding Events
This is with the Show method on the class which causes two windows to appear
Closing the second window that popped up causes the expected behaviour
Any help is greatly appreciated.
Well the thing is that the closeEvent() does not work so a workaround is that you can add a property to the Window Component of the qml that is onClosing like:
onClosing: {
print("Exiting Application (1)")
backend.closeFunction()
}
/*
// close.accepted = false -- if you need to do something else before the window can be closed
onClosing: function(close) {
print("Exiting Application (1)")
close.accepted = false
backend.closeFunction()
}
*/
This was Answered by #ndias on the Qt Forum
Hope this helps.

Custom QQuickItem is not painted

I am trying to write my own QQuickItems that use the SceneGraph to draw shapes as if it the parent Item was a Canvas. I am using PySide6 and Python. During my attempts I found a bug in PySide6, which was earlier reported. I downloaded the patch that fixed it and it seems to be ok now (https://bugreports.qt.io/browse/PYSIDE-1345).
My program now compiles and runs, but the node is not painted. If I understand the documentation correctly, I need to do three things to have a custom QQuickItem painted:
Inherit from QQuickItem
Override updatePaintNode
Set the ItemHasContents flag
I did all that, registered my type, added it in QML and made sure it has non-zero dimensions. Unfortunately, it still does not get paited. I do not know what to do next.
Here's the minimal working example:
main.py
# This Python file uses the following encoding: utf-8
import os
from pathlib import Path
import sys
from PySide6.QtGui import QGuiApplication, QColor
from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
from PySide6.QtQuick import QQuickItem, QSGGeometryNode, QSGGeometry, QSGFlatColorMaterial
class JustItem(QQuickItem):
def __init__(self, parent=None):
super().__init__(parent)
self.setFlag(QQuickItem.ItemHasContents)
def updatePaintNode(self, node, update_data):
if node is None:
node = QSGGeometryNode()
geometry = QSGGeometry(QSGGeometry.defaultAttributes_Point2D(), 4)
geometry.setDrawingMode(QSGGeometry.DrawTriangles)
vertex_data = geometry.vertexDataAsPoint2D()
vertex_data[0].set(10, 10)
vertex_data[1].set(100, 10)
vertex_data[2].set(100, 100)
vertex_data[3].set(10, 100)
material = QSGFlatColorMaterial()
material.setColor(QColor(255, 0, 0, 127))
node.setGeometry(geometry)
node.setMaterial(material)
return node
if __name__ == "__main__":
qmlRegisterType(JustItem, "PythonTypes", 1, 0, "JustItem")
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load(os.fspath(Path(__file__).resolve().parent / "main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
And QML:
import QtQuick
import QtQuick.Window
import PythonTypes 1.0
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
JustItem {
}
}
The result is an empty white window. When I resize it, it segfaults.
This is a bug in PySide 2. Currently it is not possible to draw your custom QQuickItems in PySide 2.
The bug is discussed in more detail here: https://forum.qt.io/topic/116585/qsggeometry-does-not-work-on-pyside2/16
There's also a bug report, which has a proposed fix. The fix causes segmentation fault: https://bugreports.qt.io/browse/PYSIDE-1345
Unfortunately, if you want to draw custom QQuickItems you either need to write them in C++ or use another GUI framework entirely.

Injecting Properties into QML-Component

I am fairly new to Qt/PyQt and currently struggling with some basic functionality. What I'm trying to do is, to dynamically load QML-Views (*.qml) files from python and replace specific content on the fly. For example a checkbox gets checked and part of my current view is replaced with another qml file. First I wanted to provide this logic via PyQt, but it seems a StackView is a better idea (multiple qml files in pyqt).
However, in this case I am not able to inject properties into my QML files. I am only able to inject a property into the rootContext. That however limits the usage of my QML-Views since I can only use one view (of the same type) at once. I would like to inject properties dynamically into QML-Views and make them only visible to this particular view. In this case I can use the same view more than once with more than one object in the back-end to catch the signals.
Here is my SimpleMainWindow.qml file (the main view:
import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import QtQuick.Controls 1.4
ApplicationWindow {
id: window
visible: true
width: 640
height: 480
title: qsTr("Hello World")
objectName : "WINDOW"
property ApplicationWindow appWindow : window
}
And here the file I try to load (TestViewButton.qml):
import QtQuick 2.9
import QtQuick.Controls 1.4
Button {
id: test
text: "test"
objectName : "Button"
onClicked: {
configurator.sum(1, 2)
configurator.info("TestOutput")
}
}
Python file loading QML-View (Component)
from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine, QQmlComponent
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load("qml/SimpleMainWindow.qml")
engine.quit.connect(app.quit)
rootWindow = engine.rootObjects()[0].children()[0].parent()
# create component
component = QQmlComponent(engine)
component.loadUrl(QUrl("qml/TestViewButton.qml"))
configurator = SignalHandler()
component.setProperty("configurator", configurator)
itm = component.create()
itm.setParentItem(rootWindow.children()[0])
itm.setProperty("configurator", configurator)
app.exec_()
And the python object that I use to handle the signals from the view (SignalHandler.py):
from PyQt5.QtCore import QObject, pyqtSlot
class SignalHandler(QObject):
#pyqtSlot(int, int)
def sum(self, arg1, arg2):
print("Adding two numbers")
#pyqtSlot(str)
def info(self, arg1):
print("STR " + arg1)
The button loads fine (by the way, is there a better way to identify the parent I want to add my button to, wasn't having any look with findChild). What is not working is the component.setProperty.... part. If I set the property in the rootContext of the engine it works fine (the SignalHandler methods are called). Already checked similar topics (like Load a qml component from another qml file dynamically and access that component's qml type properties ...)
Is this possible, or am I getting something wrong here?
thanks
From what I understand, you want to load the configuration object only in TestViewButton.qml and it is not visible in SimpleMainWindow.qml.
To do this TestViewButton.qml must have its own QQmlContext when it is loaded and is not the rootContext().
To test my response and observe that behavior we will create a similar button that tries to use the configurator, if this is pressed it should throw an error noting that the property does not exist but if the button loaded is pressed by the QQmlComponent should do its job normally.
qml/SimpleMainWindow.qml
import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import QtQuick.Controls 1.4
ApplicationWindow {
id: window
visible: true
width: 640
color: "red"
height: 480
title: qsTr("Hello World")
Button {
x: 100
y: 100
text: "ApplicationWindow"
onClicked: {
console.log("ApplicationWindow")
configurator.sum(1, 2)
configurator.info("TestOutput")
}
}
}
As I commented previously I added the component with a new context:
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
configurator = SignalHandler()
engine.load("qml/SimpleMainWindow.qml")
engine.quit.connect(app.quit)
rootWindow = engine.rootObjects()[0]
content_item = rootWindow.property("contentItem")
context = QQmlContext(engine)
component = QQmlComponent(engine)
component.loadUrl(QUrl("qml/TestViewButton.qml"))
itm = component.create(context)
context.setContextProperty("configurator", configurator)
itm.setProperty("parent", content_item)
sys.exit(app.exec_())
At the end we get the following output:
qml: Component
Adding two numbers
STR TestOutput
qml: ApplicationWindow
file:///xxx/qml/SimpleMainWindow.qml:20: ReferenceError: configurator is not defined
qml: Component
Adding two numbers
STR TestOutput
qml: ApplicationWindow
file:///xxx/qml/SimpleMainWindow.qml:20: ReferenceError: configurator is not defined
Where we observe the desired behavior. The complete example can be found in the following link.

PyQt: Emit signal when cell entered in QCalendarWidget

In my Qt application I'm using the QCalendarWidget and I would like to get notified when the mouse enters a new cell of the calendar. I know that the QCalendarWidget is using a QTableView internally which inherits from QAbstractItemView and this has an entered signal:
This signal is emitted when the mouse cursor enters the item specified
by index. Mouse tracking needs to be enabled for this feature to work.
I tried to receive the signal with following code:
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyCalendar:
def __init__(self):
app = QApplication(sys.argv)
window = QMainWindow()
cal = QCalendarWidget(window)
window.resize(320, 240)
cal.resize(320, 240)
table = cal.findChild(QTableView)
table.setMouseTracking(True)
table.entered.connect(self.onCellEntered)
window.show()
sys.exit(app.exec_())
def onCellEntered(self, index):
print("CellEntered")
if __name__ == "__main__":
window = MyCalendar()
But my callback function is never called. Do you have any ideas why?
The QCalendarWidget class uses a custom table-view which bypasses the normal mouse-event handlers - so the enetered signal never gets emitted. However, it is possible to work-around that by using an event-filter to emit a custom signal that does the same thing.
Here is a demo script that implements that:
import sys
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
cellEntered = QtCore.pyqtSignal(object)
def __init__(self):
super(Window, self).__init__()
self.calendar = QtGui.QCalendarWidget(self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.calendar)
self._table = self.calendar.findChild(QtGui.QTableView)
self._table.setMouseTracking(True)
self._table.installEventFilter(self)
self._index = None
self.cellEntered.connect(self.handleCellEntered)
def eventFilter(self, source, event):
if source is self._table:
if event.type() == QtCore.QEvent.MouseMove:
index = QtCore.QPersistentModelIndex(
source.indexAt(event.pos()))
if index != self._index:
self._index = index
self.cellEntered.emit(QtCore.QModelIndex(index))
elif event.type() == QtCore.QEvent.Leave:
self._index = None
return super(Window, self).eventFilter(source, event)
def handleCellEntered(self, index):
print(index.row(), index.column())
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(600, 100, 300, 200)
window.show()
sys.exit(app.exec_())
I've investigated a bit and I think I know why this happens.
QCalendarWidget creates a private subclass of QTableView called QCalendarView and instantiates it as a child of itself. (This instance is called qt_calendar_calendarview.)
If you look at QCalendarView's code (Qt 5), you'll see this:
void QCalendarView::mouseMoveEvent(QMouseEvent *event)
{
[...]
if (!calendarModel) {
QTableView::mouseMoveEvent(event);
return;
}
[...]
}
This means only if there is no calendarModel, the superclasses mouseMoveEventis called which is responsible for emitting the entered signal. All of this is an implementation detail of `QCalendarWidget, so it's probably best not to rely on any of this anyway.
So for a way around this, I'm not sure what the best approach is. You'll have to catch the event before it gets to the table. This can be done using QObject.installEventFilter() or re-implementing QWidget.mouseMoveEvent(), but then you don't get the model index directly. You could probably use QAbstractItemView.indexAt() for this purpose.

Accessing QTextHtmlImporter in PyQt4

I'm writing a cross platform app in PyQt4. For a particular feature, I would like to access the QTextHtmlImporter class of Qt4. There is no direct python adapter class available in PyQt4. The class is part of the src/gui/text/qtextdocumentfragment_p.h file. Is there any way I can access that in Python?
I would like to modify QTextDocument.setHtml(), which code is:
void QTextDocument::setHtml(const QString &html) {
Q_D(QTextDocument); setUndoRedoEnabled(false);
d->clear();
QTextHtmlImporter(this, html).import();
setUndoRedoEnabled(true);
}
to
void QTextDocument::setHtml(const QString &html) {
Q_D(QTextDocument);
QTextHtmlImporter(this, html).import();
}
Basically setting the HTML without clearing the history. I planned to do this by using a derived class of PyQt4's QTextDocument overriding the setHtml function. Is there any other way to do this?
QTextHtmlImporter isn't even part of the Qt4 API, so the short answer is: no, there's no way to access it in PyQt4.
You could, of course, attempt to port the code to PyQt4, but I'm guessing that would be a non-trivial task.
The question is: why do you think you need to do this?
Why can't you use QTextCursor.insertHtml or QTextDocumentFragment.fromHtml?
EDIT
Here's an example of how to set the html in a text document without clearing the undo history:
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
layout = QtGui.QVBoxLayout(self)
self.edit = QtGui.QTextEdit(self)
self.undo = QtGui.QPushButton('Undo')
self.redo = QtGui.QPushButton('Redo')
self.insert = QtGui.QPushButton('Set Html')
layout.addWidget(self.edit)
layout.addWidget(self.undo)
layout.addWidget(self.redo)
layout.addWidget(self.insert)
self.undo.clicked.connect(self.edit.undo)
self.redo.clicked.connect(self.edit.redo)
self.insert.clicked.connect(self.handleInsert)
self.edit.append('One')
self.edit.append('Two')
self.edit.append('Three')
def handleInsert(self):
cursor = QtGui.QTextCursor(self.edit.document())
cursor.select(QtGui.QTextCursor.Document)
cursor.insertHtml("""<p>Some <b>HTML</b> text</p>""")
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())

Categories

Resources