Binding C++ properties exposed to QML to dynamically created QML objects
-
@Eeli-K It very well might be that you are right about the problem being something related to C++! I created a working example where I bind properties of dynamic objects using QML only, and it works! It does however use a method explicitly warned against in the documentation, so I think I should find an alternative. The documentation says about ListModel.get that():
Warning: The returned object is not guaranteed to remain valid. It should not be used in property bindings.
which is what I'm doing now.
I will have to check again on Monday why this doesn't work in the original code, and if the problem lies with the C++ property!
main.qml
import QtQuick 2.6 import QtQuick.Window 2.2 import QtQuick.Controls 2.1 Window { visible: true width: 640 height: 480 ListModel { id: listModel } Column { id: mainCol anchors.centerIn: parent spacing: 10 Button { text: "Click to create object" onClicked: { var component = Qt.createComponent("Box.qml"); var obj = component.createObject(mainCol); listModel.append({"obj": obj}) } } TextField { id: inputTxt } Button { text: "Click to bind properties" onClicked: { for (var i = 0; i < listModel.count; ++i) { var dynObj = listModel.get(i).obj; dynObj.boxTxt = Qt.binding(function() {return inputTxt.text}); } } } } }
Box.qml
import QtQuick 2.0 Rectangle { property alias boxTxt: txt.text width: 100 height: 100 color: "lightblue" Text { id: txt text: "Dynamic object" } }
-
It does however use a method explicitly warned against in the documentation, so I think I should find an alternative. The documentation says about ListModel.get that():
Warning: The returned object is not guaranteed to remain valid. It should not be used in property bindings.I think it rather means using it in the right side of the binding. But I may be wrong. After all, I have demonstrably been wrong sometimes :)
But here is my code:
main.qml:import QtQuick 2.7 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.3 ApplicationWindow { visible: true width: 640 height: 480 title: qsTr("Hello World") ColumnLayout { id: layout anchors.fill:parent ListModel { id: boxListModel } Button { text: "create qml model" onClicked: { for (var i = 0; i < 1; ++i) { var component = Qt.createComponent("Box.qml"); var obj = component.createObject(layout); boxListModel.append({"obj": obj}) obj.boxTxt = Qt.binding(function() {return backend.proplist[i].x}) } } } Button { id: button1 text: "change backend" onClicked: { backend.changeList() } } } }
backend.changeList() which you should be able to add to your backend class easily:
void MyClass::changeList() { float f = m_list.at(0).value<QVector2D>().x() + 1; m_list.clear(); m_list.append(QVariant(QVector2D{f, f})); emit arrayChanged(); }
(The m_list is supposed to have at least one QVector2D in it.)
This doesn't work, but if you change
obj.boxTxt = Qt.binding(function() {return backend.proplist[i].x})
to
obj.boxTxt = Qt.binding(function() {return backend.proplist[0].x})
it works!
-
@Obi-Wan (Had to finish the previous post in a hurry, continuing...) So the problem is not in dynamic objects or C++ but that the property binding function can't handle variables taken from the immediate function context, in this case the loop counter. However, I got this working:
var f = function(indx) {return function(){return backend.array[indx].x}} obj.boxTxt = Qt.binding(f(i))
Don't ask me why.
-
@Eeli-K said in Binding C++ properties exposed to QML to dynamically created QML objects:
Don't ask me why.
I asked myself. This seems to be an example of a closure and functional programming in javascript. f() returns a function where indx is bound, it has a new value in each invocation of f(), and therefore the inner anonymous function works in the binding without the original scope. The backend object is in scope anyways (in QML) and the function is run every time when the array property is changed.
There's nothing miraculous in it, but the Qt documentation doesn't seem to give any advice about these kinds of situations where the binding function should handle variables which are in the outer function scope but not in the QML scope.
-
In the documentation it says:
The Repeater type creates all of its delegate items when the repeater is first created. This can be inefficient if there are a large number of delegate items and not all of the items are required to be visible at the same time. If this is the case, consider using other view types like ListView (which only creates delegate items when they are scrolled into view) or use the Dynamic Object Creation methods to create items as they are required.
The Dynamic Object Creation methods they refer to are the ones I am using :)
Why do you need that though ? A repeater can do what you are doing with less code.
Most of the time, handling dynamic object creation is counter-productive. -
-
@GrecKo @Eeli-K Thank you very much both of you!
I immediately got it working with your fix Eeli-K. I guess it goes to show that some knowledge of javascript is indeed useful! (I personaly have about zero, and just try to do what I would in C++ and adapt it from there ...)
I also got it working with the Repeater now, and it does indeed do what I want with a lot less code:
Repeater { model: backEnd.array.length // Adjusts number of boxes according to C++ list length Box { // Binds x and y properties using Repeater's index in the array pos_x: backEnd.array[index].x pos_y: backEnd.array[index].y } }
For some reason my mind was sort of set on the Repeater having to know the number of items on startup. It could just be that I'm not used to thinking QML'y and couldn't comprehend that binding the model property to the array length would create the number of objects as needed. (It isn't crucial, but I still don't understand the name of that property, what is the rationale behind "model" meaning roughly "number of items created by Repeater"?)
Googling Dynamic Object Creation QML leads you here which I guess led me to think this was the primary way of achieving dynamic object creation.
I guess I really should read up on models, views and delegates, and just the general QML "mindset"!
Again, thanks, I learned a lot from this little exercise! :)
-
@Obi-Wan a model can be a
QAbstractItemModel
,QList<QObject*>
,QStringList
, a js array, an integer, a single instance of an object, etc. You can read more about it here : http://doc.qt.io/qt-5/qtquick-modelviewsdata-modelview.htmlYour example could be simplified by directly using the array, and not just its length
Repeater { model: backEnd.array Box { pos_x: modelData.x pos_y: modelData.y } }
-
Indeed :) , sorry for the brevity of my answer but I wasn't sure what you needed and was kinda lost by where the previous discussion was heading.
As you said, I feel that the Dynamic Object Creation in QML article is misguiding a lot of people (here and on StackOverflow) by not mentioning saner alternatives like model + repeater/view or even a declaratively createdComponent
.Ultimately I think that you should use imperative object creation only for temporary object needed by the UI layer, like showing a dialog for example. I have yet to see another legit usecase for it (or I don't remember it).
-
@GrecKo said in Binding C++ properties exposed to QML to dynamically created QML objects:
Ultimately I think that you should use imperative object creation only for temporary object needed by the UI layer, like showing a dialog for example. I have yet to see another legit usecase for it (or I don't remember it).
I will keep this in mind!
Hopefully this little discusion might help someone else struggling to understand the same concepts!