10

I want to show a rectangle in Qml and I want to change the rectangle's properties(width, length) from my python code. In fact, there is a socket connection in the python code, through which the values of width and length are received from another computer. To put it simple: another user should be able to adjust this rectangle in real-time. I know how to make a socket connection in my python file and using PyQt5, I can show the qml file from python.

However, I am in trouble to access the rectangle's parameters through my python code. How can I do that?

This is a simplified sample of my qml file:

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"
    }
}

And here is what I have written in my .py file:

from PyQt5.QtQml import QQmlApplicationEngine, QQmlProperty
from PyQt5.QtQuick import QQuickWindow, QQuickView
from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtWidgets import QApplication
import sys
def run():
    myApp = QApplication(sys.argv)
    myEngine = QQmlApplicationEngine()

    myEngine.load('mainViewofHoomanApp.qml')


    if not myEngine.rootObjects():
        return -1
    return myApp.exec_()

if __name__ == "__main__":
    sys.exit(run())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
HoOman
  • 455
  • 1
  • 7
  • 15

3 Answers3

18

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++.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thank you for your response. I used a similar method, but the problem is that when the myApp.exec_() is called, the graphical interface will no longer react to python scripts.For example, if I have a ((while True:)) loop where the python is listening to the server to receive the demanded width of the rectangle. And if I write the while loop before calling myApp.exec_(), The graphical interface would never be shown – HoOman Feb 17 '19 at 06:48
  • 1
    @HoOman Locking tasks such as the infinite loop must be executed in another thread because as you point out it will freeze the GUI and send the data to the GUI thread through signals, another approach is to use QtNetwork to implement the sockets since they do not block the event loop of Qt. I could help you solve the problem if you provide a [mcve]. On the other hand if my answer helps you do not forget to mark it as correct. :-) – eyllanesc Feb 17 '19 at 06:54
  • 1
    @HoOman I recommend posting a new question for your problem with the socket :-) – eyllanesc Feb 17 '19 at 06:57
  • Sorry, I thought that I had marked your answer as correct. Done – HoOman Feb 17 '19 at 06:57
  • Dear @eyllanesc I have created a new post and have written my question there: https://stackoverflow.com/questions/54731102/how-to-make-a-qml-objects-property-dynamically-updatable-through-socket-conne – HoOman Feb 17 '19 at 07:34
  • @eyllanesc This is a fantastic answer and goes a long way towards my understanding of how to interact between QML and Python in general. I'm bothered by all the examples that use FindChild. Do you have any suggestions as far as general reading along these lines. I can read C++ examples, but what I really want is a deeper philosophical understand of how the business layers and QML are designed to interact. – Andrew Voelkel Oct 31 '20 at 00:10
  • I have same code as you in qml and im searching for child the same way but every time it finds None childs – Jiri Otoupal イり オトウパー Apr 04 '21 at 14:03
2

Try some thing like below (Not tested, but will give you an idea).

create some objectname for rectangle as shown below:

Rectangle {
        id: rectangle
        x: 187
        y: 92
        width: 200
        height: 200
        color: "blue"
        objectName: "myRect"
    }

Interact with QML and find your child, then set the property.

    #INTERACT WITH QML
    engine = QQmlEngine()
    component = QQmlComponent(engine)
    component.loadUrl(QUrl('mainViewofHoomanApp.qml'))
    object = component.create()

    #FIND YOUR RECTANGLE AND SET WIDTH
    child = object.findChild(QObject,"myRect")
    child.setProperty("width", 500)  
Pavan Chandaka
  • 11,671
  • 5
  • 26
  • 34
  • Thank you for your response. I applied what you recommended. However, when the myApp.exec_() is called, the graphical interface will no longer react to python code lines that are written after myAPP.exec_(). Indeed, I have a ((while True:)) loop where the python is listening to the server to receive the demanded width of the rectangle. And if I write the while loop before calling myApp.exec_(), The graphical interface would never be shown – – HoOman Feb 17 '19 at 06:51
1

This is what I do in PySide6 (6.2.4 at the point of testing this):

If I have a custom property defined in QML like this:

import pyobjects

CustomPyObject {
    id: iTheMighty

    property var myCustomProperty: "is awesome"

    Component.onCompleted: iTheMighty.subscribe_to_property_changes()
}

I define my python object like this:

QML_IMPORT_NAME = "pyobjects"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
class CustomPyObject(QQuickItem):

    @Slot()
    def subscribe_to_property_changes(self):
        self.myCustomPropertyChanged.connect(
            lambda: self.on_my_custom_property_changed(self.property("myCustomProperty"))
        )
        # or
        self.myCustomPropertyChanged.connect(
            lambda: self.on_my_custom_property_changed(QQmlProperty(self, "myCustomProperty").read())
        )

    def on_my_custom_property_changed(self, new_value):
        print("Got new value", new_value)

This way I get notified whenever a Qml property changes. Subscribing in the constructor of CustomPyObject is not possible as the custom property will only be ready after the object was created. Hence, the Component.onCompleted trigger.

trin94
  • 170
  • 8