Overcoming trivially constructable types
Problem statement
We already learned [1] that as a consequence of how new types are defined, QML components have to be trivially constructable: Besides the hidden parent parameter, no other parameter can be injected directly into the constructor. Instead, we have to use properties to set up the object.
Let's recap object instantiation in QML:
Derived { amount: 5 iconSource: "other/path/to/icon.png" }
But what if Derived was a Python class, defined in our backend? Then the restriction on trivial constructors would quickly become annoying. Often enough, you want to inject dependencies through constructor parameters [2].
On top, there is a functional difference between waiting for property evaluation before the object is fully usuable and having immediately useful objects right after construction, and sometimes, we really need the latter.
So how can we have our cake and eat it too?
Factory methods and singletons
Factories? Singletons? Probably not the cake we asked for, but it's arguably the best trade-off available in this situation.
QML mainly interacts with backend objects through properties and slots. Slots make Python methods directly callable for QML. Here's how:
@Slot(type0, type1, ..., result=typeN)
def callMe(self, param0: type0, param1: type1, ...) -> typeN:
...
Then in QML, we could have for instance:
otherProp: MyObject {}
someProp: {
const result = otherProp.callMe(arg0, arg1, ...);
return result;
}
Note
The right-hand side of a property binding in QML is a JavaScript expression. So whenever we need to write some imperative code, we can simply make a new property and bind a JavaScript code block to it. If we wrap the code block in curly braces, we also need to explicitly return a result.
It's easy to see how inside the slot, we could construct a new object and return that instead. Nothing of how slots work in PySide6 prevents us from doing that. But we'll need a separate class to store the slot method. That's where the factory pattern comes in. Here, it's simply another QObject-based type:
@QmlElement @QmlSingleton class Factory(QObject): @Slot(QUrl, int, result=Derived) def makeDerived(self, icon_source: QUrl, amount: int) -> Derived: return Derived(icon_source, amount, self)
We only need one factory instance, so we might as well turn it into a singleton, from QML's perspective. The QmlSingleton decorator [3] does just that but it'll only work if we don't provide our own __init__() method for the factory.
The QML singleton simplifies the factory usage on the QML side, as we don't need to create a factory instance ourselves:
readonly property Derived derived: Factory.makeDerived("path/to/icon.png", 5)
Here's the complete implemention of the factory and the Derived class in Python:
from PySide6.QtCore import Property, QObject, Qt, QUrl, Signal, Slot from PySide6.QtQml import QmlElement, QmlSingleton QML_IMPORT_NAME = "MyApp.Tools" QML_IMPORT_MAJOR_VERSION = 1 @QmlElement class Derived(QObject): iconSourceChanged = Signal(QUrl) amountChanged = Signal(int) def __init__(self, icon_source: QUrl, amount: int, parent: QObject = None): super().__init__(parent) self._icon_source = icon_source self._amount = amount @Property(QUrl, notify=iconSourceChanged) def iconSource(self): return self._icon_source @Property(int, notify=amountChanged) def amount(self): return self._amount @QmlElement @QmlSingleton class Factory(QObject): # By introducing optional parameters, we've created an overloaded method # issue that the QML engine cannot resolve unless we define `@Slot` # decorators for each case. @Slot(QUrl, int, result=Derived) @Slot(QUrl, int, QObject, result=Derived) def makeDerived( self, icon_source: QUrl, amount: int, parent: QObject = None ) -> Derived: # We need to decide on an owner, not just for the QML engine. # Otherwise, we risk that the instance we want to return will # immediately be cleaned up by Python's garbage collector. owner = parent if parent else self return Derived(icon_source, amount, owner)
And here's how we can make use of them in QML:
import QtQuick import QtQuick.Controls import MyApp.Tools as Tools ApplicationWindow { id: root readonly property Tools.Derived derived: { const url = Qt.resolvedUrl("path/to/icon.png"); return Tools.Factory.makeDerived(url, 5); } title: "Overcoming Trivially Constructable Types" width: 1280 height: 800 visible: true Flow { anchors.centerIn: parent flow: Flow.TopToBottom Text { text: `root.derived.amount: ${root.derived.amount}` } Text { text: `root.derived.iconSource: ${root.derived.iconSource}` } } }
Conclusion
In terms of use, both the declaratively created instance in the beginning of the article and the Derived instance we got from the factory are similar. Both instances also got created by the QML engine, which becomes an important consideration should your application use multithreading.
Where the former was defined as a pure QML type however, the latter offers us additional control and more implementation flexibility through the Python backend.