Qt 可绑定属性¶
Qt的可绑定属性。
Qt 提供了可绑定的属性。可绑定的属性是指那些要么有一个值,要么通过任何 C++ 函数(通常是 C++ lambda 表达式)来指定的属性。如果它们是通过 C++ 函数指定的,那么每当它们的依赖项发生变化时,它们会自动更新。
可绑定属性在类 QProperty 中实现,该类由数据对象和指向管理数据结构的指针组成,以及在类 QObjectBindableProperty 中实现,该类仅由数据对象组成,并使用封装的 QObject 来存储指向管理数据结构的指针。
为什么使用可绑定属性?¶
属性绑定是QML的核心功能之一。它们允许指定不同对象属性之间的关系,并在依赖项发生变化时自动更新属性值。可绑定属性不仅可以在QML代码中实现这一点,还可以在C++中实现。使用可绑定属性可以通过消除大量用于跟踪和响应不同对象依赖项更新的样板代码来简化您的程序。
下面的入门示例演示了在C++代码中使用可绑定属性的用法。您还可以查看可绑定属性示例,了解可绑定属性如何帮助改进您的代码。
入门示例¶
绑定表达式通过读取其他QProperty值来计算值。在幕后,这种依赖关系被跟踪。每当检测到任何属性的依赖关系发生变化时,绑定表达式会重新评估,并将新结果应用于该属性。例如:
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"
当为firstname属性分配新值时,fullname的绑定表达式会重新评估。因此,当最后一个qDebug()语句尝试读取fullname属性的名称值时,将返回新值。
由于绑定是C++函数,它们可以执行C++中任何可能的操作。这包括调用其他函数。如果这些函数访问由QProperty持有的值,它们会自动成为绑定的依赖项。
绑定表达式可以使用任何类型的属性,因此在上面的示例中,age 是一个整数,并通过转换为整数的方式合并到字符串值中,但依赖关系被完全跟踪。
可绑定属性的获取器和设置器¶
当一个类具有可绑定属性时,无论是使用QProperty还是QObjectBindableProperty,在制定该属性的getter和setter时需要特别注意。
可绑定属性获取器¶
为了确保自动依赖跟踪系统的正常运行,getter中的每个可能的代码路径都需要从底层属性对象中读取。此外,属性不能在getter内部被写入。在getter中重新计算或刷新任何内容的设计模式与可绑定属性不兼容。
因此建议仅将简单的getter与可绑定属性一起使用。
可绑定属性设置器¶
为了确保自动依赖跟踪系统的正常运行,setter中的每个可能的代码路径都需要写入底层属性对象,即使值没有改变。
在setter中的任何其他代码都有很高的可能性是不正确的。任何基于新值进行更新的代码很可能是一个错误,因为当通过绑定更改属性时,这段代码不会被执行。
因此,建议仅对可绑定属性使用简单的设置器。
写入可绑定属性¶
可绑定属性会通知其依赖属性每次更改。这可能会触发更改处理程序,进而可能调用任意代码。因此,必须仔细检查对可绑定属性的每次写入。可能会出现以下问题。
将中间值写入可绑定属性¶
可绑定属性不得在算法中用作变量。每个写入的值都会传递给依赖属性。例如,在以下代码中,依赖于myProperty的其他属性将首先被告知更改为42,然后被告知更改为maxValue。
myProperty = somecomputation(); // returning, say, 42 if (myProperty.value() > maxValue) myProperty = maxValue;
相反,在一个单独的变量中执行计算。正确的用法如下例所示。
int newValue = someComputation(); if (newValue > maxValue) newValue = maxValue; myProperty = newValue; // only write to the property once
在过渡状态下编写可绑定属性¶
当一个可绑定属性是类的成员时,每次对该属性的写入可能会将当前状态暴露给外部。因此,在类不变量未满足的瞬态下,不得写入可绑定属性。
例如,在一个表示圆的类中,该类保持两个成员半径和面积一致,一个设置器可能看起来像这样(其中半径是一个可绑定的属性):
void setRadius(double newValue) { radius = newValue; // this might trigger change handlers area = M_PI * radius * radius; emit radiusChanged(); }
在这里,更改处理程序中触发的代码可能会使用圆,虽然它有了新的半径,但仍然有旧的面积。
具有虚拟设置器和获取器的可绑定属性¶
属性的设置器和获取器通常应该是最小的,除了设置属性外不做任何事情;因此,这样的设置器和获取器通常不适合是虚拟的。派生类没有什么有意义的事情可以做。
然而,一些Qt类可能具有带有虚拟设置器的属性。在继承这样的Qt类时,覆盖这些设置器需要特别小心。
无论如何,必须调用基础实现才能使绑定正常工作。
以下说明了这种方法。
void DerivedClass::setValue(int val) { // do something BaseClass::setValue(val); // probably do something else }
所有关于写入可绑定属性的常见规则和建议也适用于此。一旦调用了基类实现,所有观察者都会收到属性更改的通知。这意味着在调用基类实现之前,必须满足类不变量。
在极少数情况下,当需要这样的虚拟getter或setter时,基类应记录其对重写的要求。
制定属性绑定¶
任何评估为正确类型的C++表达式都可以用作绑定表达式,并传递给setBinding()方法。然而,为了制定正确的绑定,必须遵循一些规则。
依赖跟踪仅适用于可绑定属性。开发人员有责任确保绑定表达式中使用的所有属性都是可绑定属性。当绑定表达式中使用不可绑定属性时,这些属性的更改不会触发绑定属性的更新。在编译时或运行时都不会生成警告或错误。只有在绑定表达式中使用的可绑定属性发生更改时,绑定属性才会更新。如果能够确保在每次不可绑定依赖项更改时调用markDirty,则可以在绑定中使用不可绑定属性。
绑定属性在其生命周期内可能会多次评估其绑定。开发人员必须确保绑定表达式中使用的所有对象的生命周期都长于绑定。
可绑定属性系统不是线程安全的。在一个线程上绑定表达式中使用的属性不能被任何其他线程读取或修改。具有绑定属性的QObject派生类的对象不能被移动到不同的线程。此外,具有在绑定中使用的属性的QObject派生类的对象也不能被移动到不同的线程。在这种情况下,无论它是在同一对象的属性绑定中使用,还是在另一个对象的属性绑定中使用,都是无关紧要的。
绑定表达式不应从其绑定的属性中读取。否则,将存在一个评估循环。
绑定表达式不得写入其绑定的属性。
用作绑定的函数以及绑定内调用的所有代码不得使用co_await。这样做可能会混淆属性系统对依赖项的跟踪。
可绑定属性和多线程¶
除非另有说明,否则可绑定属性不是线程安全的。可绑定属性不得由创建它的线程以外的任何线程读取或修改。
跟踪可绑定属性¶
有时属性之间的关系无法使用绑定来表达。相反,您可能需要在属性值更改时运行自定义代码,而不是将值分配给另一个属性,而是将其传递给应用程序的其他部分。例如,将数据写入网络套接字或打印调试输出。QProperty 提供了两种跟踪机制。
您可以通过使用onValueChanged()注册一个回调函数,以便在属性值发生变化时调用。如果您还希望回调函数在属性的当前值上也被调用,请改用subscribe()注册您的回调。
与Q_PROPERTY的交互¶
一个定义了BINDABLE的Q_PROPERTY可以被绑定并在绑定表达式中使用。你可以使用QProperty、QObjectBindableProperty或QObjectComputedProperty来实现这样的属性。
没有BINDABLE的Q_PROPERTYs也可以绑定并用于绑定表达式,只要它们定义了NOTIFY信号。您必须使用QBindable(QObject* obj, const char* property)构造函数将属性包装在QBindable中。然后,可以使用setBinding()绑定属性,或通过value()在绑定表达式中使用。如果属性不是BINDABLE,则必须在绑定表达式中使用QBindable::value()而不是普通的属性READ函数(或MEMBER)以启用依赖跟踪。