C++/QML: How to define and handle multiple contexts for dynamically created components?

StackOverflow https://stackoverflow.com/questions/23613936

  •  20-07-2023
  •  | 
  •  

Question

Basically my situation is like this:

I've got a class that extends QQuickView and that exposes certain objects from C++ to QML by setting context properties. The views that are shown are created from QML and are all different istances of the same custom made component; new views are created when certain events occur, and when that happens the existing views should show the objects that were initially assigned to them in the C++ side, and the new ones should show the things assigned to them.

So, in the C++ side I've got something like this:

WindowManager::WindowManager(QQuickView *parent) :
QQuickView(parent)
{
      // Setting the source file to use
      this->setSource(QUrl("qrc:/qml/main.qml"));

      // Exposing the istance of this class to QML for later use
      this->rootContext()->setContextProperty("qquickView", this);

      // Calling the method that will create dynamically a new view that will be child of main.qml; the parameter is not important, just a random number to start with
      this->prepareNewView(3)

      this->showFullScreen();
}

WindowManager::prepareNewView(int stuffId)
{
      MyDatabase db;

      // Getting something to show in QML from somewhere based on the parameter received
      SomeStuff stuff = db.getStuff(stuffId)

      // Exposing the object I need to show in QML
      this->rootContext()->setContextProperty("someStuff", stuff);



      QObject *object = this->rootObject();

      // Here I'm invoking a function from main.qml that will add a new view dynamically
      QMetaObject::invokeMethod(object, "addView");
}

Now, in the QML side I've got a main file like this:

// main.qml
Rectangle {
    id: mainWindow
    width: 1000
    height: 1000

    // This function adds a component to mainWindow
    function addView()
    {
        // Creating the component from my custom made component
        var component = Qt.createComponent("MyComponent.qml");

        // Creating an istance of that component as a child of mainWindow
        var newView = component.createObject(mainWindow);


        // ... Now I would be doing something with this new view, like connecting signals to slots and such
    }
}

Then I've got my custom component, which is the view that will be created dynamically:

// MyComponent.qml
Rectangle {
    id: customComponent

    // Here I would be using the object I exposed from the C++ side
    x: someStuff.x
    y: someStuff.y
    width: someStuff.width
    height: someStuff.height

    // Here I'm creating a MouseArea so that clicking this component will cause the creation of another view, that will have to show diffrent things since the parameter I'm passing should be different from the starting parameter passed in the constructor of WindowManager
    MouseArea {
        anchors.fill: parent
        onClicked: qquickView.prepareNewView(Math.random())
    }
}

Now, with everything as it is, at first it will show "the stuff" with id 3, that was exposed as a context property of the main context.

But then, if I click on the MouseArea, assuming that an id other than 3 will be passed, a new context property with the same name will be exposed, causing the override of the old property. This means that the first view will now show "the stuff" just exposed, not "the stuff" based from the stuffId equals to 3, while what I need is the first view to keep showing what it was supposed to show ("the stuff" with id = 3), and any other view that will come later the things corresponding to their ids.

This happens because I'm defining a property in the context that is common to every component, while I should be defining a property that is visible ONLY by the new istance of the component that is being created dynamically. But how do I do that?

In the documentation I read that it's possibile to create a component directly from C++ and defining the context that it should use... something like this (snippet taken from here):

QQmlEngine engine;
QStringListModel modelData;
QQmlContext *context = new QQmlContext(engine.rootContext());
context->setContextProperty("myModel", &modelData);

QQmlComponent component(&engine);
component.setData("import QtQuick 2.0\nListView { model: myModel }", QUrl());
QObject *window = component.create(context);

I think that this would work for what I intend to do. Whenever I create a new view from C++ (caused by the click on the MouseArea) I create a new context with "someStuff" as its property, so that each view has its own "stuff"... but I need to have access to the newly created view from QML, because after I create it in the addView() function inside main.qml I access the view in order to do certain thins (not important what exactly), and if I create the istance of the component from C++ I don't know how to access it from QML... is there a way maybe to pass the component from C++ to QML in order to access it?

I'm out of ideas on how to resolve this or to find another way to have dynamically created views with custom contents all visible at the same time... any suggestions are appreciated.

Was it helpful?

Solution

I actually found out that it is possible (and easy) to directly pass a component created in C++ to QML.

So right now, I modified the code pretty much like this:

WindowManager::prepareNewView(int stuffId)
{
    MyDatabase db;

    // Getting something to show in QML from somewhere based on the parameter received
    SomeStuff stuff = db.getStuff(stuffId)


    // Creating the new context, based on the global one
    QQmlContext *context = new QQmlContext(this->rootContext());


    // Exposing the object I need to show in QML to the new context
    context ->setContextProperty("someStuff", stuff);

    // Creating the component
    QQmlComponent component(this->engine(), QUrl("qrc:/qml/MyComponent.qml"));

    // Creating the istance of the new component using the new context
    QQuickItem *newView = qobject_cast<QQuickItem*>(component.create(context));


    // Getting the root component (the Rectangle with it mainWindow)
    QObject *object = this->rootObject();

    // Manually setting the new component as a child of mainWIndow
    newView->setParentItem(qobject_cast<QQuickItem*>(object));

    // Invoking the QML that will connect the events of the new window, while passing the component created above as QVariant
    QMetaObject::invokeMethod(object, "addView", Q_ARG(QVariant, QVariant::fromValue(newView)));
 }

In QML the function in the main.qml is now like this:

// Function called from C++; the param "newView" is the last component added
function addView(newView)
{
    // ... Here I would use the new view to connect signals to slots and such as if I created "newView" directly in QML
}

So I managed not to change the code too much after all.

OTHER TIPS

I think you can pass your component instance (QObject) by setting an object as a context property as you did in your code.

class ViewInstance : public QObject
{
Q_OBJECT
    public:
    Q_INVOKABLE QObject* getCurrentViewInstance() {
        ...
        QObject *window = component.create(context);
        return window;
    }
};

int main(int argc, char *argv[]) {
    ...
    QQuickView view;
    ViewInstance data;
    view.rootContext()->setContextProperty("viewInstance", &data);
}

Then, in your qml, you can get the component instance by calling viewInstance.getCurrentViewInstance(). Hope this helps.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top