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 @Property(QColor, notify=colorChanged, final=True)
21@QmlElement
22class PieChart (QQuickPaintedItem):
23
24 chartCleared = Signal()
25 nameChanged = Signal()
26 colorChanged = Signal()
Then, we emit this signal in setColor()
:
43 @color.setter
44 def color(self, value):
45 if value != self._color:
46 self._color = value
47 self.update()
48 self.colorChanged.emit()
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.
// 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
"""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.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.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)