QT 的可绑定属性,简化信号、槽(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 特别要注意。

  1. getters

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

  1. setters

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

  1. 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 中的一个错误

QProperty

QProperty 是实现 Qt Bindable Properties 的类之一。它是一个包含 T 实例的容器。可以为其赋值,并且通过 value() 函数或 T 的转换操作符读取值。还可以将属性绑定到动态计算值的表达式(即绑定表达式,常为 C++ lambda表达式),用于表示应用程序中不同属性之间的关系。

注意:对于 QML 来讲,在 Q_PROPERTY 中使用 BINDABLE 关键字公开 QProperty 的属性,QML 引擎会将其用作可绑定接口来设置属性绑定。在 C++ 中,BINDABLE 的属性可以通过:QProperty::onValueChanged、QProperty::takeBinding 和 QBindable::hasBinding 与其进行交互。QML 引擎使用 C++ 属性系统的更改跟踪功能来获取有关更改的通知,并不赖于发出的信号。

QObjectBindableProperty

QObjectBindableProperty 与 QProperty 类似,是一个 T 实例的容器,是实现 Qt Bindable Properties 的类之一。与 QProperty 不同,它将用于管理的数据结构存储在 QObject 中。附加的模板参数用于标识 QObject 类和该类用于处理变更的成员函数。

QObjectBindableProperty 可用于 Q_PROPERTY 的 BINDABLE 定义。为了在属性更改时触发 change 信号,构造 QObjectBindableProperty 时需要将属性的 change 信号作为回调函数。通常不直接使用 QObjectBindableProperty,而是使用 QOBJECTBINDABLE_PROPERTY 宏来创建实例。示例如下:

class MyClass : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int x READ x WRITE setX NOTIFY xChanged BINDABLE bindableX)
public:
    int x() const { return xProp; }
    void setX(int x) { xProp = x; }
    QBindable<int> bindableX() { return QBindable<int>(&xProp); }

signals:
    void xChanged();

private:
    // Declare the instance of the bindable property data.
    Q_OBJECT_BINDABLE_PROPERTY(MyClass, int, xProp, &MyClass::xChanged)
};

如果需要直接使用某个非默认值初始化属性,则可以使用 Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS 宏,它有一个初始化值的参数。上例中的代码可以修改如下:

private:
    // Declare the instance of int bindable property data and
    // 初始值为 5
    Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(MyClass, int, xProp, 5, &MyClass::xChanged)

Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS 不支持使用参数列表初始化,如果属性需要多个参数进行初始化,需要显式调用其构造函数。

class CustomType
{
public:
    CustomType(int val, int otherVal) : value(val), anotherValue(otherVal) { }

private:
    int value = 0;
    int anotherValue = 0;
};

// later when using CustomType as a property
Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(MyClass, CustomType xProp, CustomType(5, 10),
                                     &MyClass::xChanged)

属性变更的 signal 可以接受一个与属性类型相同的参数,用于传递新的属性值;一般情况下,它不应该有参数。
如果属性不需要更改通知,则可以省略 QPROPERTY 宏中的 NOTIFY声明,并省略 Q_OBJECT_BINDABLE_PROPERTY 和 Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS 宏的最后一个参数。

QObjectComputedProperty

QObjectComputedProperty 是一个只读属性,每次读取时都会重新计算。它不存储计算的值。它是实现 Qt Bindable Properties 的内部类之一。QObjectComputedProperty 通常不直接使用,而是使用 QOBJECTCOMPUTED_PROPERTY 宏来创建实例。

class Client{};

class MyClassPrivate : public QObjectPrivate
{
public:
    QList<Client> clients;
    bool hasClientsActualCalculation() const { return clients.size() > 0; }
    Q_OBJECT_COMPUTED_PROPERTY(MyClassPrivate, bool, hasClientsData,
                               &MyClassPrivate::hasClientsActualCalculation)
};

class MyClass : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool hasClients READ hasClients STORED false BINDABLE bindableHasClients)
public:
    QBindable<bool> bindableHasClients()
    {
        return QBindable<bool>(&d_func()->hasClientsData);
    }
    bool hasClients() const
    {
        return d_func()->hasClientsData.value();
    }
    void addClient(const Client &c)
    {
        Q_D(MyClass);
        d->clients.push_back(c);
        // notify that the value could have changed
        d->hasClientsData.notify();
    }
private:
    Q_DECLARE_PRIVATE(MyClass)
};

可绑定属性的 Getter 和 Setters 规则也适用于 QObjectComputedProperty。特别是,getter 应该只返回 QObjectComputedProperty 对象的值。提供给 QObjectComputedProperty 的回调通常应该是一个私有方法,该方法仅由 QObjectComputedProperty 调用。因为 QObjectComputedProperty 是只读的,所以不需要也不允许有 setter。

每当回调方法中使用的可绑定属性发生更改时,QObjectComputedProperty 都会得到通知。如果回调方法的结果是因非可绑定属性的值更改的,则开发人员需要调用 QObjectComputedProperty 的 notify 方法通知 QObjectComputedProperty 对象。

注意:当类处于过渡状态或无效状态时,不得调用 notify。

QObjectComputedProperty 不适用于依赖输入的计算,因为输入更改时 QObjectComputedProperty 对象得不到通知。例如,使用文件的内容进行计算

参考:Qt Bindable Properties

posted @ 2025-04-12 17:35  永不停转  阅读(171)  评论(0)    收藏  举报