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保持的值,则它们自动成为绑定的一部分。

绑定表达式可以使用任何类型的属性,因此在上面的示例中,年龄是一个整数,通过整型转换折叠到字符串值中,但依赖项仍然得到完全跟踪。

可绑定属性获取器和设置器#

当一个类具有可绑定的属性,无论是使用QProperty还是QObjectBindableProperty时,在为该属性定义获取和设置方法时需要特别注意。

可绑定的属性获取器

为确保自动依赖追踪系统的正常工作,获取器中的每一个可能的代码路径都必须读取底层属性对象。此外,属性不得在获取器中被写入。在获取器中重新计算或刷新任何内容的设计模式与可绑定的属性不相兼容。

因此,建议仅使用简单的获取器与可绑定的属性配合使用。

可绑定的属性设置器

为确保自动依赖追踪系统的正常工作,设置器中的每一个可能的代码路径都必须写入底层属性对象,即使值没有改变。

设置器中的任何其他代码很可能是不正确的。任何基于新值进行更新的代码很可能是一个错误,因为这个代码在属性通过绑定更改时不会执行。

因此,建议仅使用简单的设置器与可绑定的属性配合使用。

写入可绑定的属性

可绑定属性会通知它们依赖的属性关于每个更改的信息。这可能会触发更改处理器,进而可能会调用任意代码。因此,写入可绑定的属性的每一个操作都必须仔细检查。可能会出现以下问题。

向可绑定属性写入中间值

不能将可绑定属性用作算法中的变量。每个写入的值都会被传达给依赖的属性。例如,在以下代码中,其他依赖于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

在过渡状态下写入可绑定的属性

当一个可绑定的属性是类的一个成员时,对该属性的每一个写入都可能将当前状态暴露给外部。因此,在类的不变性没有达到时,不要在瞬态状态下写入可绑定的属性。

例如,在一个表示圆的类中,它持有两个相互一致的成员radiusarea,设置器可能看起来像这样(其中radius是一个可绑定的属性):

void setRadius(double newValue)
{
    radius = newValue; // this might trigger change handlers
    area = M_PI * radius * radius;
    emit radiusChanged();
}

在这里,触发更改处理器的代码可能会使用这个圆,但它已经有了新的半径,但仍然有旧的面积。

具有虚拟设置器和获取器的可绑定的属性

属性设置器和获取器通常应该是最小化和仅设置属性的;因此,通常不适当让这样一个设置器和获取器是虚拟的。对于派生类来说,没有有意义的事情可以让它来做。

然而,一些Qt类可以有具有虚拟设置器的属性。当此类派生时,需要特别小心重写这样的设置器。

在任何情况下,都必须调用基本实现确保绑定正确工作。

以下示例说明了这种方法。

void DerivedClass::setValue(int val)
{
    // do something
    BaseClass::setValue(val);
    // probably do something else
}

所有与绑定属性写入相关的通用规则和建议也适用于此处。一旦调用基类的实现,所有观察者都会被告知属性的变化。这意味着在调用基实现之前必须满足类的不变性。

在必要时,则基类应该记录其强加给覆盖的重载的要求。

创建属性绑定

任何计算结果为正确类型的C++表达式都可以用作绑定表达式,并将其传递给setBinding()方法。然而,为了形成正确的绑定,必须遵循一些规则。

依赖跟踪仅适用于绑定属性。确保用于绑定表达式的所有属性都是绑定属性是开发者的责任。当在绑定表达式中使用非绑定属性时,这些属性的变化不会触发绑定属性更新。在编译时或运行时都不会生成警告或错误。仅当绑定表达式中使用的绑定属性发生变化时,绑定属性才会更新。如果可以确保每次非绑定依赖发生变化时都调用标记_DIRTY,则可以在绑定中使用非绑定属性。

绑定属性可能会在其生命周期内多次评估其绑定。开发者必须确保绑定表达式中使用的所有对象在绑定结束后仍然有效。

绑定属性系统不是线程安全的。在一个线程上使用的绑定表达式中的属性不得被任何其他线程读取或修改。具有绑定属性的QObject派生类对象不得移动到不同的线程。同样,用于绑定的QObject派生类对象也不能移动到不同的线程。在这种情况下,它与它是在同一对象的属性绑定中还是在另一个对象的属性绑定中无关。

绑定表达式不应从其作为绑定的属性中读取。否则,将存在评估循环。

绑定表达式不得写入其作为绑定的属性。

用作绑定的函数以及绑定内部调用的所有代码不得使用co_await。这样做可能会混淆属性系统对依赖的跟踪。

绑定属性与多线程#

除非另有说明,否则绑定属性不是线程安全的。绑定属性不得被创建它的线程之外的任何线程读取或修改。

跟踪绑定属性#

有时,属性之间的关系不能用绑定来表示。在这种情况下,您可能需要在属性值改变时运行自定义代码,而不是将值赋给另一个属性,而是将其传递到应用程序的其他部分。例如将数据写入网络套接字或打印调试输出。QProperty提供两种跟踪机制。

您可以使用onValueChanged()注册一个回调函数,在属性值改变时调用它。如果您想使回调同时为属性的当前值而调用,请使用subscribe()注册您的回调。

与Q_PROPERTYs的交互#

定义了 BINDABLEQ_PROPERTY 可以进行绑定并在绑定表达式中使用。您可以使用 QPropertyQObjectBindablePropertyQObjectComputedProperty 来实现此类属性。

没有 BINDABLE 的 Q_PROPERTY 也可以进行绑定并在绑定表达式中使用,只要它们定义了一个 NOTIFY 信号。您必须使用 QBindable(QObject* obj, const char* property) 构造函数将该属性包装在 QBindable 中。然后,可以使用 \l setBinding() 来绑定属性,或者通过 \l value() 在绑定表达式中使用。如果属性不是 BINDABLE,则必须在绑定表达式中使用 QBindable::value() 而不是正常的属性 READ 函数(或 MEMBER)以启用依赖跟踪。