How objects are constructed in QML

Making a new type

Here's how you derive a new class from a base class in pure QML, with a public API of two mandatory properties. They fill the role of constructor parameters:

import QtQuick

QtObject {
    required property int amount
    required property url iconSource
}

The name of the new class, or QML component, is derived from the file name. So if the file name is Derived.qml, the new type will be available under Derived. This behaviour can be controlled in a special module file called qmldir.

We can now instantiate our object from the component:

Derived {
    amount: 5
    iconSource: "other/path/to/icon.png"
}

QML object construction could be roughly translated to Python like so:

obj = Derived()
obj.amount = 5
obj.icon_path = "/path/to/icon.png"

What's unusual perhaps is that the constructor takes no arguments. Instead amount and icon_path are set on the object after instantiation.

Multi-phase construction

Note

How the QML engine internally constructs objects is much more complicated. The information presented here serves as a conceptual overview only.

In the first phase of construction, the QML engine calls a hidden constructor and injects the parent argument. We can imagine the constructor to be defined like so:

class Derived(QObject):
    def __init__(self, parent: QObject=None):
        super().__init__(parent)

We now have an instance of our Derived component, but the properties that we declared as required aren't initialised just yet. The QML engine would complain with a fatal error if we tried to use the instance in this state.

The parent points to the object that will take ownership over this new object. In QML, this parent-child relationship is commonly expressed through nesting:

Parent {          // owns the Child instance
    Child { ... } // `parent` property will refer to Parent instance
}

In the second phase, the initial state of the new object is set up. This happens by evaluating the expressions that are bound to the properties, as we already saw:

Derived {
    amount: 5
    iconSource: "other/path/to/icon.png"
}

Evaluation happens property by property and in random order. This has far reaching consequences! Properties can depend on other properties and the QML engine will try its best to resolve the dependencies. It's up to the developer however to prevent circular dependencies:

Derived {
    amount: String(iconSource).length // Don't #1
    iconSource: `path/to/icon-${amount}.png` // Don't #2
}

The QML engine cannot resolve this. Individually, each property and their dependency on the other property would be fine. Combined however, this spells doom and we'll be greeted by the infamous binding loop warning:

QML Derived: Binding loop detected for property "amount"

Not all binding loops can be detected by the engine. When that happens, the application will either hang or crash.

In the third phase, when all bound property expressions have been evaluated once, Component.onCompleted will be called. This allows us to run code after the object has been constructed but before it'll be used by others.

Derived {
    amount: 5
    iconSource: "other/path/to/icon.png"

    Component.onCompleted: console.log(`amount: ${amount}, iconSource: "${iconSource}"`)
}

We can also attach new properties to an existing type:

Derived {
    readonly property string label: "A new property"

    amount: 7
    iconSource: "path/to/icon.png"

    Component.onCompleted: console.log(`label: ${label}`)
}

This triggers an additional construction phase: Because of the new property, the QML engine has to derive a new implicit type from our original Derived component. This new type contains the injected label property. As a new type, it also has its own Component.onCompleted handler which will run after label has been evaluated once.

Recursive construction

It is not guaranteed that all construction phases run one after another. For instance, object A could be created before object B, but properties will be evaluated for B before A's property are checked. Therefore, B could reach full initialisation before A.

In the general case, innermost objects will be constructed before the other objects:

Ancestor { // last to be fully constructed
    Parent { // second to be fully constructed
        Child {} // first to be fully construced
    }
}

In a typical QML application with a graphical interface, the outermost object would be the ApplicationWindow, so it'ill be constructed last. The recursive construction spans all components nested within, with each component following the multi-phase construction process.