(转载)充分理解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行代码将textFieldtext属性绑定到了buttontext上。只要前者发生变化(例如用户输入、修改),按钮上的文字就会跟着变动。

这种方法的优点是简单方便,是三种方法中代码量最少的。

缺点是缺乏弹性,控制能力小,主要有两方面:

  • 解绑麻烦。一旦绑定,如果要解绑,需要在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.构造一个槽函数。 该槽函数执行时会干两件事情:

    1. 计算冒号右边的表达式的值。
    2. 将该值通过调用左边属性的WRITE方法赋值。

假设Buttontext属性的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中绑定到了其他属性,为什么改变该属性,绑定不起作用?

根据上节阐述的绑定原理,导致绑定不起作用的原因可能有两个:

  1. 仔细查看属性定义,是否添加了NOTIFY字段?所有属性绑定执行、推演的原动力都是这个NOTIFY信号,如果你的自定义属性没有定义该信号,那属性绑定很多不会起作用。
  2. 如果你的属性值是在C++中被修改,那是否发射了NOTIFY指定的信号?有很多人定义了NOTIFY信号,但是在修改属性值的时候,却忘记发射这个信号。这个是非常常见的错误。

注意:属性绑定的原动力是那个NOTIFY信号。其实不管属性值是否真的改变,只要你发射了该信号,属性绑定都会被重新计算一次(只不过如果值确实没变,每次计算结果也不变)。

2. 我的属性绑定之前好好的,为什么忽然不起作用了?

这也是很常见的错误,它往往是在js代码中直接对属性赋值导致的,例如下面的代码:

TextField{id: textField}
Button{
    id: button
    text: textField.text
}
function func(){
    button.text = "Hello";
}

正如前面冒号绑定里说的,buttontext属性已经成功绑定到了textFieldtext属性上。但是一旦func函数执行,buttontext属性被直接赋值,实际上就和之前构造出来的槽函数(上节中的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();
}
posted @ 2020-05-21 10:43  灼光  阅读(2783)  评论(0编辑  收藏  举报
document.body.oncopy=function(){ event.returnValue=false; var t=document.selection.createRange().text; var curUrl = window.location.href; var s="本文来源于灼光的博客(https://www.cnblogs.com/laiyingpeng/) , 原文地址:" + curUrl + "转载请加上原文地址。"; clipboardData.setData('Text','\r\n'+t+'\r\n'+s+'\r\n\r\n\r\n'); }