(转载)充分理解QML的属性绑定
原文地址:
https://zhuanlan.zhihu.com/p/56401271
感谢作者!
这里我想给大家讲讲Qt QML里非常重要的一个概念:属性绑定(Property binding)。现代化的开发语言、框架都讲究自动化、智能化,在笔者看来,属性绑定则是QML中这方面的代表。用好属性绑定可以极大提高我们的开发效率。本文首先介绍何为QML属性绑定,然后用通俗易懂的说法来阐释其底层原理,最后和大家讨论开发时几个常见的问题。
如何实现QML属性绑定
在QML中实现属性绑定有三种方法,每种方法都有其各自的优缺点。
1. 冒号绑定
这是最常见的绑定方法,在定义属性时使用QML的冒号语法,所以笔者称之为“冒号绑定”。
例如下面的QML代码就实现了一个属性绑定:
TextField{id: textField}
Button{
id: button
text: textField.text
}
上面第4行代码将textField
的text
属性绑定到了button
的text
上。只要前者发生变化(例如用户输入、修改),按钮上的文字就会跟着变动。
这种方法的优点是简单方便,是三种方法中代码量最少的。
缺点是缺乏弹性,控制能力小,主要有两方面:
- 解绑麻烦。一旦绑定,如果要解绑,需要在js代码中使用赋值操作;对于需要反复绑定、解绑的情况更是麻烦。
- 无法绑定不在当前QML文件的对象。因为冒号绑定只能写在QML对象定义的地方,所以对于别处传进来的对象,例如通过
setContextProperty
传进来的对象,就爱莫能助了。
2. 使用Binding
QML中专门提供了一个类型Binding
来实现属性绑定。上面的例子如果改用Biding来写则代码如下:
TextField{id: textField}
Button{id: button}
Binding{
target: button
property: "text"
value: textField.text
}
这种方法的优点主要有两个:
- 绑定控制能力强。查阅
Binding
的文档,可以看到它有一个when
属性,当其为true
时,绑定生效;为false
时绑定无效,也就是解绑。 - 可以绑定任意对象。
target
属性除了可以设置为当前QML文件中的对象,也可以设置任何地方传进来的对象。
这两个可以说完美解决了上面第一种属性绑定的问题。
该方法有两个缺点:
- 代码量多。每个属性绑定都用
Binding
那是不能忍受的。 - 构造
Binding
对象需要花一点时间,这个对于特别大的程序可能会有一定影响。
所以只在需要的时候用该方法。
3. 使用Qt.binding()函数
这是最后一种属性绑定方法。上面的例子改用该方法的话代码如下:
TextField{id: textField}
Button{id: button}
Component.onCompleted:{
button.text = Qt.binding(function(){return textField.text;});
}
该方法的好处是可以写在任何js执行代码里。
缺点是只能运行时绑定。由于前两种方法都是QML语法申明,QML执行引擎在初始化的时候有机会使用JIT技术和Cache技术进行优化,而动态执行的js语句是没法进行这种优化的,因此这种方法的执行效率是三种方法中最低的。
QML属性绑定原理
在实际开发中,笔者发现很多人会用属性绑定,但经常出错,发生意外的解绑、循环绑定等问题。究其原因,往往是因为对QML属性绑定的底层原理不甚清楚,一旦程序变复杂很容易糊涂。
所谓属性绑定的原理,用直白点的话来说,就是:为什么一个属性变化了,和它绑定的属性能跟着变化?
我们还是用上节中冒号绑定的例子。当我们写下text: textField.text
这行代码的时候,QML引擎实际上做了下面这些事情:
1.构造一个槽函数。 该槽函数执行时会干两件事情:
- 计算冒号右边的表达式的值。
- 将该值通过调用左边属性的
WRITE
方法赋值。
假设Button
的text
属性的WRITE
方法为setText
,用C++的形式描述这个槽函数大概是下面这样子:
public slots:
void textBindingSlot(){
QString newText = evaluateJavaScript("function(){return textField.text;}();");
setText(newText);
}
2.扫描冒号右边的表达式,找到所有具有NOTIFY
信号的属性。 所谓NOTIFY
信号是指Qt定义属性时候的NOTIFY
字段,具体可以参看文档Qt Property System。在这个例子中,textField.text
是一个具有NOTIFY
信号的属性,也就是说它被修改后发射NOTIFY
信号。这里假设该信号为textChanged()
。
3.将这些属性的NOTIFY
信号和上面的槽函数连接。 使用connect
连接:connect(textField, SIGNAL(textChanged()), button, SLOT(textBindingSlot));
所以当任何冒号左边表达式里的具有NOTIFY
信号属性值改变时,相关信号发射,然后构造的槽函数得到执行,计算出新的值,最后将该值赋给被绑定的属性。
上述过程对于第二和第三种绑定方法也是大体一样的。
根据上述过程描述,我们也得出另一个结论:QML的属性绑定是单向的。所谓单向,是指textField.text
的改变会引起button.text
的改变;但反过来则不会,因为并没有经过上述的构造过程。
常见问题讨论
下面就笔者在实际开发中遇到的几个问题展开讨论。
1. 我在QObject派生类中自定义了一个属性,然后在QML中绑定到了其他属性,为什么改变该属性,绑定不起作用?
根据上节阐述的绑定原理,导致绑定不起作用的原因可能有两个:
- 仔细查看属性定义,是否添加了
NOTIFY
字段?所有属性绑定执行、推演的原动力都是这个NOTIFY
信号,如果你的自定义属性没有定义该信号,那属性绑定很多不会起作用。 - 如果你的属性值是在C++中被修改,那是否发射了
NOTIFY
指定的信号?有很多人定义了NOTIFY
信号,但是在修改属性值的时候,却忘记发射这个信号。这个是非常常见的错误。
注意:属性绑定的原动力是那个NOTIFY
信号。其实不管属性值是否真的改变,只要你发射了该信号,属性绑定都会被重新计算一次(只不过如果值确实没变,每次计算结果也不变)。
2. 我的属性绑定之前好好的,为什么忽然不起作用了?
这也是很常见的错误,它往往是在js代码中直接对属性赋值导致的,例如下面的代码:
TextField{id: textField}
Button{
id: button
text: textField.text
}
function func(){
button.text = "Hello";
}
正如前面冒号绑定里说的,button
的text
属性已经成功绑定到了textField
的text
属性上。但是一旦func
函数执行,button
的text
属性被直接赋值,实际上就和之前构造出来的槽函数(上节中的textBindingSlot
)解了绑。
如果直接赋值不可避免,又想在赋值之后重新绑定,那么可以用第三种绑定方法进行再绑定。或者一开始就用Binding
来绑定,用它的when
属性来控制绑定是否起作用。
3. 如何做双向绑定?
前面我们提到了QML的属性绑定是单向的,但如果我们确实需要做双向绑定该怎么办?
对于都是Qt Quick自带类型,可以很简单,例如:
TextField{
id: textField
text: button.text
}
Button{
id: button
text: textField.text
}
也就是说,上面各自做一次属性绑定即可。
但如果是自定义属性,要特别注意WRITE
部分要检查属性值是否真的被修改;只有真的被修改才往外发射NOTIFY
信号,否则很可能进入死循环。例如下面的WRITE
函数:
void setText(QString newText){
// 如果没修改,则直接返回
if(text == newText) return;
text = newText;
emit textChanged();
}