Extending QML - Adding Property Bindings

This is the third of a series of 6 examples forming a tutorial about extending QML with Python.

Property binding is a powerful feature of QML that allows values of different types to be synchronized automatically. It uses signals to notify and update other types’ values when property values are changed.

Let’s enable property bindings for the color property. That means if we have code like this:

 7Item {
 8    width: 300; height: 200
 9
10    Row {
11        anchors.centerIn: parent
12        spacing: 20
13
14        PieChart {
15            id: chartA
16            width: 100; height: 100
17            color: "red"
18        }
19
20        PieChart {
21            id: chartB
22            width: 100; height: 100
23            color: chartA.color
24        }
25    }
26
27    MouseArea {
28        anchors.fill: parent
29        onClicked: { chartA.color = "blue" }
30    }
31
32    Text {
33        anchors {
34            bottom: parent.bottom;
35            horizontalCenter: parent.horizontalCenter;
36            bottomMargin: 20
37        }
38        text: "Click anywhere to change the chart color"
39    }
40}

The color: chartA.color statement binds the color value of chartB to the color of chartA. Whenever chartA ‘s color value changes, chartB ‘s color value updates to the same value. When the window is clicked, the onClicked handler in the MouseArea changes the color of chartA , thereby changing both charts to the color blue.

It’s easy to enable property binding for the color property. We add a notify parameter to its Property decorator to indicate that a colorChanged signal is emitted whenever the value changes.

39
21
22@QmlElement
23class PieChart (QQuickPaintedItem):
24
25    chartCleared = Signal()
26    nameChanged = Signal()

Then, we emit this signal in setColor():

43
44    @color.setter
45    def color(self, value):
46        if value != self._color:
47            self._color = value
48            self.update()

It’s important for setColor() to check that the color value has actually changed before emitting colorChanged(). This ensures the signal is not emitted unnecessarily and also prevents loops when other types respond to the value change.

The use of bindings is essential to QML. You should always add notify signals for properties if they are able to be implemented, so that your properties can be used in bindings. Properties that cannot be bound cannot be automatically updated and cannot be used as flexibly in QML. Also, since bindings are invoked so often and relied upon in QML usage, users of your custom QML types may see unexpected behavior if bindings are not implemented.

Download this example

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import Charts
import QtQuick

Item {
    width: 300; height: 200

    Row {
        anchors.centerIn: parent
        spacing: 20

        PieChart {
            id: chartA
            width: 100; height: 100
            color: "red"
        }

        PieChart {
            id: chartB
            width: 100; height: 100
            color: chartA.color
        }
    }

    MouseArea {
        anchors.fill: parent
        onClicked: { chartA.color = "blue" }
    }

    Text {
        anchors {
            bottom: parent.bottom;
            horizontalCenter: parent.horizontalCenter;
            bottomMargin: 20
        }
        text: "Click anywhere to change the chart color"
    }
}
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

"""PySide6 port of the qml/tutorials/extending-qml/chapter3-bindings example from Qt v5.x"""

import os
from pathlib import Path
import sys

from PySide6.QtCore import Property, Signal, Slot, QUrl, Qt
from PySide6.QtGui import QGuiApplication, QPen, QPainter, QColor
from PySide6.QtQml import QmlElement
from PySide6.QtQuick import QQuickPaintedItem, QQuickView

# To be used on the @QmlElement decorator
# (QML_IMPORT_MINOR_VERSION is optional)
QML_IMPORT_NAME = "Charts"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
class PieChart (QQuickPaintedItem):

    chartCleared = Signal()
    nameChanged = Signal()
    colorChanged = Signal()

    def __init__(self, parent=None):
        QQuickPaintedItem.__init__(self, parent)
        self._name = u''
        self._color = QColor()

    def paint(self, painter):
        pen = QPen(self._color, 2)
        painter.setPen(pen)
        painter.setRenderHints(QPainter.RenderHint.Antialiasing, True)
        painter.drawPie(self.boundingRect().adjusted(1, 1, -1, -1), 90 * 16, 290 * 16)

    @Property(QColor, notify=colorChanged, final=True)
    def color(self):
        return self._color

    @color.setter
    def color(self, value):
        if value != self._color:
            self._color = value
            self.update()
            self.colorChanged.emit()

    @Property(str, notify=nameChanged, final=True)
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @Slot()  # This should be something like @Invokable
    def clearChart(self):
        self.color = Qt.transparent
        self.update()
        self.chartCleared.emit()


if __name__ == '__main__':
    app = QGuiApplication(sys.argv)

    view = QQuickView()
    view.setResizeMode(QQuickView.SizeRootObjectToView)
    qml_file = os.fspath(Path(__file__).resolve().parent / 'app.qml')
    view.setSource(QUrl.fromLocalFile(qml_file))
    if view.status() == QQuickView.Status.Error:
        sys.exit(-1)
    view.show()
    res = app.exec()
    # Deleting the view before it goes out of scope is required to make sure all child QML instances
    # are destroyed in the correct order.
    del view
    sys.exit(res)