Qt Bindable Properties
Qt provides bindable properties. Bindable properties are properties which either have a value or are specified using any C++ function, typically a C++ lambda expression. In case they are specified using a C++ function, they are updated automatically whenever their dependencies change.
Bindable properties are implemented in the class QProperty, which consists of the data object and a pointer to a management data structure, and in class QObjectBindableProperty, which consists only of the data object and uses the encapsulating QObject to store the pointer to the management data structure.
Why Use Bindable Properties?
Property bindings are one of the core features of QML. They allow to specify relationships between different object properties and automatically update properties' values whenever their dependencies change. Bindable properties allow to achieve the same not only in QML code, but also in C++. Using bindable properties can help to simplify your program, by eliminating a lot of boilerplate code for tracking and reacting to dependency updates of different objects.
The Introductory Example below demonstrates the usage of bindable properties in C++ code. You can also check Bindable Properties example to see how the bindable properties can help to improve your code.
Introductory Example
The binding expression computes the value by reading other QProperty values. Behind the scenes this dependency is tracked. Whenever a change in any property's dependency is detected, the binding expression is re-evaluated and the new result is applied to the property. For example:
QProperty<QString> firstname("John"); QProperty<QString> lastname("Smith"); QProperty<int> age(41); QProperty<QString> fullname; fullname.setBinding([&]() { return firstname.value() + " " + lastname.value() + " age: " + QString::number(age.value()); }); qDebug() << fullname.value(); // Prints "John Smith age: 41" firstname = "Emma"; // Triggers binding reevaluation qDebug() << fullname.value(); // Prints the new value "Emma Smith age: 41" // Birthday is coming up age.setValue(age.value() + 1); // Triggers re-evaluation qDebug() << fullname.value(); // Prints "Emma Smith age: 42"
When a new value is assigned to the firstname
property, the binding expression for fullname
is reevaluated. So when the last qDebug()
statement tries to read the name value of the fullname
property, the new value is returned.
Since bindings are C++ functions, they may do anything that's possible in C++. This includes calling other functions. If those functions access values held by QProperty, they automatically become dependencies to the binding.
Binding expressions may use properties of any type, so in the above example the age is an integer and folded into the string value using conversion to integer, but the dependency is fully tracked.
Bindable Property Getters and Setters
When a class has a bindable property, either using QProperty or QObjectBindableProperty, special care has to be taken when formulating getters and setters for that property.
Bindable Property Getters
To ensure proper operation of the automatic dependency-tracking system, every possible code path in a getter needs to read from the underlying property object. In addition, the property must not be written inside the getter. Design patterns which recompute or refresh anything in the getter are not compatible with bindable properties.
It is therefore recommended to only use trivial getters with bindable properties.
Bindable Property Setters
To ensure proper operation of the automatic dependency-tracking system, every possible code path in a setter needs to write to the underlying property object, even if the value did not change.
Any other code in a setter has a high propability of being incorrect. Any code doing updates based on the new value is most likely a bug, as this code won't be executed when the property is changed through a binding.
It is therefore recommended to only use trivial setters with bindable properties.
Writing to a Bindable Property
Bindable properties inform their dependent properties about each change. This might trigger change handlers, which in turn might call arbitrary code. Thus, every write to a bindable property has to be inspected carefully. The following problems might occur.
Writing Intermediate Values to Bindable Properties
Bindable properties must not be used as variables in algorithms. Each value written would be communicated to dependent properties. For example, in the following code, other properties that depend on myProperty would be first informed about the change to 42, then about the change to maxValue.
myProperty = somecomputation(); // returning, say, 42 if (myProperty.value() > maxValue) myProperty = maxValue;
Instead, perform the computation in a separate variable. Correct usage is shown in the following example.
int newValue = someComputation(); if (newValue > maxValue) newValue = maxValue; myProperty = newValue; // only write to the property once
Writing Bindable Properties in Transitional States
When a bindable property is a member of a class, each write to that property might expose the current state to the outside. So bindable properties must not be written in transient states, when class invariants are not met.
For example, in a class representing a circle which holds two members radius and area consistent, a setter might look like this (where radius is a bindable property):
void setRadius(double newValue) { radius = newValue; // this might trigger change handlers area = M_PI * radius * radius; emit radiusChanged(); }
Here, code triggered in change handlers might use the circle, while it has the new radius, but still the old area.
Bindable Properties with Virtual Setters and Getters
Property setters and getters should normally be minimal and do nothing but setting the property; hence it is not normally appropriate for such setters and getters to be virtual. There is nothing it makes sense for the derived class to do.
However some Qt classes can have properties with virtual setters. When subclassing such a Qt class, overriding such setters requires special care.
In any case the base implementation must be called for the binding to work correctly.
The following illustrates this approach.
void DerivedClass::setValue(int val) { // do something BaseClass::setValue(val); // probably do something else }
All the common rules and recommendations regarding writing to bindable properties also apply here. As soon as the base class implementation is called, all the observers are notified about the change to the property. This means that class invariants must be met before calling the base implementation.
In the rare case where such virtual getters or setters are necessary, the base class should document the requirements it imposes on overrides.
Formulating a Property Binding
Any C++ expression evaluating to the correct type can be used as a binding expression and be given to the setBinding() method. However, to formulate a correct binding, some rules must be followed.
Dependency tracking only works on bindable properties. It's the developer's responsibility to ensure that all properties used in the binding expression are bindable properties. When non-bindable properties are used in a binding expression, changes to those properties do not trigger updates to the bound property. No warning or error is generated either at compile-time or at run-time. The bound property will be updated only when bindable properties used in the binding expression are changed. Non-bindable properties might be used in a binding if it's possible to ensure that markDirty is called on the property being bound on each change of the non-bindable dependency.
The bound property might evaluate its binding several times during its lifetime. The developer must make sure that all objects used in the binding expression live longer than the binding.
The bindable property system is not thread-safe. Properties used in the binding expression on one thread must not be read or modified by any other thread. An object of a QObject-derived class which has a property with a binding must not be moved to a different thread. Also, an object of a QObject-derived class which has a property which is used in a binding must not be moved to a different thread. In this context, it's irrelevant whether it's used in a binding of a property in the same object or in a binding of a property in another object.
The binding expression should not read from the property it's a binding for. Otherwise, an evaluation loop exists.
The binding expression must not write to the property it's a binding for.
Functions used as bindings as well as all code which is called inside a binding must not co_await. Doing so can confuse the property system's tracking of dependencies.
Bindable Properties and Multithreading
Bindable properties are not threadsafe, unless stated otherwise. A bindable property must not be read or modified by any thread other than the one is was created in.
Tracking Bindable Properties
Sometimes the relationships between properties cannot be expressed using bindings. Instead you may need to run custom code whenever the value of a property changes and instead of assigning the value to another property, pass it to other parts of your application. For example writing data into a network socket or printing debug output. QProperty provides two mechanisms for tracking.
You can register for a callback function to be called whenever the value of a property changes, by using onValueChanged(). If you want the callback to also be called for the current value of the property, register your callback using subscribe() instead.
Interaction with Q_PROPERTYs
A Q_PROPERTY that defines BINDABLE
can be bound and used in binding expressions. You can implement such properties using QProperty, QObjectBindableProperty, or QObjectComputedProperty.
Q_PROPERTYs without BINDABLE
can also be bound and be used in binding expressions, as long as they define a NOTIFY
signal. You must wrap the property in a QBindable using the QBindable(QObject* obj, const char* property)
constructor. Then, the property can be bound using QBindable::setBinding() or used in a binding expression via QBindable::value(). You must use QBindable::value()
in binding expressions instead of the normal property READ
function (or MEMBER
) to enable dependency tracking if the property is not BINDABLE
.
© 2024 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.