Solved Bindings between dynamic objects from C++
-
Hi all.
I'm don't know how to call this topic correctly, an issue is following:
I have a QtWidget application, which I need to port to QtQuick. This application contains a set of some objects with some properties. This properties can be changed via 'config' widgets which are created by objects. I.e. each object has a direct relationship with own config widget:
class BaseObject : public QObject { Q_OBJECT public: explicit BaseObject(QObject *parent = nullptr); virtual QWidget *createConfig() = 0; }; class FooObject : public BaseObject { Q_OBJECT Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged) public: explicit FooObject(QObject *parent = nullptr); void setValue(int value); int value() const; signals: void valueChanged(int value); private: QWidget *createConfig() final; int m_value = 0; };
for example, the FooObject know an own widget and vice-versa: the user can change the object properties from the widget's controls, and also any object's property change are displayed on a widget:
QWidget *FooObject::createConfig() { const auto w = new QSpinBox; connect(w, &QSpinBox::valueChanged, this, &FooObject::setValue); connect(this, &FooObject::valueChanged, w, &QSpinBox::setValue); return w; }
so, with QtWidgets all fine.. But, troubles starts when I try to use the QtQuick, where I use similar approach:
== main.h ==
#include <QObject> #include <QVector> #include <QPointer> class QQmlComponent; class QQmlEngine; // BaseObject class BaseObject : public QObject { Q_OBJECT public: explicit BaseObject(QObject *parent = nullptr); virtual QQmlComponent *createConfig(QQmlEngine *engine) = 0; protected: QPointer<QQmlComponent> m_component; }; // FooObject class FooObject : public BaseObject { Q_OBJECT Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged) public: explicit FooObject(QObject *parent = nullptr); void setValue(int value); int value() const; signals: void valueChanged(int value); private: QQmlComponent *createConfig(QQmlEngine *engine) final; int m_value = 0; }; // BarObject class BarObject : public BaseObject { Q_OBJECT Q_PROPERTY(bool value READ value WRITE setValue NOTIFY valueChanged) public: explicit BarObject(QObject *parent = nullptr); void setValue(bool value); bool value() const; signals: void valueChanged(bool value); private: QQmlComponent *createConfig(QQmlEngine *engine) final; bool m_value = false; }; // ObjectFactory class ObjectFactory : public QObject { Q_OBJECT public: explicit ObjectFactory(QObject *parent = nullptr); Q_INVOKABLE QObject *createConfig(int index) const; private: QVector<BaseObject *> m_objects; };
== main.cpp ==
#include "main.h" #include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlComponent> #include <QDebug> // BaseObject BaseObject::BaseObject(QObject *parent) : QObject(parent) { } // FooObject FooObject::FooObject(QObject *parent) : BaseObject(parent) { } void FooObject::setValue(int value) { if (m_value == value) return; m_value = value; qDebug() << "Foo: set value:" << m_value; emit valueChanged(m_value); } int FooObject::value() const { return m_value; } QQmlComponent *FooObject::createConfig(QQmlEngine *engine) { if (!m_component) m_component = new QQmlComponent(engine, QStringLiteral("qrc:/FooConfigItem.qml"), this); return m_component; } // BarObject BarObject::BarObject(QObject *parent) : BaseObject(parent) { } void BarObject::setValue(bool value) { if (m_value == value) return; m_value = value; qDebug() << "Bar: set value:" << m_value; emit valueChanged(m_value); } bool BarObject::value() const { return m_value; } QQmlComponent *BarObject::createConfig(QQmlEngine *engine) { if (!m_component) m_component = new QQmlComponent(engine, QStringLiteral("qrc:/BarConfigItem.qml"), this); return m_component; } // ObjectFactory ObjectFactory::ObjectFactory(QObject *parent) : QObject(parent) { m_objects.push_back(new FooObject(this)); m_objects.push_back(new BarObject(this)); } QObject *ObjectFactory::createConfig(int index) const { return m_objects.at(index)->createConfig(qmlEngine(this)); } // main int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); qmlRegisterType<ObjectFactory>("com.my.lib", 1, 0, "ObjectFactory"); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); }
== main.qml ==
import QtQuick 2.9 import QtQuick.Window 2.2 import QtQuick.Controls 2.5 import QtQuick.Layouts 1.12 import com.my.lib 1.0 as Library Window { visible: true width: 640 height: 480 title: qsTr("Hello World") Library.ObjectFactory { id: factory } ColumnLayout { anchors.fill: parent Loader { id: loader } Item { id: spacer Layout.fillHeight: true Layout.fillWidth: true } Button { text: qsTr("Create Component") property int index: 0 onClicked: { loader.sourceComponent = factory.createConfig(index); ++index; if (index > 1) index = 0; } Layout.alignment: Qt.AlignHCenter } } }
== FooConfigItem.qml ==
import QtQuick 2.9 import QtQuick.Controls 2.5 SpinBox { Component.onCompleted: console.info("Foo Config completed") Component.onDestruction: console.info("Foo Config destruction") }
== BarConfigItem.qml ==
import QtQuick 2.9 import QtQuick.Controls 2.5 CheckBox { Component.onCompleted: console.info("Bar Config completed") Component.onDestruction: console.info("Bar Config destruction") }
In this example I use the similar QQmlComponent *BaseObject::createConfig(QQmlEngine *engine) method which creates an appropriate QML component (which are sets to the QML loader)...
BUT! I don't know how to set relationship between object properties and the appropriate QML item properties. Is any patterns how to do it?
-
You are trying to transpose a solution that fits QWidget point-of-view directly into QML point-of-view and these two are quite different. So, it's like in Apollo 13 when they were trying to fit something square with something circular.
So, my first suggestion is to re-factor your approach in a more QML friendly way.Said that ... let's try to understand how to make your current code working with minimal change.
The main issues is around the fact the "createConfig" in QWidget world directly return a QWidget and then directly inside "createConfig" you can setup the relationship.
But QML prefers to create the Items by its own and having ownerships, hence the "createConfig" return a QQmlComponent that it's the definition, not the instance of the Item. In fact, you need to pass it to a loader ... and then it's the loader that create the actual "item" being display.
That means the creation of the config "item" is out of your control and you can't setup the relationship during creation time, you have to do it later as some point.A minimal change in your code is to add a property to your config Item:
/import QtQuick 2.9 import QtQuick.Controls 2.5 Item { property var objectToConfig SpinBox { id: spinb onValueModified: objectToConfig.property1 = spinb.value Component.onCompleted: console.info("Foo Config completed") Component.onDestruction: console.info("Foo Config destruction") } }
And then you need some QML code to set this relationship during loading:
// a wrapper to Loader that add this relationship binding Item { property int objectId Loader { id: loader onStatusChanged: { if (loader.status == Loader.Ready) { loader.item.objectToConfig = factory.getObject(objectId); } } } onObjectIdChanged: loader.sourceComponent = factory.createConfig(objectId) }
The above code works in this way: when you set the objectId property then it triggers the change of the loader.sourceComponent getting the component from the factory.createConfig, the loader will create the component and when the onStatusChanged is called with Ready, then the code set the objectToConfig property of the loader.item getting the object from factory.getObject(objectId).
And then relationship between the config item and the object is done.Beware that all these things happens asynchronously and the code above is only a sketch of an idea. Proper checks need to be added to be sure the code will run correctly in case all the information are not available yet.
For example, the config item should not crash if the "objectToConfig" is null. -
Yes, many thanks. Right now I created a similar approach, where I return a ready QML item from the C++ code:
QObject *FooObject::createConfig(QQmlEngine *engine) { if (!m_component) m_component = new QQmlComponent(engine, QStringLiteral("qrc:/FooConfigItem.qml"), this); const auto status = m_component->status(); if (status == QQmlComponent::Ready && !m_config) { m_config = m_component->create(); if (m_config) { // UPDATE ITEM ACCORDING TO CURRENT VALUE m_config->setProperty("value", m_value); // WHEN AN ITEM'S VALUE WILL BE CHANGED, WE CAN CATCH IT INTO SLOT setValueFromConfig!!! QObject::connect(m_config, SIGNAL(valueChanged()), this, SLOT(setValueFromConfig())); } } return m_config; }
And then I use QML StackView instead of Loader to setup a new Item:
Window { id: window visible: true width: 640 height: 480 title: qsTr("Hello World") Library.ObjectFactory { id: factory } ColumnLayout { anchors.fill: parent StackView { id: view Layout.fillHeight: true Layout.fillWidth: true } Item { id: spacer Layout.fillHeight: true Layout.fillWidth: true } Button { text: qsTr("Create Component") property int index: 0 onClicked: { var config = factory.createConfig(index); view.replace(config); ++index; if (index > 1) index = 0; } Layout.alignment: Qt.AlignHCenter } } }
Seems, this approach does work! :)
PS: It is somehow ugly.. but...