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

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

  1. Qt 跨UI线程的数据交换和信号-槽调用实现方案汇总

    一.目录 转载1: http://my.oschina.NET/fanhuazi/blog/737224?ref=myread 点击打开链接 转载2: http://www.qtcn.org/bbs/ ...

  2. Xamarin Bindableproperty 可绑定属性

    重要的事情说三遍: 本文基本是取自微软官方 Bindable Properties, 官方也提供了机翻的中文版本,笔者只是尝试用自己的理解描述一遍,便于记忆.如有不对之处,欢迎拍砖. 本文基本是取自微 ...

  3. qt信号signal和槽slot机制

    内容: 一.概述 二.信号 三.槽 四.信号与槽的关联 五.元对象工具 六.程序样例 七.应注意的问题 信号与槽作为QT的核心机制在QT编程中有着广泛的应用,本文介绍了信号与槽的一些基本概念.元对象工 ...

  4. Qt信号槽-原理分析

    目录 一.问题 二.Moc 1.变量 2.Q_OBJECT展开后的函数声明 3.自定义信号 三.connect 四.信号触发 1.直连 2.队列连接 五.总结 六.推荐阅读 一.问题 学习Qt有一段时 ...

  5. Qt 信号槽如何传递参数(或带参数的信号槽)

                                    信号槽如何传递参数(或带参数的信号槽) 利用Qt进行程序开发时,有时需要信号槽来完成参数传递.带参数的信号槽在使用时,有几点需要注意的地 ...

  6. 如何才能学到Qt的精髓——信号槽之间的无关性,提供了绝佳的对象间通讯方式,QT的GUI全是自己的一套,并且完全开源,提供了一个绝好机会窥视gui具体实现

    姚冬,中老年程序员 叶韵.KY Xu.赵奋强 等人赞同 被邀请了很久了,一直在思考,今天终于下决心开始写回答. 这个问题的确是够大的,Qt的代码规模在整个开源世界里也是名列前茅的,这么大的项目其中的精 ...

  7. QT 信号槽connect中解决自定义数据类型或数组作为函数参数的问题——QT qRegisterMetaType 注册MetaType——关键:注册自定义数据类型或QMap等容器类

    一般情况下信号槽直接连接方式不会出现问题,但是如果信号与槽在不同线程或Qt::QueuedConnection方式连接,可能会在连接期间报以下类似问题,如: QObject::connect: Can ...

  8. Qt 学习之路 2(4):信号槽

    Home / Qt 学习之路 2 / Qt 学习之路 2(4):信号槽 Qt 学习之路 2(4):信号槽  豆子  2012年8月23日  Qt 学习之路 2  110条评论 信号槽是 Qt 框架引以 ...

  9. QT窗体间传值总结之Signal&Slot

    在写程序时,难免会碰到多窗体之间进行传值的问题.依照自己的理解,我把多窗体传值的可以使用的方法归纳如下: 1.使用QT中的Signal&Slot机制进行传值: 2.使用全局变量: 3.使用pu ...

  10. 【学习笔记】QT从入门到实战完整版(按钮和信号槽)(1)

    介绍说明 学习 QT 的目的只是为了可以实现跨平台的具有GUI 的程序,以前用的 MFC,但是无法应用在嵌入式平台.后来在全志的 Tina 系统中有看到 QT ,因此特地去了解了QT,挺有意思的,UI ...

随机推荐

  1. 存储过程专题(Oracle)

    本文转自 https://www.cnblogs.com/lukelook/p/9600407.html,感谢博主 豆豆DE思念 整理分享. 1.Oracle 存储过程基本格式  最简单的版本 is ...

  2. uni-app之条件编译

    ifdef 仅出现在XXx平台上 仅出现在 App 平台下的代码 #ifdef APP-PLUS 需条件编译的代码 #endif <!-- 只在H5上有哈 --> <!-- #ifd ...

  3. SQL Server 2022新功能:将数据库备份到S3兼容的对象存储

    SQL Server 2022新功能:将数据库备份到S3兼容的对象存储 本文介绍将S3兼容的对象存储用作数据库备份目标所需的概念.要求和组件. 数据库备份和恢复功能在概念上类似于使用SQL Serve ...

  4. 使用guava的cache实现缓存

    一.maven <?xml version="1.0" encoding="UTF-8"?> <project xmlns="htt ...

  5. Presto配置调整

    一.常用优化 1.每个查询最大使用内存1T,目前配置 3T query.max-memory=1T 2.每个工作节点最多加载10GB数据,目前 60GB query.max-memory-per-no ...

  6. SHA1字符串加密

    使用SHA1算法,生成某个字符串的hash值作为该字符串所代表对象的唯一标识: Demo: using System; using System.Collections.Generic; using ...

  7. 『Python底层原理』--Python整数为什么可以无限大

    整数类型是编程中最常见的数据类型之一,但它的实现细节却鲜为人知. 与其他语言不同,Python 的整数是任意精度的,这意味着它们可以无限大,仅受限于内存. 这种特性使得 Python 在处理大整数时非 ...

  8. RNN、lstm和GRU推导

    RNN:(Recurrent Neural Networks)循环神经网络 第t层神经元的输入,除了其自身的输入xt,还包括上一层神经元的隐含层输出st−1 每一层的参数U,W,V都是共享的 lstm ...

  9. 从零构建你的第一个RESTful API:HTTP协议与API设计超图解指南 🌐

    title: 从零构建你的第一个RESTful API:HTTP协议与API设计超图解指南 date: 2025/2/26 updated: 2025/2/26 author: cmdragon ex ...

  10. CF2018C Tree Pruning

    分析 好像官方题解是反向求解的,这里提供一个正向求解的思路,即直接求出最后所有叶节点到根的距离相同为 \(x\) 时需要删除的结点数 \(ans_x\) . 如果我们最后到根的相同距离为 \(x\), ...