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...


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.