QML Property behaviour
-
Hello everyone,
I have the following behaviour in my project that I cant understand nor find exlanations for it:
I am able to update QML properties from C++. Also I managed to send qml data to C++. Buto I cant modify a property from inside my qml component AND from C++ at the same time - at least not as I would expect.
In my example I have a counter in C++ that is bound to a qml property "value" displayed as a text. At first the text keeps updating fine and always shows the c++ counters value.
But my component also contains a mousearea that, when pressed, sets the same property "value" to another number. Once I click that mouse area, all the changes from the c++ counter are ignored and from now on can only be modified from inside the qml component.Id expect the latest change to take effekt, no matter if it comes from the mousearea or from the c++ counter. Is my expectation wrong? Any explanations are warmly welcome.
-
@Mr-Dollbohrer
can you show us the code at which you are facing the issue ?Below links might help you.
The Property System
Exposing Attributes of C++ Types to QMLAll the best.
-
i guess you are breaking the binding when you change the value from QML code.
You can create a c++ method inside your counter class, and use that method to change the value from your QML code// counter.qml Window { visible: true width: 640 height: 480 Text { id:lab anchors.centerIn: parent text: counter.cpt } Button{ text: "set value to -666" // onClicked: {lab.text = -666} onClicked: {counter.setQmlValue(-666)} // c++ method } } // Counter.h #ifndef COUNTER_H #define COUNTER_H #include <QObject> #include <QTimer> class Counter : public QObject { Q_OBJECT Q_PROPERTY(int cpt READ cpt WRITE setCpt NOTIFY cptChanged) public: explicit Counter(QObject *parent = nullptr); Q_INVOKABLE void setQmlValue(const int &qmlVal){ setCpt(qmlVal); } int cpt() const { return m_cpt; } void setCpt(int cpt) { if (m_cpt == cpt) return; m_cpt = cpt; emit cptChanged(m_cpt); } signals: void cptChanged(int); private : int m_cpt=0; QTimer *countTimer; void incrementCount(){setCpt(m_cpt+1);} }; #endif // COUNTER_H #include "counter.h" //Counter.cpp Counter::Counter(QObject *parent) : QObject(parent) { countTimer = new QTimer(this); QObject::connect(countTimer,&QTimer::timeout,this,Counter::incrementCount); countTimer->start(1000); } //main.cpp #include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include "counter.h" int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); QQmlApplicationEngine engine; Counter cpt; engine.rootContext()->setContextProperty("counter",&cpt); engine.load(QUrl(QStringLiteral("qrc:/counter.qml"))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); }
-
Helly everyone and thanks a lot for your answers.
I understand that the behaviour of my code is not following a concept/design so the problem must lie in my code. Here is a simple example that follows the design of my project and illustrates the loss of binding from c++.
If you start the project, c++ modifies the value every second. But once you click on the qml button, there will never be any update from c++ anymore.
//object.h #ifndef OBJECT_H #define OBJECT_H #include <QObject> class object : public QObject { Q_OBJECT Q_PROPERTY(double value READ getValue WRITE setValue NOTIFY valueChanged) private: double value; public: object(double); double getValue(); void setValue(double); signals: void valueChanged(); }; #endif // OBJECT_H
//object.cpp #include "object.h" object::object(double init) { value = init; } double object::getValue(){ return value; } void object::setValue(double newValue){ value = newValue; emit valueChanged(); }
//backend.h #ifndef BACKEND_H #define BACKEND_H #include <QObject> #include "object.h" class backend : public QObject { Q_OBJECT public: backend(); QList<QObject*> objectList; public slots: void modifyValue(); private: double anotherValue; }; #endif // BACKEND_H
//backend.cpp #include <QTimer> #include "backend.h" backend::backend() { anotherValue = 1.23; objectList.append(new object(anotherValue)); objectList.append(new object(4.56)); objectList.append(new object(7.89)); QTimer *timer = new QTimer(this); QObject::connect(timer, SIGNAL(timeout()), this, SLOT(modifyValue())); timer->start(1000); } void backend::modifyValue() { anotherValue += 1; qobject_cast<object *>(objectList.at(0))->setValue(anotherValue); }
//main.cpp #include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include <backend.h> int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); backend myBackend; QQmlApplicationEngine engine; QQmlContext *ctxt = engine.rootContext(); ctxt->setContextProperty("objectModel", QVariant::fromValue(myBackend.objectList)); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); }
//main.qml import QtQuick 2.12 import QtQuick.Window 2.12 Window { visible: true width: 640 height: 480 title: qsTr("Hello World") Component { id: objectDelegate Item { height: 100 width: 300 ObjectModifier { id: objectModifier myValue: model.modelData.value } } } ListView { id: objectView anchors.fill: parent model: objectModel delegate: objectDelegate clip: true } }
//ObjectModifier.qml import QtQuick 2.0 import QtQuick.Layouts 1.12 Item { property real myValue: 0.0 anchors.fill: parent ColumnLayout{ spacing: 2 Rectangle { height: 50 width: 250 border.width: 3 radius: 5 Text{ anchors.fill: parent text: "Value: "+myValue horizontalAlignment: "AlignHCenter" verticalAlignment: "AlignVCenter" color: "black" } } Rectangle { height: 50 width: 250 color: "gray" Text{ anchors.fill: parent text: "Modify object" horizontalAlignment: "AlignHCenter" verticalAlignment: "AlignVCenter" color: "white" } MouseArea { id: theClickBox anchors.fill: parent onClicked: { myValue = myValue+0.01 } } } } }
-
@Mr.-Dollbohrer said in QML Property behaviour:
If you start the project, c++ modifies the value every second. But once you click on the qml button, there will never be any update from c++ anymore.
This is because you are breaking your binding.
Here you define your binding:ObjectModifier { id: objectModifier myValue: model.modelData.value }
Here you break it:
onClicked: { myValue = myValue+0.01 }
You are binding a new value to myValue.
Your approach is off to begin with. Use the built in roles (search role to find info about index and modelData) that are exposed to delegates of a ListView. They are index and modelData. You could use modelData in place of myValue and it would work. However your model is suspect.
ctxt->setContextProperty("objectModel", QVariant::fromValue(myBackend.objectList));
The docs say it returns a QVariant containing a copy of the data. I am not sure this is what you want. Especially if you intend to work with this data in the backend. If you want to expose a list properly then you need to use QAbstractListModel and derive a proper list class from that.
Here is more info on doing that. The cool part of building the list model from the abstract class is you get to define the role names yourself. You can make them a lot more relevant to your task.
-
Also, here is an example using default roles:
main.cpp#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include <QVector> int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); QQmlApplicationEngine engine; // simple list data QStringList slist; slist << "one"; slist << "two"; slist << "three"; slist << "four"; slist << "five"; QQmlContext *ctxt = engine.rootContext(); ctxt->setContextProperty("objectModel", slist); const QUrl url(QStringLiteral("qrc:/main.qml")); QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, [url](QObject *obj, const QUrl &objUrl) { if (!obj && url == objUrl) QCoreApplication::exit(-1); }, Qt::QueuedConnection); engine.load(url); return app.exec(); }
main.qml:
import QtQuick 2.12 import QtQuick.Window 2.12 Window { visible: true width: 640 height: 480 title: qsTr("QML List Roles") ListView { id: objectView anchors.fill: parent model: objectModel delegate: objectDelegate clip: true } Component { id: objectDelegate Item { height: 100 width: 300 ObjectModifier { id: objectModifier } } } }
Your ObjectModifier.qml updated with modelData usage:
import QtQuick 2.0 import QtQuick.Layouts 1.12 Item { anchors.fill: parent ColumnLayout { spacing: 2 Rectangle { height: 50 width: 250 border.width: 3 radius: 5 Text{ anchors.fill: parent text: "Value: "+modelData horizontalAlignment: "AlignHCenter" verticalAlignment: "AlignVCenter" color: "black" } } Rectangle { height: 50 width: 250 color: "gray" Text{ anchors.fill: parent text: "Modify object" horizontalAlignment: "AlignHCenter" verticalAlignment: "AlignVCenter" color: "white" } MouseArea { id: theClickBox anchors.fill: parent onClicked: { modelData = modelData+"x" } } } } }
-
Thanks so much for all that great help. I am basically going through the examples copying code and trying modify it with a lot of trial and error so that I sometimes dont really know why stuff work or doesnt work.
Thanks to your explanations I now have a clue how binding works (only one binding is allowed, what makes sense now that im thinking about it) and that it is possible to directly write to the modeldata.
Also Ill try to dive deeper into how to expose lists to qml. My "solution" was just another part I found in an example somewhere without really knowing what other options I have and what the advantages and disadvantages are.
Again thanks for your time and all the great hints and help!!!
-
I am trying to see if there is a way to do a generic templated way to expose common QML QVector types to QML from C++. But for some reason my list values do not get updated properly even though the C++ values are getting changed and the signal is getting called.
main.cpp:#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include <QVector> #include <QAbstractListModel> #include <QDebug> template<class T> class VExposer : public QAbstractListModel { public: explicit VExposer(QObject *parent = nullptr):QAbstractListModel(parent){} int rowCount(const QModelIndex &parent = QModelIndex()) const override { Q_UNUSED(parent); return m_vector.count(); } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { int row = index.row(); if(row < 0 || row >= m_vector.count()) return QVariant(); if(role == Qt::DisplayRole) return m_vector[row]; else return QVariant(); } bool setData(const QModelIndex &index, const QVariant &value, int role) override { if (!hasIndex(index.row(), index.column(), index.parent()) || !value.isValid()) return false; qInfo() << index.row() << value.toString(); T &item = m_vector[index.row()]; //if (role == Qt::DisplayRole){ if (role == Qt::EditRole){ item = value.toDouble(); qInfo() << m_vector[index.row()]; } else return false; emit dataChanged(index, index, { role } ); return true ; } // hacky, don't do this for real app, create a bunch of functions for getting/setting values QVector<T>& getVector(){return m_vector;} private: QVector<T> m_vector; }; void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles){ qInfo() << "dataChanged signal gets called!" << topLeft.row() << bottomRight.row(); } int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); QQmlApplicationEngine engine; VExposer<qreal> vex(&app); auto& v_vex = vex.getVector(); v_vex << 1.0; v_vex << 2.0; v_vex << 3.0; v_vex << 4.0; v_vex << 5.0; //qInfo() << vex.rowCount(); // proves signals work and get called QObject::connect(&vex, &VExposer<qreal>::dataChanged, dataChanged); QQmlContext *ctxt = engine.rootContext(); // make model available ctxt->setContextProperty("objectModel", &vex); const QUrl url(QStringLiteral("qrc:/main.qml")); QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, [url](QObject *obj, const QUrl &objUrl) { if (!obj && url == objUrl) QCoreApplication::exit(-1); }, Qt::QueuedConnection); engine.load(url); return app.exec(); } //#include "main.moc"
main.qml:
import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Layouts 1.12 Window { visible: true width: 640 height: 480 title: qsTr("QML List Roles") ListView { id: objectView anchors.fill: parent model: objectModel delegate: objectDelegate clip: true } Component { id: objectDelegate Item { height: 100 width: 300 Item { anchors.fill: parent ColumnLayout { spacing: 2 Rectangle { height: 50 width: 250 border.width: 3 radius: 5 Text{ anchors.fill: parent text: "Value: %1".arg(display) horizontalAlignment: "AlignHCenter" verticalAlignment: "AlignVCenter" color: "black" } } Rectangle { height: 50 width: 250 color: "gray" Text{ anchors.fill: parent text: "Modify object" horizontalAlignment: "AlignHCenter" verticalAlignment: "AlignVCenter" color: "white" } MouseArea { id: theClickBox anchors.fill: parent onClicked: { //display = display+0.01 // updates display edit = display+0.01 // changes value in variable, but does not update display } } } } } } } }
I know I cannot template a class that has the Q_OBJECT macro in it. But you can template a class that derives from a QObject class. So I figured QAbstractListModel would be a good candidate. Everything works except the signal does not seem to update the ListView in QML properly.
Does anyone know if I am missing something important here? I would love for this template to work as it would save time writing boiler plate code to expose vectors as models.
-
If you want a generic
QAbstractItemModel
from a list of objects deriving QObject, there is : QQmlObjectListModel from http://gitlab.unique-conception.org/qt-qml-tricks/qt-qml-models (some slides about it here : https://docs.google.com/presentation/d/13pkRav2Fks_AKTXfKtGTyBuc_RmkZGiDRQZTr4usQFk/pub?start=false&loop=false&delayms=3000 ) -
@GrecKo said in QML Property behaviour:
If you want a generic QAbstractItemModel from a list of objects deriving QObject
I saw that, but I was hoping to make a generic one for just types like double, int, QString. I will create a new post as I think I have one most of the way there, but for some reason it won't update the list values even though the signal is getting called.
-
Ah yes, I misread.
Indeed, it should be doable withQVariant::value<T>()
andQVariant::fromValue(const T &value)