Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Tying a context to the lifetime of a qml component?



  • I have been trying to understand contexts. I found that if I add something to the root context via setContextProperty, then delete that object, and then try to access that property via qml it will crash the program. I had thought context property lifetime was tied to the object. This is not true. So I started experimenting with sub contexts (contexts based on root). I want to tie the lifetime of the context to a qml component. Most of our apps just use the global context, but I am concerned about the robustness of this approach. So I created the following program to test this:

    main.cpp:

    #include <QGuiApplication>
    #include <QQmlApplicationEngine>
    #include <QQmlContext>
    
    #include "contextcontainingclass.h"
    
    int main(int argc, char *argv[])
    {
        QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    
        QGuiApplication app(argc, argv);
    
        //ContextContainingClass* contextclass1 = new ContextContainingClass;
        NoContextContainingClass* nocontextclass1 = new NoContextContainingClass;
        ContextLauncher cl;
    
        QQmlApplicationEngine engine;
        auto context = engine.rootContext();
        cl.setContext(context);
        context->setContextProperty("contextLauncher", &cl);
    
        //contextclass1->setContext(context);
        nocontextclass1->setContext(context);
    
        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);
    
        delete nocontextclass1; // will cause crash when evaluated again in qml
    
        return app.exec();
    }
    

    main.qml:

    import QtQuick 2.12
    import QtQuick.Controls 2.12
    import QtQuick.Window 2.12
    
    Window {
        id: window
        visible: true
        width: 640
        height: 480
        title: qsTr("Hello World")
    
        property int evalvar: 1
    
        Column {
            anchors.fill: parent
    
            Text {
                width: 100
                height: 20
                //text: window.evalvar ? contextContaining.name : "nada"
                text: "dummy1"
            }
            Text {
                width: 100
                height: 20
                text: window.evalvar ? nocontextContaining.name : "nada"
                //text: "dummy2"
            }
            Button {
                text: "Eval"
                onClicked: {
                    window.evalvar += 1
                }
            }
            Button {
                text: "Launch sub window with context"
                onClicked: {
                    console.log("launching")
                    contextLauncher.launchWithContext("qrc:/SubView.qml")
                }
            }
        }
    }
    

    context creation objects
    contextcontainingclass.h:

    #ifndef CONTEXTCONTAININGCLASS_H
    #define CONTEXTCONTAININGCLASS_H
    
    #include <QObject>
    #include <QQmlContext>
    
    class ContextContainingClass : public QObject
    {
        Q_OBJECT
        Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)
    public:
        explicit ContextContainingClass(QObject *parent = nullptr);
    
        void setContext(QQmlContext* root);
        QQmlContext* getContext();
    
    signals:
    
        void nameChanged(QString name);
    
    protected:
        QString m_name = "ContextContainingClass";
        QQmlContext* m_context;
    };
    
    class NoContextContainingClass : public QObject
    {
        Q_OBJECT
        Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)
    public:
        explicit NoContextContainingClass(QObject *parent = nullptr);
    
        void setContext(QQmlContext* root);
    
    signals:
    
        void nameChanged(QString name);
    
    protected:
        QString m_name = "NoContextContainingClass";
    };
    
    class ContextLauncher : public QObject
    {
        Q_OBJECT
    public:
        explicit ContextLauncher(QObject *parent = nullptr);
    
        void setContext(QQmlContext* root);
    
    signals:
    
    public slots:
        void launchWithContext(QUrl qmlfile);
    
    protected:
        QQmlContext* m_context;
    };
    
    #endif // CONTEXTCONTAININGCLASS_H
    
    

    contextcontainingclass.cpp:

    #include "contextcontainingclass.h"
    #include <QQmlComponent>
    #include <QQmlApplicationEngine>
    #include <QQuickItem>
    //#include <QQmlEngine>
    #include <QDebug>
    
    ContextContainingClass::ContextContainingClass(QObject *parent)
        : QObject(parent)
        , m_context(nullptr)
    {
    
    }
    
    void ContextContainingClass::setContext(QQmlContext *root)
    {
        m_context = new QQmlContext(root, this); // set owner to object
        m_context->setContextProperty("contextContaining", this);
    }
    
    QQmlContext *ContextContainingClass::getContext()
    {
        return m_context;
    }
    
    NoContextContainingClass::NoContextContainingClass(QObject *parent)
        : QObject(parent)
    {
    
    }
    
    void NoContextContainingClass::setContext(QQmlContext *root)
    {
        root->setContextProperty("nocontextContaining", this);
    }
    
    ContextLauncher::ContextLauncher(QObject *parent)
        : QObject(parent)
        , m_context(nullptr)
    {
    
    }
    
    void ContextLauncher::setContext(QQmlContext *root)
    {
        m_context = root;
    }
    
    void ContextLauncher::launchWithContext(QUrl qmlfile)
    {
        if(!m_context)
            return;
    
        auto engine = dynamic_cast<QQmlApplicationEngine*>(m_context->engine());
    
        if(!engine){
            qInfo() << "no engine";
            return;
        }
        auto window = engine->rootObjects()[0];
        auto comp = new QQmlComponent(engine, qmlfile, window); // main window as parent
        //comp->loadUrl(qmlfile);
    
        // create ui element
        auto uielement = new ContextContainingClass();
        uielement->setContext(m_context);
        auto newcontext = uielement->getContext();
        qInfo() << uielement << newcontext;
    
        // create component with new context
        QQuickItem* subcomp = dynamic_cast<QQuickItem*>(comp->create(newcontext));
        qInfo() << subcomp;
        uielement->setParent(subcomp); // component is parent of context so it gets destroyed with component
    }
    

    SubView.qml:

    import QtQuick 2.0
    
    Item {
        z: 100
        anchors.centerIn: parent
        width: 200
        height: 200
    
        Rectangle {
            anchors.fill: parent
            color: "steelblue"
            Text {
                width: parent.width
                height: 20
                text: contextContaining.name
            }
        }
    }
    

    This works and creates an object that is tied to a component with its own context. It sets properties specific to the qml file. It is not showing anything though as I have not tied the component to any property on an existing qml component.

    To me this seemed kind of cumbersome (at least the way I did this). Is there a better way to do this?



  • I would strongly suggest to avoid setting context properties. If you're creating components from C++ anyway, just define them as real properties of the QML component, and use QQmlComponent::createWithInitialProperties to initialize them.

    You can use the same approach for your main entry point with QQmlApplicationEngine::setInitialProperties

    If you don't create your components manually, and want to provide access to values passed in from C++, consider using qmlRegisterSingletonType and qmlRegisterSingletonInstance.



  • @FKosmale said in Tying a context to the lifetime of a qml component?:

    If you're creating components from C++ anyway

    We currently are not. We create an object called a UI object that is subclassed from QObject. Then we set a context property in the root context for this UI object. The QML that uses it is then pushed on a stack and it uses that context property. I am unsure how to go from that design to a design that does it more cleanly. I don't see any examples for how to do this otherwise.



  • @FKosmale said in Tying a context to the lifetime of a qml component?:

    avoid setting context properties

    I guess I could create QQuickItem objects and add the relevant properties to those. I will have to think on this.


Log in to reply