martedì 15 novembre 2016

QML trick: force re-evaluation of a property binding

Everyone who has used the QML programming language should love its property binding mechanism, which is made very easy to express via its declarative paradigm:


import QtQuick 2.0

Column {
  TextEdit {
    id: inputField
  }
  Text {
    id: secondLabel
    text: "You typed: " + inputField.text
  }
}

If you have worked on a project where QML was used along with C++ code, you might have bumped into a scenario where some bindings don't seem to work as you'd expect:


import QtQuick 2.0

Text {
  text: cppModel.get(row, "description")

  MyCppModel {
    id: cppModel
  }
}

Whenever the value of the row variable changes, the property binding is re-evaluated and the text is correctly updated; however, if row is not changed and it's only the cppModel contents to change, then you'll be out of luck: the text won't update accordingly.
This apparently unexplicable behaviour has in fact an obvious explanation: the QML engine re-evaluates the property bindings only when a variable appearing in the expression (or in a javascript function used in the expression) is modified. But since in this case the function get() is a C++ method, the QML engine is not aware of this method's internals and does not see any change requiring refreshing the binding.

The first thing we have to do in order to solve this problem is to find a signal which tells us when it's time to re-evaluate the bindings. If you are working with a model derived from QAbstractItemModel, then you probably want to bind to the dataChanged signal, or maybe modelReset (if you simply reset the whole model when updating it). In the general case, your C++ class should have a signal or a property which gets updated when needed. If not, add it! :-)

The trick I've been using in some occasions is then to artificially trigger a change in a property used in the binding, in order to force the refresh:

import QtQuick 2.0

Text {
  text: cppModel.get(row, "description")

  MyCppModel {
    id: cppModel
    onDataChanged: {
      var tmp = row
      row = -1
      row = tmp
    }
  }
}

I know, I know, this is horrible. It works, but it causes the expression to be re-evaluated twice, since we are changing the row property twice. Besides, chaning the row property might have some unwanted side effects in other objects, if this property is used in other property bindings. And what if the C++ method doesn't take any parameters at all? How can we trigger its re-evaluation then?

I recently came up with another solution, which is the reason of this blog post. And yes, it's still a hack, but an elegant one. It uses the rarely used comma operator, which in Javascript works in the same way as in C++: it evaluates all operands, and returns the value of the last one. So, this is what we can do:


import QtQuick 2.0

Text {
  text: cppModel.updateCounter, cppModel.get(row, "description")

  MyCppModel {
    id: cppModel
    property int updateCounter: 0
    onDataChanged: updateCounter++
  }
}

Note that we've added an extra property, updateCounter, which we'll change whenever we want the binding expression to be re-evaluated. In this case I've added the property to the QML component, but if you own the C++ object you could add it to your C++ object. And of course, if your C++ object already has a property which gets updated when you also want to update the binding, then you can use that instead: it can be of any type, it doesn't have to be an int.

I hope you'll find this tip useful. :-)

Etichette: , , ,

0 Commenti:

Posta un commento

Iscriviti a Commenti sul post [Atom]

<< Home page