All about property bindings in Qt 6.2
关于Qt6.2中的所有属性绑定
Thursday December 23, 2021 by Sona Kurazyan | Comments
2021年12月23日星期四由 Sona Kurazyan 发表评论
Qt 6 introduced bindable properties a while ago. Based on our experience and feedback we got after the 6.0 release, we further improved the underlying engine. In this post we will give the overview of the bindable properties, see what has changed since our last update on property bindings, and discuss why you may want to use the new property bindings in your C++ code.
Qt6不久前引入了可绑定属性。根据我们在6.0发布后获得的经验和反馈,我们进一步改进了底层引擎。在本文中,我们将给出可绑定属性的概述,看看自从上次对属性绑定的更新以来发生了什么变化,并讨论为什么您可能希望在C++代码中使用新的属性绑定。
Overview of bindable properties
可绑定属性概述
Let's first review what bindable properties are, and the motivation behind them.
As readers might already know, property bindings are one of the core features of QML that allow to specify relationships between different object properties. When any dependency of a property changes, the value of the property is automatically updated. The new bindable property system brings this concept into C++ code. Consider the following simple QML example:
让我们首先回顾一下什么是可绑定属性,以及它们背后的动机。
读者可能已经知道,属性绑定是QML的核心特性之一,它允许指定不同对象属性之间的关系。当属性的任何依赖项发生更改时,该属性的值将自动更新。新的绑定属性系统将这个概念引入C++代码中。考虑下面的简单QML示例:
Rectangle {
height: 10
width: 20
area: height * width
}
The equivalent C++ with bindable properties would be:
具有可绑定属性的等效C++代码:
struct Rectangle
{
QProperty<int> height { 10 };
QProperty<int> width { 20 };
QProperty<int> area;
Rectangle() { area.setBinding([&] { return height * width; }); }
};
If width or height changes, area will be automatically recalculated:
如果宽度或高度发生变化,面积将自动重新计算:
Rectangle rect; // rect.area == 200
rect.width = 5; // rect.area == 50
QProperty also allows adding observers, to get notified as soon as its value changes:
QProperty还允许添加观察员,以便在其值更改时立即收到通知:
auto notifier = rect.area.addNotifier([&] {
qDebug()<<"Area changed:"<<rect.area;
});
rect.height = 20; // This will output "Area changed: 100"
Notice that there's no signal-slot connection involved, and Rectangle doesn't depend on the metaobject system. Of course, if you want, you can still expose your properties to the metaobject system (for example, if you want to make the property available also in QML) by marking your Q_PROPERTYs with the BINDABLE keyword:
请注意,不涉及信号槽连接,Rectangle不依赖于元对象系统。当然,如果您愿意,您仍然可以通过使用BINDABLE关键字标记Q_PROPERTY来向元对象系统公开您的属性(例如,如果您希望使该属性在QML中也可用):
class Rectangle : public QObject {
Q_OBJECT
Q_PROPERTY(int height READ height WRITE setHeight NOTIFY heightChanged BINDABLE bindableHeight)
Q_PROPERTY(int width READ width WRITE setWidth NOTIFY widthChanged BINDABLE bindableWidth)
Q_PROPERTY(int area READ area NOTIFY areaChanged BINDABLE bindableArea)
public:
Rectangle() { m_area.setBinding([&] { return m_height * m_width; }); }
void setHeight(int h) { m_height = h; }
int height() const { return m_height; }
QBindable<int> bindableHeight() { return &m_height; }
void setWidth(int w) { m_width = w; }
int width() const { return m_width; }
QBindable<int> bindableWidth() { return &m_width; }
int area() const { return m_area; }
QBindable<int> bindableArea() { return &m_area; }
signals:
void heightChanged();
void widthChanged();
void areaChanged();
private:
Q_OBJECT_BINDABLE_PROPERTY(Rectangle, int, m_height, &Rectangle::heightChanged)
Q_OBJECT_BINDABLE_PROPERTY(Rectangle, int, m_width, &Rectangle::widthChanged)
Q_OBJECT_BINDABLE_PROPERTY(Rectangle, int, m_area, &Rectangle::areaChanged)
};
As you can see, apart from the BINDABLE keyword, we have a few other additions compared to Qt 5 properties. Firstly, for each property we have a bindable*() method that returns a QBindable. This is needed for retrieving bindings and accessing the functionality available in QProperty, for example:
正如您所看到的,除了BINDABLE关键字之外,与Qt5属性相比,我们还有一些其他的添加。首先,对于每个属性,我们都有一个bindable*()方法,该方法返回一个QBindable。这是检索绑定和访问QProperty中可用功能所必需的,例如:
Rectangle rect;
auto notifier = rect. bindableArea().addNotifier([&] { ... });
Secondly, the properties are declared via Q_OBJECT_BINDABLE_PROPERTY macros, to automatically emit the property-change notifications whenever the value of the property changes. Before setting a new value, the setters don't need to check if the new value actually differs from the old one and emit the property-change signals because the bindable properties already do that behind the scenes.
其次,通过Q_OBJECT_BINDABLE_PROPERTY宏声明属性,以便在属性值更改时自动发出属性更改通知。在设置新值之前,setter不需要检查新值是否与旧值实际不同,并发出属性更改信号,因为可绑定属性已经在幕后执行了此操作。
But everything described here is not new, we had this since Qt 6.0, so let's see what has changed with the latter releases.
但是这里描述的一切都不是新的,我们从Qt6.0开始就有了,所以让我们看看后面的版本有什么变化。
Changes to bindable property system
对可绑定属性系统的更改
The initial implementation of bindable properties was relying on deferred (or lazy) binding evaluation, which means that a property change doesn't immediately trigger re-evaluation of bindings that depend on the property. Instead, the bindings are evaluated lazily, only when the property is read. The advantage is that the bindings that depend on multiple properties won't be re-evaluated every time one of the dependent properties changes. This also results in less property-change notifications, since they aren't sent for intermediate values when evaluating a binding.
可绑定属性的初始实现依赖于延迟(或延时)绑定评估,这意味着属性更改不会立即触发依赖于该属性的绑定的重新评估。相反,只有在读取属性时,才会对绑定进行延迟计算。其优点是,依赖于多个属性的绑定不会在每次一个依赖属性更改时重新计算。这也会导致更少的属性更改通知,因为在评估绑定时,它们不会发送给中间值。
This sounds great, but when porting some of the properties of different Qt modules to the new bindable property system, we realized that for most of the properties deferred evaluation doesn't work well. Most of the existing code requires eager evaluation, and changing it to work with lazy evaluation without breaking the logic requires a lot of modifications and significant rewrites in some cases. This might also be true for the user code. So it was decided to use eager evaluation and add support of grouping property updates together, to minimize unnecessary recalculations and change notifications. For example:
这听起来不错,但是当将不同Qt模块的一些属性移植到新的可绑定属性系统时,我们意识到对于大多数属性,延迟求值并不能很好地工作。大多数现有代码都需要即时的求值,在不破坏逻辑的情况下将其更改为使用惰性求值,在某些情况下需要进行大量修改和重大重写。对于用户代码,这也可能是正确的。因此,决定使用即时评估,并添加对将属性更新分组在一起的支持,以最大限度地减少不必要的重新计算和更改通知。例如:
QProperty<int> a {1};
QProperty<int> b {2};
QProperty<int> c;
c.setBinding([&] { return a + b; });
auto notifier = c.addNotifier([&] {
qDebug()<<"Value of c changed:"<<c.value();
});
Here we have the property c, that depends on a and b. If we change the values of a and b, due to the eager evaluation, the value of c will be updated after each change:
这里我们有属性c,它依赖于a和b。如果我们更改a和b的值,由于即时评估,c的值将在每次更改后更新:
a = 2;
b = 3;
This will output:
这将输出:
Value of c changed: 4
Value of c changed: 5
And c's value will be updated after each assignment. In contrast, if we group the property updates:
c的值将在每次赋值后更新。相反,如果我们将属性更新分组:
Qt::beginPropertyUpdateGroup();
a = 2;
b = 3;
Qt::endPropertyUpdateGroup();
This will output:
这将输出:
Value of c changed: 5
If we read the value of c between Qt::beginPropertyUpdateGroup() and Qt::endPropertyUpdateGroup(), we'll see the old value (3 in this case), and the value change notification will be sent only once, after all property updates inside the group are done.
如果我们读取Qt::beginPropertyUpdateGroup()和Qt::endPropertyUpdateGroup()之间的c值,我们将看到旧值(本例中为3),并且在组内的所有属性更新完成后,值更改通知将只发送一次。
To recap, the major change is using eager evaluation instead of lazy evaluation, with an option to defer the evaluation by grouping together related property updates.
综上所述,主要的变化是使用即时求值而不是延时求值,通过将相关的属性更新分组在一起,可以延时求值。
Bindable properties in action
行为中的可绑定属性
If you're not convinced yet that using bindable properties is a good idea and if it makes sense to port properties in your existing code, let's look at a practical example and see how property bindings can simplify your code.
如果您还不确信,使用可绑定属性是一个好主意,并且在现有代码中移植属性是有意义的,那么让我们看一个实际示例,看看属性绑定如何简化代码。
Let's say we are modeling a subscription system and we want to calculate the cost of the subscription based on the age and country of the user, and the duration of the subscription. So we have the Subscription class:
假设我们正在为订阅系统建模,我们希望根据用户的年龄和国家以及订阅的持续时间来计算订阅的成本。我们有订阅类:
class Subscription : public QObject
{
Q_OBJECT
public:
enum Duration { Monthly = 1, Quarterly = 4, Yearly = 12 };
Subscription(User *user);
void calculatePrice();
int price() const { return m_price; }
Duration duration() const { return m_duration; }
void setDuration(Duration newDuration);
bool isValid() const { return m_isValid; }
void updateValidity();
signals:
void priceChanged();
void durationChanged();
void isValidChanged();
private:
double calculateDiscount() const;
int basePrice() const;
QPointer<User> m_user;
Duration m_duration = Monthly;
int m_price = 0;
bool m_isValid = false;
};
The price is calculated based on the country of the user and the duration of the subscription:
价格根据用户所在国家/地区和订阅持续时间计算:
void Subscription::calculatePrice()
{
const auto oldPrice = m_price;
m_price = qRound(calculateDiscount() * m_duration * basePrice());
if (m_price != oldPrice)
emit priceChanged();
}
int Subscription::basePrice() const
{
return (m_user->country() == User::Norway) ? 100 : 80;
}
double Subscription::calculateDiscount() const
{
switch (m_duration) {
case Monthly:
return 1;
case Quarterly:
return 0.9;
case Yearly:
return 0.6;
}
return -1;
}
When the duration of the subscription changes, Subscription needs to recalculate the price and notify about the duration change:
当订阅的持续时间发生变化时,订阅需要重新计算价格并通知持续时间的变化:
void Subscription::setDuration(Duration newDuration)
{
if (newDuration != m_duration) {
m_duration = newDuration;
calculatePrice();
emit durationChanged();
}
}
Additionally, we want to enable subscription only for the valid countries and users older than 12:
此外,我们希望仅为有效国家/地区和12岁以上的用户启用订阅:
void Subscription::updateValidity()
{
bool isValid = m_isValid;
m_isValid = m_user->country() != User::None && m_user->age() > 12;
if (m_isValid != isValid)
emit isValidChanged();
}
The User class is simpler: it stores country and age of the user, and provides the corresponding getters, setters, and notifier signals:
User类更简单:它存储用户的国家和年龄,并提供相应的getter、setter和notifier信号:
class User : public QObject
{
Q_OBJECT
public:
enum Country { None, Finland, Germany, Norway };
Country country() const { return m_country; }
void setCountry(Country country)
{
if (m_country != country) {
m_country = country;
emit countryChanged();
}
}
int age() const { return m_age; }
void setAge(int age)
{
if (m_age != age) {
m_age = age;
emit ageChanged();
}
}
signals:
void countryChanged();
void ageChanged();
private:
Country m_country = Country::None;
int m_age = 0;
};
To update the price in the UI when any of the parameters change, we need to create the proper signal-slot connections:
要在任何参数更改时更新UI中的价格,我们需要创建适当的信号插槽连接:
User user;
Subscription subscription(&user);
...
QObject::connect(&subscription, &Subscription::priceChanged, [&] {
priceDisplay->setText(QString::number(subscription.price()));
});
QObject::connect(&subscription, &Subscription::isValidChanged, [&] {
priceDisplay->setEnabled(subscription.isValid());
});
QObject::connect(&user, &User::countryChanged, [&] {
subscription.calculatePrice();
subscription.updateValidity();
});
QObject::connect(&user, &User::ageChanged, [&] {
subscription.updateValidity();
});
This works, but isn't ideal. We need to rely on the metaobject system to handle the changes to user and subscription via signal-slot connections. We need to be careful to recalculate the price and update the validity, whenever the country, age, or duration changes. If more dependencies are added in the future, we might end up with "spaghetti" code, which will be hard to extend and maintain.
这是可行的,但并不理想。我们需要依靠元对象系统通过信号槽连接来处理对用户和订阅的更改。每当国家、年龄或期限发生变化时,我们需要小心地重新计算价格并更新有效期。如果将来添加更多依赖项,我们可能会得到“意大利面”代码,这将很难扩展和维护。
Now let's see how the same problem can be solved using the property bindings.
现在,让我们看看如何使用属性绑定解决相同的问题。
class BindableSubscription
{
public:
enum Duration { Monthly = 1, Quarterly = 4, Yearly = 12 };
BindableSubscription(BindableUser *user);
BindableSubscription(const BindableSubscription &) = delete;
int price() const { return m_price; }
QBindable<int> bindablePrice() { return &m_price; }
Duration duration() const { return m_duration; }
void setDuration(Duration newDuration) { m_duration = duration; }
QBindable<Duration> bindableDuration() { return &m_duration; }
bool isValid() const { return m_isValid; }
QBindable<bool> bindableIsValid() { return &m_isValid; }
private:
double calculateDiscount() const;
int basePrice() const;
BindableUser *m_user;
QProperty<Duration> m_duration { Monthly };
QProperty<int> m_price { 0 };
QProperty<bool> m_isValid { false };
};
Instead of the QObject-based Subscription class, we now have BindableSubscription, which wraps its data members inside QProperty classes and doesn't have any signals. The setter for duration is now trivial, and the calculatePrice() and updateValidty() methods for manually triggering the price and validity updates are gone. Instead, the relationships between different properties are specified in BindableSubscription's constructor:
我们现在使用的不是基于QObject的Subscription类,而是BindableSubscription,它将其数据成员包装在QProperty类中,并且没有任何信号。持续时间的setter现在变得微不足道,用于手动触发价格和有效性更新的calculatePrice()和updateValidty()方法也不复存在。相反,不同属性之间的关系在BindableSubscription的构造函数中指定:
BindableSubscription::BindableSubscription(BindableUser *user) : m_user(user)
{
m_price.setBinding([this] { return qRound(calculateDiscount() * m_duration * basePrice()); });
m_isValid.setBinding([this] {
return m_user->country() != BindableUser::None && m_user->age() > 12;
});
}
The User class is also simplified:
User类也被简化:
class BindableUser
{
public:
enum Country { None, Finland, Germany, Norway };
BindableUser() = default;
BindableUser(const BindableUser &) = delete;
Country country() const { return m_country; }
void setCountry(Country country) { m_country = country; }
QBindable<Country> bindableCountry() { return &m_country; }
int age() const { return m_age; }
void setAge(int age) { m_age = age; }
QBindable<int> bindableAge() { return &m_age; }
private:
QProperty<Country> m_country { None };
QProperty<int> m_age { 0 };
};
All we need to do for updating the price in the UI when any of the properties change, is to subscribe to price and validity changes:
当任何属性更改时,我们需要在UI中更新价格,只需订阅价格和有效性更改:
auto priceChangeHandler = subscription.bindablePrice().subscribe([&] {
priceDisplay->setText(QString::number(subscription.price()));
});
auto priceValidHandler = subscription.bindableIsValid().subscribe([&] {
priceDisplay->setEnabled(subscription.isValid());
});
Notice how much boilerplate code is removed. If more dependencies are added in future, you only need to add the corresponding bindable properties and specify relationships between them by setting the correct bindings. Also note that we don't need to depend on the metaobject system to handle the program logic.
注意删除了多少样板代码。如果将来添加更多依赖项,则只需添加相应的可绑定属性,并通过设置正确的绑定来指定它们之间的关系。还要注意,我们不需要依赖元对象系统来处理程序逻辑。
If you find this interesting, you can also check out the source code of the example implemented with both signal-slot connection and bindable properties and share your thoughts in comments.
如果您觉得这很有趣,还可以查看使用信号插槽连接和可绑定属性实现的示例的源代码,并在注释中分享您的想法。
本文介绍了Qt6.2中可绑定属性的概念,它是从QML的属性绑定功能扩展到C++的一种方式。可绑定属性允许在C++代码中实现属性之间的自动更新和依赖关系,无需信号槽连接。文章通过一个订阅系统示例展示了如何使用可绑定属性简化代码,避免了元对象系统的依赖和复杂的信号槽连接。作者还提到,Qt6.2对可绑定属性的改进包括使用即时求值而非延迟求值,并支持分组属性更新以减少不必要的计算和通知。
1401

被折叠的 条评论
为什么被折叠?



