Example 2: Binding Loop Not Related To Sizing a QML object
Most binding loop examples uses sizing and positioning to describe the QML binding loop predicament. The following is an example where a binding loop is evinced without relation to sizing and positioning of QML objects.This article is a continuation from this article:
https://qmlpoison.blogspot.com/2020/05/yet-another-qml-post.html
Let us write a QML application that simulates a flashing light:
- When the application is launched, the rectangle is red
- When a button is clicked, the rectangle will turn yellow, then turn green.
- And the cycle continues: yellow-green-yellow-green-yellow-green...
The image below shows the startup state of the flashing light application. When the button is clicked, the red rectangle will flash yellow-green continuously.
Incorrect Code With Binding Loop Error
The following is code with binding loop error.
File: main.qml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 2.0 Window { visible: true width: 640 height: 480 title: qsTr("Hello World") readonly property color redColor: Qt.rgba(1, 0, 0, 1) readonly property color yellowColor: Qt.rgba(1, 1, 0, 1) readonly property color greenColor: Qt.rgba(0, 1, 0, 1) property color currentColor: redColor onCurrentColorChanged: { if (currentColor == yellowColor) currentColor = greenColor } Button { x: 20 y: 20 width: 100 height: 50 text: "Click Me" onClicked: { currentColor = greenColor } } Rectangle { color: currentColor x: 20 y: 100 height: 100 width: 200 onColorChanged: { if (currentColor == greenColor) currentColor = yellowColor } } } |
For a binding loop to occur, there has to be at least two bindings. Let us first identify the two bindings. They are:
When the button is clicked:
- Line 15: currentColor is bound to redColor
- Line 34: color is bound to currentColor
When the button is clicked:
- Line 29: executes, and currentColor is changed to greenColor
- Line 18: executes because it is a signal handler for currentColor. However, because it only checks for yellowColor, no code will be executed
- Line 34: Rectangle's color will be set to greenColor
When Rectangle's color changes, the signal handler onColorChanged on line 41-42 will execute:
- Line 42: currentColor is set to yellowColor
When currentColor changes, signal handler onCurrentColorChange fires:
- Line 18: currentColor is set to greenColor
Rectangle's color is bound to currentColor:
- Line 34: Rectangle's color is set to greenColor
When Rectangle's color changes, the signal handler onColorChanged on line 41-42 will execute:
- Line 42: currentColor is set to yellowColor
When currentColor changes, signal handler onCurrentColorChange fires:
- Line 18: currentColor is set to greenColor
Now you see the pattern. A change in currentColor causes Rectangle's color to change. This in turn causes another change in currentColor, which will cause Rectangle's color to change again.
When a bound property changes two or more times during the same change cycle, QML will flag a binding loop error.
When a bound property changes two or more times during the same change cycle, QML will flag a binding loop error.
Correct Code Without Binding Loop Error
The following is code without binding loop error.
File:main.qml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 2.0 Window { visible: true width: 640 height: 480 title: qsTr("Hello World") readonly property color redColor: Qt.rgba(1, 0, 0, 1) readonly property color yellowColor: Qt.rgba(1, 1, 0, 1) readonly property color greenColor: Qt.rgba(0, 1, 0, 1) property color currentColor: redColor onCurrentColorChanged: { if (currentColor == yellowColor) currentColor = greenColor if (currentColor == greenColor) currentColor = yellowColor } Button { x: 20 y: 20 width: 100 height: 50 text: "Click Me" onClicked: { currentColor = greenColor } } Rectangle { color: currentColor x: 20 y: 100 height: 100 width: 200 onColorChanged: { } } } |
Button triggers a change in currentColor when it is clicked:
- Line 30: currentColor is set to greenColor
- Line 17: Signal handler of onCurrentColorChanged will fire
- Line 18: If currentColor is greenColor, currentColor is set to yellowColor
- Line 19: If currentColor is yellowColor, currentColor is set to greenColor
- And the cycle continues
Line 35: Binds to currentColor and gets an update of the latest currentColor through its signal handler. Rectangle.color does not cause a change in any other property that will cause cause Rectangle.color to change again.
A Hack: Unsanctioned Method To Avoid Binding Loop Error
The following is an alternative to fixing the binding loop error. It works. But it is a hack. It is unsanctioned. Use at your own peril.
The hack uses the function Qt.callLater(). The function, Qt.callLater(), places the callback function into the back of the event queue. It will be called when all existing events are handled. What this does is it will break the update cycle.
Rectangle.color will not be updated twice within the same update cycle because the change has been rescheduled to a later event.
File: main.qml
Note that this code is exactly the same as the code that generated the binding loop error with one exception:
What you get as a bonus using Qt.callLater() is Qt.callLater() will remove redundant calls:
In the case of this example, multiple calls to
Happy eradicating binding loop from your code base.
The hack uses the function Qt.callLater(). The function, Qt.callLater(), places the callback function into the back of the event queue. It will be called when all existing events are handled. What this does is it will break the update cycle.
Rectangle.color will not be updated twice within the same update cycle because the change has been rescheduled to a later event.
File: main.qml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 2.0 Window { visible: true width: 640 height: 480 title: qsTr("Hello World") readonly property color redColor: Qt.rgba(1, 0, 0, 1) readonly property color yellowColor: Qt.rgba(1, 1, 0, 1) readonly property color greenColor: Qt.rgba(0, 1, 0, 1) property color currentColor: redColor onCurrentColorChanged: { if (currentColor == yellowColor) currentColor = greenColor } Button { x: 20 y: 20 width: 100 height: 50 text: "Click Me" onClicked: { currentColor = greenColor } } Rectangle { color: currentColor x: 20 y: 100 height: 100 width: 200 onColorChanged: { if (currentColor == greenColor) Qt.callLater( () => {currentColor = yellowColor} ) } } } |
Note that this code is exactly the same as the code that generated the binding loop error with one exception:
- Line 42: Delay setting currentColor to yellowColor using Qt.callLater()
What you get as a bonus using Qt.callLater() is Qt.callLater() will remove redundant calls:
When this function is called multiple times in quick succession with the same function as its first argument, that function will be called only once.For an extended description of the function, read https://doc.qt.io/qt-5/qml-qtqml-qt.html#callLater-method.
In the case of this example, multiple calls to
() => {currentColor = yellowColor}will be placed on the event queue and the QML engine will only execute the last call. This effectively removes the "infinite looping" of the binding loop!
Happy eradicating binding loop from your code base.
Disclaimer
The flashing light example above will not flash the rectangle because the code did not yield processing to the QML engine to paint the Rectangle. However, it serves to demonstrate the binding loop error.
Further, the flashing light example will continue to trigger a change in color until the application runs out of stack space. Again, it servers to demonstrate the binding loop error only.
No comments:
Post a Comment