基本使用
(详见官方博文:All about property bindings in Qt 6.2)
在 QML 中,我们可以通过属性绑定使得关联的属性自动更新。如下的代码片段使得 height 绑定到 width,当 wdith 值变化时,height 的值也能根据绑定的表达式重新计算:
import QtQuick 2.15
Rectangle {
width: 10
height: width
}
在 Qt6 中,属性绑定这一机制也被引入到了 Qt C++,将上面 QML 代码转化为 C++ 逻辑就是:
class Rectangle {
public:
QProperty<int> width{10};
QProperty<int> height;
Rectangle() {
height.setBinding([this]() {
return width.value();
});
}
};
在 QML 中,属性值修改时会触发 onXXXChanged 信号,如:
onWidthChanged: { /**/ }
onHeightChanged: { /**/ }
QProperty 也提供了三个对应的接口:
//值变更时回调
template <typename Functor>
QPropertyChangeHandler<Functor> QProperty::onValueChanged(Functor f)
//绑定,以及值变更时回调
template <typename Functor>
QPropertyChangeHandler<Functor> QProperty::subscribe(Functor f)
//值变更时回调,返回的不是模板更容易存储,可作为类成员
template <typename Functor>
QPropertyNotifier QProperty::addNotifier(Functor f)
Rectangle rect;
QPropertyChangeHandler cb1=rect.height.onValueChanged([&]{
qDebug()<<"callback 1"<<rect.width.value()<<rect.height.value();
});
QPropertyChangeHandler cb2=rect.height.subscribe([&]{
qDebug()<<"callback 2"<<rect.width.value()<<rect.height.value();
});
QPropertyNotifier cb3=rect.height.addNotifier([&]{
qDebug()<<"callback 3"<<rect.width.value()<<rect.height.value();
});
qDebug()<<"before"<<rect.width.value()<<rect.height.value();
rect.width = 100;
qDebug()<<"after"<<rect.width.value()<<rect.height.value();
这里的回调订阅不依赖信号槽,Rectangle 类也不依赖 Qt 元对象系统。
要注意的是,如果返回的观察者对象释放了,回调订阅也就取消了。可以使用 addNotifier 订阅通知,然后将接口返回的观察者对象作为类成员保存。 但有时我们想用 QObject 那种信号通知的方式,只要对象没释放就能触发槽,而不是还要自己管理信号槽 Connector 对象,可以自己对 QProperty 扩展。
可以用 Q_PROPERTY 宏声明一个可绑定属性,写法上有一点变化:
借用官方示例:
class Rectangle : public QObject {
Q_OBJECT
Q_PROPERTY(int height READ height WRITE setHeight NOTIFY heightChanged BINDABLE bindableHeight)
Q_PROPERTY(int width READ width WRITE setWidth NOTIFY widthChanged BINDABLE bindableWidth)
Q_PROPERTY(int area READ area NOTIFY areaChanged BINDABLE bindableArea)
public:
Rectangle() { m_area.setBinding([&] { return m_height * m_width; }); }
void setHeight(int h) { m_height = h; }
int height() const { return m_height; }
QBindable<int> bindableHeight() { return &m_height; }
void setWidth(int w) { m_width = w; }
int width() const { return m_width; }
QBindable<int> bindableWidth() { return &m_width; }
int area() const { return m_area; }
QBindable<int> bindableArea() { return &m_area; }
signals:
void heightChanged();
void widthChanged();
void areaChanged();
private:
Q_OBJECT_BINDABLE_PROPERTY(Rectangle, int, m_height, &Rectangle::heightChanged)
Q_OBJECT_BINDABLE_PROPERTY(Rectangle, int, m_width, &Rectangle::widthChanged)
Q_OBJECT_BINDABLE_PROPERTY(Rectangle, int, m_area, &Rectangle::areaChanged)
};
可以通过这些返回 QBindable 对象的接口来完成属性绑定:
Rectangle rect;
rect.bindableHeight().setBinding([&]{
return rect.width();
});
auto cb=rect.bindableArea().addNotifier([&]{
qDebug()<<"area changed"<<rect.area();
});
rect.setWidth(100);
qDebug()<<"rect area"<<rect.area();
如果一个 QProperty 属性绑定的表达式中有多个 QProperty 参与计算,在 Qt 当前的通知策略下,每个表达式中的对象变化时都会触发通知。
QProperty<int> a {1};
QProperty<int> b {2};
QProperty<int> c;
c.setBinding([&] { return a + b; });
auto notifier = c.addNotifier([&] {
qDebug()<<"Value of c changed:"<<c.value();
});
a = 2;
b = 3;
上面的代码会输出:
Value of c changed: 4
Value of c changed: 5
通过对属性更新分组,可以使得只触发一次通知:
Qt::beginPropertyUpdateGroup();
a = 2;
b = 3;
Qt::endPropertyUpdateGroup();
此时只输出:
Value of c changed: 5
注意事项
(详见文档:https://doc.qt.io/qt-6/bindableproperties.html)
addNotifier 等接口返回的对象释放后就取消了订阅,不再触发回调。
依赖跟踪仅适用于可绑定属性,即只有绑定表达式中的可绑定对象更新才会触发绑定属性的更新。
绑定表达式中的对象需要注意生命周期,寿命应该比这个绑定长。
可绑定属性系统不是线程安全的。一个线程上的绑定表达式中使用的属性不得被任何其他线程读取或修改。不能将具有绑定属性的QObject派生类的对象移动到不同的线程。此外,具有在绑定中使用的属性的QObject派生类的对象不得移动到不同的线程。在这种情况下,它是用于同一个对象的属性绑定还是用于另一个对象的属性绑定是无关紧要的。
避免循环绑定。
用作绑定的函数以及在绑定中调用的所有代码不得 co_await,这样做会混淆属性系统对依赖项的跟踪。
简易实现
参考官方博文代码,通过一个 function 成员存储绑定的表达式,在 getter 函数中计算表达式得到绑定的值。
template <typename T>
class QProperty
{
std::function<T()> binding = nullptr;
T data;
public:
T value() const {
if (binding) return binding();
return data;
}
void setValue(const T &newValue) {
if (binding) binding = nullptr;
data = newValue;
}
void setBinding(std::function<T> b) { binding = b; }
};
在此基础上增加通知订阅和依赖跟踪等接口,就实现了简单的属性绑定。
参考
博客:https://www.qt.io/blog/property-bindings-in-qt-6
博客:https://www.qt.io/blog/all-about-property-bindings-in-qt-6.2
文档:https://doc.qt.io/qt-6/bindableproperties.html
文档:https://doc.qt.io/qt-6/qbindable.html
文档:https://doc.qt.io/qt-6/qproperty.html