QT 可绑定属性 QProperty QObjectBindableProperty QObjectComputedProperty,简化信号、槽(SIGNAL、SLOT)机制的方法

QT提供的可绑定属性是指这些属性可以绑定到其他值或表达式上(通常是 C++ lambda 表达式)。如果属性是通过表达式进行绑定,该属性会跟随表达式自动更新。可绑定属性由 QProperty 类和 QObjectBindableProperty 类实现,它们都继承自 QPropertyData 类。QProperty 类包含数据对象和指向管理数据结构(QPropertyBindingData)的指针;QObjectBindableProperty 类仅包含数据对象,使用封装的 QObject 来存储指向管理数据结构的指针。也就是说,QProperty 不依赖 QT 的元对象系统(metaobject system),QObjectBindableProperty 则需要和 QObject 一起使用。

为什么使用可绑定属性

属性绑定是 QML 的核心属性之一。它允许指定不同对象将的关系,并在其所依赖对象变更时自动更新属性值。可绑定属性不仅仅用在 QML 代码中, C++代码中都可以使用。使用可绑定属性可以简化编程,从而省略哪些通过跟踪、响应(信号、槽机制)来更新属性的代码。简化编程的示例:https://doc.qt.io/qt-6/qtcore-bindableproperties-example.html

可绑定属性在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 是 int 类型并转换为了 string 类型,但是依然被 fullname 依赖和追踪。

可绑定属性的 Getters 与 Setters

在类中使用 QProperty 或 QObjectBindableProperty 声明可绑定属性时,构建 Getters 与 Setters 特别要注意。

getters

为了确保自动依赖项跟踪系统的正确运行,getter 中需要从底层属性对象中读取值。此外,不得在 getter 中写入该属性。不能在 getter 中使用重新计算或更新任何内容的设计模式。因此对于可绑定属性,推荐只使用最简单的 getters。

setters

为了确保自动依赖项跟踪系统的正确运行,setter 中不论值是否发生改变都需要将值写入底层属性对象。setter 中的其它任何代码都是错误的。任何使用新值执行的更新操作都应视作 bug,因为当绑定属性通过绑定改变时这些代码不会执行。因此对于可绑定属性,推荐只使用最简单的 setters。

Virtual Setter 和 Virtual Getter

可绑定属性的 setter 和 getter 通常应该是最小的,并且只设置属性;因此,通常不适合将此类 setter 和 getter 设置为 virtual。这对派生类来说没有任何意义。

但是,某些 Qt 类可能有 virtual setter 的属性。在继承这样的 Qt 类时,重写 setter 需要特别小心。在任何情况下,都必须调用基本的实现才能使绑定正常工作。方法如下:

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

写入可绑定属性的所有规则和建议也适用于此处。调用基类实现后,所有观察者都会收到有关属性更改的通知。所以在调用基类实现之前,需要确保类达到稳定状态(即需要修改的属性都已修改)。

需要使用virtual getter 或 setter 的情况非常少,声明virtual getter 或 setter基类应当注明对重写的要求。

写入可绑定属性的建议

当可绑定属性改变时,该属性会通知每一个依赖该属性的属性。这会触发属性改变的处理程序,触发的处理程序时可能会执行任何类型的代码。因此所有写入可绑定属性的代码都必须认真审查。

  1. 不可将计算过程中的中间值写入可绑定属性
    可绑定属性不能在算法中用作变量。写入的每个值都将传达给依赖属性。例如,下面的代码中,依赖于 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
  1. 不可在类处于过渡状态时写入可绑定属性

当可绑定属性是类的成员时,对该属性的每次写入都可能将当前状态公开给外部。因此,当类未达到稳定状态时,不得在类的过渡状态写入可绑定属性。

例如,在表示一个圆的类中,成员 radius 和 area 应保持一致,setter代码如下(其中 radius 是可绑定属性):

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

被触发的处理程序使用该圆时,radius 是最新值,但是 area 还没有更新。

使用属性绑定的规则

任何可以得出正确类型的 C++ 表达式都可以用作绑定表达式,并提供给 setBinding() 方法。但是,要构建正确的绑定,必须遵循一些规则。

  1. 确保绑定表达式中使用的所有属性都是可绑定属性

    依赖项跟踪仅适用于可绑定属性。在绑定表达式中使用非绑定属性时,对这些属性的更改不会触发对绑定属性的更新。在编译时或运行时都不会产生警告或错误。仅当绑定表达式中使用的可绑定属性发生更改时,才会更新绑定的属性。如果可以确保非绑定属性项的每次更改都能触发绑定属性的 markDirty方法,则可以在绑定中使用非绑定属性。

  2. 确保绑定表达式中对象的生命周期足够长

    在一个对象的生命周期内,属性绑定可能会多次重新计算。需要确保在绑定表达式中使用的所有对象的生命周期都要比这个绑定本身更长,否则可能会导致运行时错误或不可预期的行为。

  3. 可绑定属性系统不是线程安全的

    在一个线程上,绑定表达式中使用的属性,任何其他线程不得读取或修改。具有带绑定的属性的 QObject 派生类的对象不得移动到其他线程。此外,如果 QObject 派生类的属性被用在绑定表达式中,则该对象不得将其移动到其他线程。不论是同一对象中的属性的绑定还是用于另一个对象中的属性的绑定都不是线程安全的。

  4. 避免死循环

    绑定表达式不应从绑定的属性(即该表达式计算后赋值的属性)中读取数据。否则会出现死循环。

  5. **绑定表达式不得写入其绑定的属性。

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

追踪可绑定属性的方式

以上讨论的是通过 setBinding() 绑定属性,有时,属性之间的关系不能用绑定来表示。在处理属性值变化时,如果不是简单地将值赋给另一个属性,而是将这个值传递给应用程序的其他部分进行进一步处理(例如,将数据写入网络套接字或打印调试输出),则需要另外的方法。QProperty 提供了两种跟踪机制。

  1. 使用 onValueChanged() 注册回调函数处理属性变化;

  2. 使用 subscribe() 注册回调函数,与 onValueChanged() 不同该方法可以处理属性的当前值(即调用 subscribe() 时会立即执行一次回调函数)。

    复制代码
     template<typename Functor>
     QPropertyChangeHandler<Functor> onValueChanged(Functor f)
     {
         static_assert(std::is_invocable_v<Functor>, "Functor callback must be callable without any parameters");
         return QPropertyChangeHandler<Functor>(*this, f);
     }
    
     template<typename Functor>
     QPropertyChangeHandler<Functor> subscribe(Functor f)
     {
         static_assert(std::is_invocable_v<Functor>, "Functor callback must be callable without any parameters");
         f();
         return onValueChanged(f);
     }
    
     template<typename Functor>
     QPropertyNotifier addNotifier(Functor f)
     {
         static_assert(std::is_invocable_v<Functor>, "Functor callback must be callable without any parameters");
         return QPropertyNotifier(*this, f);
     }

与 Q_PROPERTYs 交互

Q_PROPERTY 定义中如果指定了 BINDABLE,则该属性可以被绑定并在绑定表达式中使用。 该属性需要通过 QProperty,QObjectBindableProperty 或 QObjectComputedProperty 定义属性来实现。使用示例如下:

复制代码
#include <QObject>
#include <QProperty>
#include <QDebug>

class Foo : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int myVal READ myVal WRITE setMyVal BINDABLE bindableMyVal)
public:
    int myVal() { return myValMember.value(); }
    void setMyVal(int newvalue) { myValMember = newvalue; }
    QBindable<int> bindableMyVal() { return &myValMember; }
signals:
    void myValChanged();

private:
    Q_OBJECT_BINDABLE_PROPERTY(Foo, int, myValMember, &Foo::myValChanged);
};

int main()
{
    bool debugout(true); // enable debug log
    Foo myfoo;
    QProperty<int> prop(42);
    QObject::connect(&myfoo, &Foo::myValChanged, [&]() {
        if (debugout)
            qDebug() << myfoo.myVal();
    });
    myfoo.bindableMyVal().setBinding([&]() { return prop.value(); }); // prints "42"

    prop = 5; // prints "5"
    debugout = false;
    prop = 6; // prints nothing
    debugout = true;
    prop = 7; // prints "7"
}

#include "main.moc"

Q_PROPERTYs 定义中如果没有指定 BINDABLE,但是指定了 NOTIFY 信号,也可以被绑定并在绑定表达式中使用。此时,必须使用 QBindable(QObject *obj, const char *property) 构造函数将属性包装在 QBindable 中。然后,可以使用 QBindable::setBinding() 绑定该属性,或在绑定表达式中通过 QBindable::value() 使用该属性。如果 Q_PROPERTY 定义中没有指定 BINDABLE,要启动该属性的依赖跟踪功能,在绑定表达式中必须使用 QBindable::value(),不能使用属性的 READ 函数(或 MEMBER)。示例如下:

复制代码
#include <QObject>
#include <QBindable>
#include <QProperty>
#include <QDebug>

class Foo : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int myVal READ myVal WRITE setMyVal NOTIFY myValChanged CONSTANT)
public:
    explicit Foo():m_myVal(5){}
    int myVal() const { return m_myVal; }
    void setMyVal(int newvalue) {
        if(m_myVal == newvalue) return;
        m_myVal = newvalue;
        emit myValChanged(newvalue);
    }
signals:
    void myValChanged(int newVal);

private:
    int m_myVal;
};

int main()
{

    Foo myfoo;
    QBindable<int> obj(&myfoo, "myVal");
    QProperty<int> prop([&](){return obj.value();});
	// onValueChanged 的返回值必须保存,否则 callback 将失效
    auto change = prop.onValueChanged([&](){qDebug() << "value changed:" << prop.value();});
	// subscribe 的返回值未保存,只会执行1次回调函数
    prop.subscribe([&](){qDebug() << "call subscribe:" << prop.value();});
	// onValueChanged 和 addNotifier 如果不保存返回值,回调函数一次也不会执行
    auto notify = prop.addNotifier([&](){qDebug() << "call Notifier:" << prop.value();});
    myfoo.setMyVal(10); qDebug() << "prop =" << prop.value();
    myfoo.setMyVal(20); qDebug() << "prop =" << prop.value();
    myfoo.setMyVal(30); qDebug() << "prop =" << prop.value();

    return 0;
}

#include "main.moc"

输出内容如下:

复制代码
call subscribe: 5
call Notifier: 10
value changed: 10
prop = 10
call Notifier: 20
value changed: 20
prop = 20
call Notifier: 30
value changed: 30
prop = 30

注:使用 Qt 6.8.1,Qt Creator 15.0.0 编译以上代码时会出现如下错误
include/QtCore/qproperty.h:667:37: error: constexpr variable 'iface<int>' must be initialized by a constant expression

解决办法:打开 qproperty.h 文件,修改 667行 代码 inline constexpr QBindableInterface iface = {,将 constexpr 修饰符注释掉即可。这可能是 Qt 6.8.1 中的一个错误

参考:Qt Bindable Properties

相关推荐
牛奶咖啡.8544 小时前
第十四届蓝桥杯大赛软件赛省赛C/C++ 大学 A 组真题
c语言·数据结构·c++·算法·蓝桥杯
狄加山6755 小时前
Qt模型-视图架构
开发语言·qt
Dream it possible!6 小时前
CCF CSP 第35次(2024.09)(1_密码_C++)(哈希表)
c++·散列表·ccf csp·csp
旧时光林6 小时前
蓝桥杯 分解质因数(唯一分解定理)
数据结构·c++·算法·蓝桥杯·模拟·枚举
njsgcs7 小时前
ubuntu24.04 cmake 报错 libldap-2.5.so.0 解决办法
开发语言·c++
notfindjob7 小时前
QT Sqlite数据库-教程001 创建数据库和表-下
数据库·qt·sqlite
头发尚存的猿小二7 小时前
2024年第十五届蓝桥杯C&C++大学A组--成绩统计
c语言·开发语言·c++·蓝桥杯
JuicyActiveGilbert8 小时前
【C++游戏引擎开发】第9篇:数学计算库GLM(线性代数)、CGAL(几何计算)的安装与使用指南
c++·线性代数·游戏引擎
DevangLic8 小时前
下【STL 之速通pair vector list stack queue set map 】
开发语言·c++·list
你又食言了哦8 小时前
qt上设置 WebAssembly显示中文
开发语言·qt·wasm