Reputation: 722
I am largely following the instructions here for using properties and I could just use the Person object given there as the backend, but that is not very useful. I am trying to figure out how to do the following two things:
This is in contrast to simply defining all of these Properties along with their setter and getter on the main backend class (which I was able to do), which is what I mean by modularize in the question.
I modify the Person example from the link to make it something the UI can change and give it an extra attribute for kicks...
person.py
from PySide2.QtCore import QObject, Signal, Property
class Person(QObject):
def __init__(self, name, age):
QObject.__init__(self)
self._name = name
self._age = age
def getName(self):
return self._name
def setName(self, name):
print(f"Setting name to {name}")
self._name = name
def getAge(self):
return self._age
def setAge(self, age):
print(f"Setting age to {age}")
self._age = age
@Signal
def name_changed(self):
pass
@Signal
def age_changed(self):
pass
name = Property(str, getName, setName, notify=name_changed)
age = Property(str, getAge, setAge, notify=age_changed)
Just as an example I'll create two instances of Person. The first instance I have created as a class member. This is not really what I want, but closer resembles the way properties were used in the link. The second instance is what I really want which is that the properties are instance members, so that I can add them from elsewhere in the application at runtime. Neither method currently works
main.py
import sys
from os.path import abspath, dirname, join
from PySide2.QtCore import QObject, Property, Signal
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from person import Person
class Backend(QObject):
def __init__(self):
QObject.__init__(self)
def registerProperty(self, name : str, prop):
setattr(self, name, prop)
person1 = Person("Jill", 29)
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
context = engine.rootContext()
# Instance of the Python object
backend = Backend()
# simulate properties added by another module
backend.registerProperty("person2", Person("Jack", 30))
qmlFile = join(dirname(__file__), 'view3.qml')
engine.load(abspath(qmlFile))
# Expose the Python object to QML
context.setContextProperty("backend", backend)
# I tried this but it did nothing
# context.setContextProperty("backend.jack", backend.jack)
# context.setContextProperty("backend.jill", backend.jill)
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
finally the view3.qml file is simply
import QtQuick 2.0
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtQuick.Window 2.12
ApplicationWindow {
visible: true
ColumnLayout {
TextField {
implicitWidth: 200
onAccepted: {
backend.person1.name = text
}
}
TextField {
implicitWidth: 200
onAccepted: {
backend.person1.age = text
}
}
TextField {
implicitWidth: 200
onAccepted: {
backend.person2.name = text
}
}
TextField {
implicitWidth: 200
onAccepted: {
backend.person2.age = text
}
}
}
}
When I try to set any of the values in the UI the error is always the same (the error appears against the QML file)
TypeError: Value is undefined and could not be converted to an object
Ultimately I would like to have such objects nested to any arbitrary depth. Is there a way to achieve what I am trying to do here? Or am I maybe completely off track with the way I'm setting about this?
Upvotes: 2
Views: 1974
Reputation: 3518
I don't know that I'm qualified to advise you on overall architecture design for GUI apps, but I think I can explain what's going wrong, and suggest a way to do what you describe. Your registerProperty
method adds a Python attribute, but as you've seen, that doesn't make it visible from QML.
The bad news: Qt properties cannot be added to an object after it's created.
The good news: You can create a Qt property that's a list (or a dictionary), and add to it.
One pitfall to be aware of is that to expose a list to QML, you specify its type as 'QVariantList'
. (For dictionaries, use 'QVariantMap'
, and make sure your keys are strings.)
Here's an example of how your Backend class could look. (Using super()
to access the parent class means you don't have to pass self
to its initializer.)
from Pyside2.QtCore import QObject, Property, Signal
class Backend(QObject):
def __init__(self):
super().__init__()
self.people = []
people_changed = Signal('QVariantList')
@Property('QVariantList', notify=people_changed)
def people(self):
return self._people
@value.setter
def people(self, new_people):
self._people = new_people
self.people_changed.emit(new_people)
def add_person(self, name, age):
self.people.append(Person(name, age, self))
# Note: only ASSIGNING will automatically fire the changed
# signal. If you append, you have to fire it yourself.
self.people_changed.emit(self.people)
This will keep QML up to date as you add people; you could also create a similar method to remove people. Parenting each person to the Backend object makes sure Qt will keep references to them as long as your Backend still exists.
For a truly dynamic collection of properties, perhaps you could give your top-level Backend object a dictionary that your other components add to. So backend.people
would become backend.properties['people']
, and a specific module would be responsible for adding that key to the properties
dictionary, then adding to and removing entries from it.
Specifying all those getters and setters is a hassle, isn't it? I spent so long feeling like there must be a better way, and only recently came across a solution here on Stack Overflow. Using this metaclass, your Person class and the Backend I posted above could be simplified to:
from PySide2.QtCore import QObject
# Assuming you save the linked code as properties.py:
from properties import PropertyMeta, Property
class Person(QObject, metaclass=PropertyMeta):
name = Property(str)
age = Property(int)
def __init__(self, name, age, parent=None):
super().__init__(parent)
self.name = name
self.age = age
class Backend(QObject, metaclass=PropertyMeta):
people = Property(list)
def __init__(self):
super().__init__()
self.people = []
def add_person(self, name, age):
self.people.append(Person(name, age, self))
# Automatically emits changed signal, even for in-place changes
(I also changed age
to an int
, but it could still be a str
if you need it to be.)
Upvotes: 1