How Can I Update a Qml Object's Property from my Python file?

There are several methods to modify a property of a QML element from python/C++, and each has its advantages and disadvantages.

1. Pulling References from QML

  • Obtain the QML object through findChildren through another object.
  • Modify or access the property with setProperty() or property(), respectively or with QQmlProperty.

main.qml (the qml is for the next 2 .py)

import QtQuick 2.11
import QtQuick.Window 2.2
import QtQuick.Controls 2.2

ApplicationWindow {    
    visible: true
    width: Screen.width/2
    height: Screen.height/2
    Rectangle {
        id: rectangle
        x: 187
        y: 92
        width: 200
        height: 200
        color: "blue"
        objectName: "foo_object"
    }
}

1.1 setProperty(), property().

import os
import sys
from PyQt5 import QtCore, QtGui, QtQml
from functools import partial

def testing(r):
    import random
    w = r.property("width")
    h = r.property("height")
    print("width: {}, height: {}".format(w, h))
    r.setProperty("width", random.randint(100, 400))
    r.setProperty("height", random.randint(100, 400))

def run():
    myApp = QtGui.QGuiApplication(sys.argv)
    myEngine = QtQml.QQmlApplicationEngine()
    directory = os.path.dirname(os.path.abspath(__file__))
    myEngine.load(QtCore.QUrl.fromLocalFile(os.path.join(directory, 'main.qml')))
    if not myEngine.rootObjects():
        return -1
    r = myEngine.rootObjects()[0].findChild(QtCore.QObject, "foo_object")
    timer = QtCore.QTimer(interval=500)
    timer.timeout.connect(partial(testing, r))
    timer.start()
    return myApp.exec_()

if __name__ == "__main__":
    sys.exit(run())

1.2 QQmlProperty.

import os
import sys
from PyQt5 import QtCore, QtGui, QtQml
from functools import partial

def testing(r):
    import random
    w_prop = QtQml.QQmlProperty(r, "width")
    h_prop = QtQml.QQmlProperty(r, "height")
    print("width: {}, height: {}".format(w_prop.read(), w_prop.read()))
    w_prop.write(random.randint(100, 400))
    h_prop.write(random.randint(100, 400))

def run():
    myApp = QtGui.QGuiApplication(sys.argv)
    myEngine = QtQml.QQmlApplicationEngine()
    directory = os.path.dirname(os.path.abspath(__file__))
    myEngine.load(QtCore.QUrl.fromLocalFile(os.path.join(directory, 'main.qml')))

    if not myEngine.rootObjects():
        return -1
    r = myEngine.rootObjects()[0].findChild(QtCore.QObject, "foo_object")
    timer = QtCore.QTimer(interval=500)
    timer.timeout.connect(partial(testing, r))
    timer.start()
    return myApp.exec_()

if __name__ == "__main__":
    sys.exit(run())

A disadvantage of this method is that if the relation of the object with the rootobject is complex(Sometimes objects that are in other QMLs are hard to access with findChild) the part of accessing the object becomes complicated and sometimes impossible so this method will fail. Another problem is that when using the objectName as the main search data there is a high dependency of the Python layer to the QML layer since if the objectName is modified in QML the logic in python would have to be modified. Another disadvantage is that by not managing the life cycle of the QML object it could be eliminated without Python knowing so it would access an incorrect reference causing the application to terminate unexpectedly.

2. Pushing References to QML

  • Create a QObject that has the same type of properties.
  • Export to QML using setContextProperty.
  • Make the binding between the properties of the QObject and the properties of the item.

main.qml

import QtQuick 2.11
import QtQuick.Window 2.2
import QtQuick.Controls 2.2

ApplicationWindow {    
    visible: true
    width: Screen.width/2
    height: Screen.height/2
    Rectangle {
        id: rectangle
        x: 187
        y: 92
        width: r_manager.width
        height: r_manager.height
        color: "blue"
    }
}

main.py

import os
import sys
from PyQt5 import QtCore, QtGui, QtQml
from functools import partial

class RectangleManager(QtCore.QObject):
    widthChanged = QtCore.pyqtSignal(float)
    heightChanged = QtCore.pyqtSignal(float)

    def __init__(self, parent=None):
        super(RectangleManager, self).__init__(parent)
        self._width = 100
        self._height = 100

    @QtCore.pyqtProperty(float, notify=widthChanged)
    def width(self):
        return self._width

    @width.setter
    def width(self, w):
        if self._width != w:
            self._width = w
            self.widthChanged.emit(w)

    @QtCore.pyqtProperty(float, notify=heightChanged)
    def height(self):
        return self._height

    @height.setter
    def height(self, h):
        if self._height != h:
            self._height = h
            self.heightChanged.emit(h)

def testing(r):
    import random
    print("width: {}, height: {}".format(r.width, r.height))
    r.width = random.randint(100, 400)
    r.height = random.randint(100, 400)

def run():
    myApp = QtGui.QGuiApplication(sys.argv)
    myEngine = QtQml.QQmlApplicationEngine()
    manager = RectangleManager()
    myEngine.rootContext().setContextProperty("r_manager", manager)
    directory = os.path.dirname(os.path.abspath(__file__))
    myEngine.load(QtCore.QUrl.fromLocalFile(os.path.join(directory, 'main.qml')))

    if not myEngine.rootObjects():
        return -1
    timer = QtCore.QTimer(interval=500)
    timer.timeout.connect(partial(testing, manager))
    timer.start()
    return myApp.exec_()

if __name__ == "__main__":
    sys.exit(run())

The disadvantage is that you have to write some more code. The advantage is that the object is accessible by all the QML since it uses setContextProperty, another advantage is that if the QML object is deleted it does not generate problems since only the binding is eliminated. And finally, by not using the objectName, the dependency does not exist.


So I prefer to use the second method, for more information read Interacting with QML from C++.