Mark
Mark

Reputation: 722

How to modularize property creation in PySide

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:

  1. Use multiple instances of multiple such classes in a backend and connect them in a way that PySide/QML doesn't complain
  2. Allow customization of the backend by modules to be determined at runtime (i.e. I eventually want to componentize the application - have different components implementing an interface, the component contributes separately to both the GUI and the backend; but this question concerns only the backend)

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

Answers (1)

CrazyChucky
CrazyChucky

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

Related Questions