Qt6新资讯:将QML和Qt Quick绑定的概念带回到Qt的核心

Qt 6具有很多新功能。我们添加的最令人兴奋的功能之一是将QML和Qt Quick绑定的概念带回到Qt的核心,并允许从C ++使用它。

Qt是一个跨平台框架,通常用作图形工具包,它不仅创建CLI应用程序中非常有用。而且它也可以在三种主要的台式机操作系统以及移动操作系统(如Symbian,Nokia Belle,Meego Harmattan,MeeGo或BB10)以及嵌入式设备,Android(Necessitas)和iOS的端口上运行。现在我们为你提供了免费的试用版。赶快点击下载Qt6最新试用版>>

工具推荐:

  • QtitanRibbon| 下载试用: 遵循Microsoft Ribbon UI Paradigm for Qt技术的Ribbon UI组件,致力于为Windows、Linux和Mac OS X提供功能完整的Ribbon组件。
  • QtitanChart | 下载试用 :是一个C ++库,代表一组控件,这些控件使您可以快速地为应用程序提供漂亮而丰富的图表。并且支持所有主要的桌面操作系

Qt 6具有很多新功能。我们添加的最令人兴奋的功能之一是将QML和Qt Quick绑定的概念带回到Qt的核心,并允许从C ++使用它。

Qt 5中的绑定

让我们首先回顾一下Qt 5中属性绑定的工作方式。在那里,绑定支持仅限于Qt Quick。这是一个非常简单的示例:

import QtQuick 2.15Rectangle {    height: width    border.width: height/10}

这样做的目的是在一个 Rectangle 对象上设置两个绑定。第一个绑定确保Rectangle永远是正方形。第二个绑定将边框宽度设置为高度的10%。然后,Qt中的QML引擎确保这些关系将被保留,并在Rectangle的宽度改变时自动调整高度和边框宽度。

这种绑定的机制是使Qt Quick中的UI定义大多以声明的方式编写。绑定表达式(绑定的右侧)可以任意复杂,包含对其他对象属性的引用,甚至调用其他方法。

在Qt 5的生命周期中,我们已经看到,绑定使代码的表现力更强,并删除了很多需要编写的胶水代码。所以,在Qt 6中,我们的目标是允许作为一个C++开发者也能使用这种机制。

让我们看看如何在C++中表达同样的关系。下面是我们希望这样一个Rectangle如何写成一个C++类。

class Rectangle {public:    Property<int> width;    Property<int> height;    Property<int> border;    Rectangle() {        height.setBinding(width);        border.setBinding([this]() {            return height / 10;        });    }};

这定义了一个具有3个属性的Rectangle类:width,height和border。然后,构造函数设置两个绑定,一个绑定将高度绑定到宽度,另一个绑定将边框绑定到高度的10%。

当我们着手进行Qt 6时,我们面临的问题是我们是否可以以高效且高效的方式来实现这一目标。

绑定系统的目标

除了良好且易于使用的语法外,系统还需要满足其他一些要求。

  • 性能系统需要快速,评估绑定时开销应尽可能少
  • 无开销当系统不使用时,系统不应增加明显的运行时开销
  • 内存效率高 系统应该具有较低的内存开销
  • 与QObject中的现有属性系统集成Qt具有针对基于QObject的类的预先存在的属性系统,并且新系统应集成/扩展该系统

让我们看一下新系统的实施方式以及我们如何实现上述目标。

简单实施

让我们从最简单的方法开始,以实现支持我们正在寻找的功能的 QProperty类:

template <typename T>class QProperty{    std::function<T()> binding = nullptr;    T data;public:    T value() const {        if (binding) return binding();        return data;    }    void setValue(const T &newValue) {        if (binding) binding = nullptr;        data = newValue;    }    void setBinding(std::function<T> b) { binding = b; }};

上面的实现可能是实现支持绑定的QProperty类的最简单方法。它基本上包含了属性数据和一个有可能为空的绑定的函数指针。每当在属性上设置了一个绑定,如果设置了一个绑定,属性获取器将总是执行绑定来检索值。

然而这种实现有几个严重的缺点,使得它不适合按原样使用。最明显的一个缺点就是性能会非常差,特别是当绑定依赖于其他属性,而这些属性本身也有绑定的时候。每次调用getter时都要评估这些绑定,会造成严重的性能问题。更糟糕的是,这可能会导致应用程序崩溃或死锁,万一一个绑定以某种方式引用回自己。

立即和延迟的绑定评估

所以我们确实需要一个更高级一点的设计。基本上有两种可能的方法来避免每次调用setter时计算绑定的值。这两种方法都涉及到将结果值缓存在数据中。此外,我们还需要记住一个绑定所依赖的属性。

Qt Quick在Qt 5中做的就是即时绑定评估,这意味着每当一个属性被改变,我们就会立即触发对所有依赖这个属性的绑定的重新评估。这个系统的缺点是,它可能会导致不必要的绑定表达式的评估。一个例子是一个被绑定为width*height的属性区域。如果宽度和高度都被分配了新的值,面积就会被计算两次,尽管只有第二个结果会被使用。

因此,在 Qt 6 中,我们使用了延迟绑定评估。这意味着我们递归地将所有依赖于属性的绑定标记为 dirty。然后,属性获取器检查该 dirty 标志,如果它为真,则重新评估绑定表达式,然后将结果存储在数据中并清除 dirty 标志。

这就是QProperty现在的简化视图。

template <typename T>class QProperty{    T val;    QPropertyBindingData d;public:    T value() const {        if (d.hasBinding())            d.evaluateIfDirty(this);        d.registerWithCurrenlyEvaluatingBinding();        return this->val;    }    void setValue(const T &t) {        d.removeBinding();        if (this->val == t)            return;        this->val = t;        notify();    }};     

这里发生的事情是,getter检查我们是否有一个绑定,如果有,则重新评估它。之后,作为第二步,它将自己与任何可能正在评估的绑定进行注册。setValue()与之前相当类似。如果新旧值相同,我们就会快捷设置器,以避免这种情况下的绑定重新评估。如果设置了新的值,我们就调用notify(),而notify()又会将所有依赖于这个属性的绑定标记为dirty。

还有很多细节需要我们去解决。例如,依赖注册使用线程本地存储来了解当前正在评估的绑定。如果你想知道所有的细节,请看Qt 6中QProperty的实现。

通知和变更处理程序

除了设置绑定外,QProperty还允许为属性注册变化处理程序。使用QProperty的onValueChanged()或subscribe()方法,可以注册一个回调,每当属性的底层值发生变化时,这个回调就会被调用。

当属性的值通过调用setter而改变时,或者当属性的绑定因为它的一个依赖关系改变而被标记为dirty时,回调将被调用。

QObjects属性系统中的绑定支持

回顾上面概述的目标,你可能已经注意到,QProperty的实现并没有解决Qt 6中绑定引擎的所有目标。它的性能确实非常好(见下面进一步的性能数据),而且它只是在没有使用绑定时增加了一个小的开销。这个开销主要是在getter中检查我们是否有绑定和对当前正在评估的绑定进行TLS查找,在setter中快速检查依赖关系。

但它确实给每个属性带来了不可忽视的额外4到8个字节的内存开销,而且它也没有和QObject中现有的属性系统集成。接下来我们来看看这些是如何解决的。

虽然现在的QProperty可以独立使用,也可以在任何类中使用,但我们希望有一个能与QObject中现有的属性系统无缝集成、兼容的东西。这个系统是围绕QObject的属性建立的,只是在类定义中拥有一个setter和一个getter作为公共成员。这如何用数据来支持有些无关紧要。

为了支持这些属性的数据绑定,我们需要看看如何调整QProperty的想法来适应这里。

我们最终得到的是一个实现属性的QObject公共API的简单扩展。

class MyObject : public QObject{    Q_PROPERTY(int x GET x SET setX BINDABLE bindableX)    // the line below was “int xData;” in Qt 5    Q_OBJECT_BINDABLE_PROPERTY(MyObject, int, xData)public:    int x() { return xData; }    void setX(int x) { xData = x; }    QBindable<int> bindableX() { return &xData; }};

红色标记的部分是Qt 6中的新内容。正如你所看到的那样,在Qt 6中,使一个属性可绑定所需的改动相对较少。简单的用于存储数据的 “int xData; “被一个实现绑定逻辑的宏所取代,即QProperty作为一个独立类所做的一些事情。此外,我们增加了一个新的bindableX()方法,该方法返回一个QBindable<int>,并在Q_PROPERTY宏中告诉元对象系统。

QBindable<T>是一个轻量级接口,它提供了QProperty中也有的附加功能。它允许设置和检索绑定并注册通知。例如,在MyObject的x属性上设置一个绑定可以通过调用来实现。

myObject-> bindableX()。setBinding([otherObject](){    return otherObject-> x()+ otherObject-> width();}  

使用这些宏以及我们知道QObject正在使用它的事实有两个优点。与QProperty不同,Q_OBJECT_BINDABLE_PROPERTY不会增加任何内存开销。宏实现的对象的大小与要存储的数据的大小相同。这是通过将绑定数据移到整个QObject实例的公共数据结构(按需分配)中来实现的。

它使查找绑定的速度稍微慢一些,但是另一方面,由于在QObject中具有按需数据结构,因此我们可以避免对当前正在执行的绑定进行TLS查找。这也意味着,当不使用绑定程序对setter和getter进行指针查找和比较时,可以减少运行时开销。

让我们快速看一下它是如何实现的。为了允许在QObject属性中使用绑定,上面的Q_OBJECT_BINDABLE_PROPERTY宏扩展为两件事。首先,它在对象内部定义了一个静态成员函数:

static constexpr size_t _qt_property_cData_offset(){    return offsetof(MyObject, xData);}

然后,此方法允许被用作下一行中定义的QObjectBindableProperty实例的模板参数:

QObjectBindableProperty <MyObject,int,MyObject :: _qt_property_cData_offset> xData;

这样做的结果是,我们现在有了一个方法,可以从属性数据的this指针计算出拥有属性数据的QObject的this指针。这个东西我们又用来从QObject中检索一个QBindingStorage指针。这个指针可能是空的,在这种情况下,我们有快速路径,在这个对象上没有使用绑定。否则,我们在QBindingStorage中查找QProperty内置的QPropertyBindingData。一旦我们检索到一个有效的绑定数据的指针,QObjectBindableProperty基本上就会进行和QProperty一样的操作。

向后兼容

像Qt 5一样使用changeSignal()作为通知实现的属性将继续像以前一样工作。这意味着它们可以与Qt Quick中的绑定一起使用,但不能与C ++中的绑定一起使用。但是,他们还将继续使用即时绑定评估。

为了获得新系统的全部好处,您应该考虑将绑定支持添加到您自己的属性中。这将使它们可以从C ++绑定,并且在大多数情况下将开始使用延迟绑定评估。向QObject的现有属性添加绑定支持是100%向后兼容的。

Qt 6本身的大多数属性仍未移植为也不支持新的绑定引擎。我们计划在Qt 6.1和6.2中实现这一点。

基准数据

我们先来看看不使用绑定时属性读写的性能。这一点很重要,因为我们不希望现有代码出现较大的回归。为了测试,我们看一个整数属性。这测试的是最坏的情况,因为读写一个整数的速度是最快的,因此结果将最清楚地显示任何增加的项。

旧样式属性 3,8ns 7.2ns
QObjectBindableProperty(无通知) 4,3ns 4,5ns
QObjectBindableProperty(信号已更改) 4,3ns 8.2ns
QProperty 9,1ns 5,4ns

表中显示了结果,测试了几个案例。第一个是用Qt 5的方式实现的一个属性,有getter、setter和一个变化的信号。接下来的两行使用Q_OBJECT_BINDABLE_PROPERTY使属性可绑定。在一种情况下,我们没有添加Qt 5风格的改变信号(因为新系统并不依赖它们),另一种情况下,为了向后兼容,仍然发出一个改变信号。最后一行显示了QProperty的表现。

正如你所看到的,我们对于getter的速度慢了10%左右(但请注意,旧式属性的getter扩展为一个包含三条指令的函数调用)。对于最常见的属性没有变化信号的情况,setter要快40%。QProperty稍微慢一些,因为它需要做一个TLS查找。

对于基于QString的属性来说,差异会小得多,所以我们可以得出结论,在没有使用绑定的情况下,我们成功地添加了对绑定的支持,而没有显著的开销。

现在让我们看看绑定的性能如何。为此,我们使用一个整数属性与另一个整数属性的简单直接绑定。我们有两个测试案例,一个案例是我们连续设置第一个属性,然后读取第二个属性的值。在第二个案例中,我们只对第一个属性进行写入,但从不读取第二个属性。每一个案例,我们都分成两个子案例,一个是我们通过QObjects通用属性接口(setProperty()和property())读写值,一个是我们使用C++ setter和getter。

然后,我们为旧式属性以及支持直接绑定的新属性运行这些测试用例。

让我们从一个用QML定义的绑定开始,并像在Qt 5中一样进行评估。

访问方式 写读 只写 写读 只写
setProperty /属性 设置器/获取器
旧样式属性 370ns 240ns 130ns 130ns
QObjectBindableProperty(无通知) 370ns 110ns 120ns 14ns
QObjectBindableProperty(信号已更改) 410ns 120ns 140ns 25ns
QProperty 440ns 130ns 130ns 10ns

虽然Qt 5中的QML为某些选定的属性提供了一些快捷方式,但某些属性可能最终会通过QObject的通用属性接口进行访问。该表第一行中的数字反映了我们在Qt 5.15中可以获得的最坏情况和最好情况。

其他行显示了我们在Qt 6中可以获得的性能。您会看到,在每次写入之后都进行一次读取的情况与Qt 5中的时间大致相同。这是可以预期的,因为我们需要对Qt 5进行处理。同样的工作量。但是在所有情况下,在有多次写入的情况下,在我们再次需要该属性的值之前,新系统在一定程度上击败了旧系统。

让我们看一下在C ++中设置绑定时会发生什么。由于旧的属性系统无法做到这一点,因此我们在此处通过将lambda连接到设置了新值的更改信号来对其进行仿真。应该注意的是,这不能替代绑定,因为它根本无法扩展到更复杂的绑定表达式,并且需要大量的手动设置才能捕获所有依赖项。

访问方式 写读 只写 写读 只写
setProperty /属性 设置器/获取器
旧样式属性 230ns 120ns 29ns 30ns
QObjectBindableProperty(无通知) 250ns 100ns 35ns 12ns
QObjectBindableProperty(信号已更改) 280ns 120ns 51ns 22ns
QProperty 300ns 120ns 48ns 9ns

最左边的两列主要供参考,并与上表进行比较。在C ++中,几乎永远不会通过基于字符串的通用属性API访问属性。相反,最右边的两列反映了C ++中的典型用法。

可以看出,绑定系统的性能几乎与两个旧样式属性之间的直接信号/插槽连接一样好。鉴于它要灵活得多,并且可以自动捕获所有依赖项(需要使用信号/插槽手动声明),因此数量很多。

您还可以看到,使用setter和getter的基于C ++的绑定比Qt 5.15中QML中定义的绑定快3-10倍。展望未来,我们计划通过探索将QML中定义的绑定表达式编译为C ++然后进行汇编的方式来利用这一事实。

结论

Qt 5中的绑定引擎使Qt Quick如此成功。有了Qt 6,我们现在已经把这个引擎从Qt Quick中移到了Qt的核心,并且让它也能为C++开发者所用。

在这样做的同时,我们成功地实现了比 Qt 5 中的性能的显著提高。尽管如此,仍未完成工作,因为库中的大多数属性仍需要移植到新系统上。

好了这就是今天的内容了,如果今天的文章未解决你的需求,点击获取更多文章教程。不要忘了在评论与我们分享您的想法和建议。

标签:

来源:慧都

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年11月2日
下一篇 2020年11月2日

相关推荐

发表回复

登录后才能评论