1

I am working with PySide2 and QML. When I define a QtCore.Property in a Python object, it successfully sets up access for QML as well as the Python property get/set methods. However, I have a situation where it would be useful to attach the Property after defining the class, and this isn't working as I would expect.

First, an example that DOES work:

import sys
from PySide2.QtCore import QObject, Property, Signal
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine

class Person(QObject):
    def get_name(self):
        print('trying to access name')
        return self._name

    def set_name(self, new_name):
        print('trying to set name')
        self._name = new_name

    name = Property(str, get_name, set_name)
    
bob = Person()
bob.name = 'Bob'
print(f'Name: "{bob.name}"')
print(bob.__dict__)

app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty('backend', bob)
engine.load("example.qml")
sys.exit(app.exec_())

And a simple QML file to view:

import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    visible: true
    
    Component.onCompleted: { console.debug(JSON.stringify(backend)) }
    
    Text { text: backend.name }
}

The resulting graphical window displays "Bob", and the console output includes:

trying to set name
trying to access name
Name: "Bob"
{'_name': 'Bob'}
qml: {"objectName":"","name":"Bob"}

However, with what seems like a simple change, things get confusing. I replaced the Python class definition with:

class Person(QObject):
    pass
    
def get_name(obj):
    print('trying to access name')
    return obj._name

def set_name(obj, new_name):
    print('trying to set name')
    obj._name = new_name

Person.name = Property(str, get_name, set_name)

That is, the getter, setter, and Property are defined outside of the class definition, and attached to it after the fact. What's strange is that QML can't see the Property, but Python still knows about its correct use:

trying to set name
trying to access name
Name: "Bob"
{'_name': 'Bob'}
qml: {"objectName":""}
file:///Users/charles/Projects/qt/property-test/example.qml:9:9: Unable to assign [undefined] to QString

I'm scratching my head as to how QtCore.Property is correctly setting up the Python property, but not the QML one. Is there something I'm missing about its implementation?

CrazyChucky
  • 3,263
  • 4
  • 11
  • 25

1 Answers1

2

TL; DR; QProperties cannot be defined in runtime.


QML doesn't use Python introspection to access QObjects properties, instead Qt has its own introspection implemented in QMetaObject, and that can be seen in the following code:

class Person(QObject):
    def get_name(self):
        return self._name

    def set_name(self, new_name):
        self._name = new_name

    name = Property(str, get_name, set_name)


bob = Person()
bob.name = "Bob"

print("qproperties:")
print("============")
mo = bob.metaObject()
for i in range(mo.propertyOffset(), mo.propertyCount()):
    prop = mo.property(i)
    print(f"{i}: {prop.name()} = {prop.read(bob)}")

Output:

qproperties:
============
1: name = Bob
class Person(QObject):
    pass


def get_name(obj):
    return obj._name


def set_name(obj, new_name):
    obj._name = new_name


Person.name = Property(str, get_name, set_name)


bob = Person()
bob.name = "Bob"

print("qproperties:")
print("============")
mo = bob.metaObject()
for i in range(mo.propertyOffset(), mo.propertyCount()):
    prop = mo.property(i)
    print(f"{i}: {prop.name()} = {prop.read(bob)}")

Output:

qproperties:
============

PySide2(also PyQt5) creates the QProperties when the class is built, so if you add QProperties after the class is built, the QMetaObject will not be added.


An alternative is to use QQmlPropertyMap:

import sys
from PySide2.QtCore import QTimer
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine, QQmlPropertyMap

person_map = QQmlPropertyMap()
person_map.insert("name", "Bob")

app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.rootContext().setContextProperty("backend", person_map)
engine.load("example.qml")

QTimer.singleShot(1000, lambda: person_map.setProperty("name", "Joe"))
sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241