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: 10width: 20area: 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_OBJECTQ_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中的所有属性绑定相关推荐

  1. java什么是成员域成员方法,java多态中的成员绑定

    多态是面向对象的三大特征之一 谈到多态,不得不提的就是,多态中的成员绑定问题. 方法绑定 将一个方法调用同一个方法主体关联起来被称为绑定.若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现), ...

  2. 五十九、Vue中的样式绑定

    @Author:Runsen @Date:2020/10/16 大四是一个焦虑的时期.烦恼有时候是具体问题带来的压力,有时候却是无端的.莫名其妙的,有时候还极易受到外界的影响,别人一句话就会激起内心难 ...

  3. vue中使用v-on绑定事件中,获取$event.currentTarget,日志打印为null

    问题:vue中使用v-on绑定事件中,获取$event.currentTarget,日志打印为null dom结构: <li @click="clickEvent('hello',$e ...

  4. vue与elementUI中给el-input绑定键盘按键--按键修饰符

    vue怎么写键盘事件 vue允许将按键值作为修饰符来使用,如监听回车事件,有两种写法,如下代码: <input type="text" @keyup.13="con ...

  5. java事件绑定,Java编程GUI中的事件绑定代码示例

    程序绑定的概念: 绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来.对java来说,绑定分为静态绑定和动态绑定:或者叫做前期绑定和后期绑定 静态绑定: 在程序执行前方法已经被绑定,此时由编译 ...

  6. 你不知道的js中关于this绑定机制的解析[看完还不懂算我输]

    前言 最近正在看<你不知道的JavaScript>,里面关于this绑定机制的部分讲的特别好,很清晰,这部分对我们js的使用也是相当关键的,并且这也是一个面试的高频考点,所以整理一篇文章分 ...

  7. 初识 Vue(11)---(Vue 中的事件绑定)

    Vue 中的事件绑定 案例:点击 Hello World ,从黑变红,再次点击,从红变黑... 通过 class 来实现 页面效果的变更 方法一:(通过对象)对象绑定 ​​<!DOCTYPE h ...

  8. React组件方法中为什么要绑定this

    React组件方法中为什么要绑定this 如果你尝试使用过React进行前端开发,一定见过下面这样的代码: //假想定义一个ToggleButton开关组件class ToggleButton ext ...

  9. WPF中DatePiker值绑定以及精简查询

    WPF中DatePiker值绑定以及精简查询 1.WPF中DatePiker值绑定 Xaml中值绑定使用Text <DatePicker Text="{Binding strMinDa ...

最新文章

  1. 神奇的requestAnimationFrame
  2. Linux 创建、删除、修改 文件夹 文件命令(笔记)
  3. Vivado中Block Memory Generator v8.3的使用
  4. [保护模式]段间跳转和跨段跳转
  5. 云服务器可以安装操作系统么,云服务器安装操作系统吗
  6. Python实现——二元线性回归(最小二乘法)
  7. Android平台RTMP多实例推送的几种情况探讨
  8. Hello Quartz (第二部分)
  9. 关于excel密码 工作表密码 工作簿密码 工程密码
  10. Redis六种底层数据结构
  11. 技术开发者该如何开展小团队的微服务之路?
  12. 用Fragments创建动态UI(翻译)
  13. 动物行为检测计算机视觉_当动物行为研究遇见机器视觉——“红外热成像+计算机视觉”动物行为研究系统...
  14. redis学习——数据持久化
  15. ASP.NET------站点地图SiteMapPath
  16. Android Widevine 基本概念
  17. C程序实例1--个人通讯录管理系统
  18. 清华计算机徐华简介,徐华
  19. Redis实现点赞功能
  20. 图解 Paxos 一致性协议

热门文章

  1. [pat乙]1032 挖掘机技术哪家强
  2. 交互入门——基于鼠标控制的射击飞碟小游戏
  3. CleanMyMac X 4.10.1许可证 Mac系统清洁加速
  4. 我们不再沉默 给入侵黑客一个“下马威”
  5. 面试网答题(如老牛网)
  6. 信息安全系统设计基础实验五—20135222胡御风20135215黄伟业
  7. ker矩阵是什么意思_矩阵分析(一):空间变换与基变换
  8. 札记-20190531
  9. UTF8、UTF16、UTF32区别
  10. 打得京东当当响 | 一点财经