2

Overview

My question deals with the lifetime of a QObject created by QQmlComponent::create(). The object returned by create() is the instantiation of a QQmlComponent, and I am adding it to a QML StackView. I am creating the object in C++ and passing it to QML to display in the StackView. The problem is that I am getting errors when I pop an item from the stack. I wrote a demo app to illustrate what's happening.

Disclaimer: Yes, I know that reaching into QML from C++ is not "best practice." Yes, I know that you should do UI stuff in QML. However, in the production world, there is a ton of C++ code that needs to be shared with the UI, so there needs to be some interop between C++ and CML. The primary mechanism I'm using is Q_PROPERTY bindings by setting the context on the C++ side.

This screen is what the demo looks like when it starts:

Screen at start-up

The StackView is in the center with a gray background and has one item in it (with the text 'Default View'); this item is instantiated and managed by QML. Now if you press the Push button, then the C++ back-end creates an object from ViewA.qml and places it on the stack...here is a screen shot showing this:

After 'Push' is pressed

At this point, I press Pop to remove "View A" (in red in the picture above) from the StackView. C++ calls into QML to pop the item from the stack and then deletes the object it created. The problem is that QML needs this object for the transition animation (I'm using the default animation for StackView), and it complains when I delete it from C++. So I think I understand why this is happening, but I'm not sure how to find out when QML is done with the object so I can delete it. How can I make sure QML is done with an object that I created in C++ so I can safely delete it?

Summarizing, here are the steps that reproduce the problem I am describing:

  1. Start program
  2. Click Push
  3. Click Pop

The following output shows the TypeErrors that happen when the item is popped in step 3 above:

Output

In the output below, I press "Push" once, then I press "Pop". Note the two TypeErrors when ~ViewA() is called.

root object name =  "appWindow"
[c++] pushView() called
qml: [qml] pushView called with QQuickRectangle(0xdf4c00, "my view")
[c++] popView() called
qml: [qml] popView called
[c++] deleting view
~ViewA() called
file:///opt/Qt5.8.0/5.8/gcc_64/qml/QtQuick/Controls/Private/StackViewSlideDelegate.qml:97: TypeError: Cannot read property 'width' of null
file:///opt/Qt5.8.0/5.8/gcc_64/qml/QtQuick/Controls/StackView.qml:899: TypeError: Type error    

Context must be set from C++

Clearly, what is happening is that the object (item) that the StackView is using is being deleted by C++, but QML still needs this item for the transition animation. I suppose I could create the object in QML and let the QML engine manage the lifetime, but I need to set the QQmlContext of the object to bind the QML view to Q_PROPERTYs on the C++ side.

See my related question on Who owns object returned by QQmlIncubator.

Code Example

I've generated a minimally complete example to illustrate the problem. All files are listed below. In particular, look at the code comments in ~ViewA().


// main.qml
import QtQuick 2.3
import QtQuick.Controls 1.4

Item {
    id: myItem
    objectName: "appWindow"

    signal signalPushView;
    signal signalPopView;

    visible: true
    width: 400
    height: 400

    Button {
        id: buttonPushView
        text: "Push"
        anchors.left: parent.left
        anchors.top: parent.top
        onClicked: signalPushView()
    }

    Button {
        id: buttonPopView
        text: "Pop"
        anchors.left: buttonPushView.left
        anchors.top: buttonPushView.bottom
        onClicked: signalPopView()
    }

    Rectangle {
        x: 100
        y: 50
        width: 250
        height: width
        border.width: 1

        StackView {
            id: stackView
            initialItem: view
            anchors.fill: parent

            Component {
                id: view

                Rectangle {
                    color: "#DDDDDD"

                    Text {
                        anchors.centerIn: parent
                        text: "Default View"
                    }
                }
            }
        }
    }

    function pushView(item) {
        console.log("[qml] pushView called with " + item)
        stackView.push(item)
    }

    function popView() {
        console.log("[qml] popView called")
        stackView.pop()
    }
}

// ViewA.qml
import QtQuick 2.0

Rectangle {
    id: myView
    objectName: "my view"

    color: "#FF4a4a"

    Text {
        text: "View A"
        anchors.centerIn: parent
    }
}

// viewa.h
    #include <QObject>

class QQmlContext;
class QQmlEngine;
class QObject;

class ViewA : public QObject
{
    Q_OBJECT
public:
    explicit ViewA(QQmlEngine* engine, QQmlContext* context, QObject *parent = 0);
    virtual ~ViewA();

    // imagine that this view has property bindings used by 'context'
    // Q_PROPERTY(type name READ name WRITE setName NOTIFY nameChanged)

    QQmlContext* context = nullptr;
    QObject* object = nullptr;
};

// viewa.cpp
#include "viewa.h"
#include <QQmlEngine>
#include <QQmlContext>
#include <QQmlComponent>
#include <QDebug>

ViewA::ViewA(QQmlEngine* engine, QQmlContext *context, QObject *parent) :
    QObject(parent),
    context(context)
{
    // make property bindings visible to created component
    this->context->setContextProperty("ViewAContext", this);

    QQmlComponent component(engine, QUrl(QLatin1String("qrc:/ViewA.qml")));
    object = component.create(context);
}

ViewA::~ViewA()
{
    qDebug() << "~ViewA() called";
    // Deleting 'object' in this destructor causes errors
    // because it is an instance of a QML component that is
    // being used in a transition. Deleting it here causes a
    // TypeError in both StackViewSlideDelegate.qml and
    // StackView.qml. If 'object' is not deleted here, then
    // no TypeError happens, but then 'object' is leaked.
    // How should 'object' be safely deleted?

    delete object;  // <--- this line causes errors

    delete context;
}   

// viewmanager.h
#include <QObject>

class ViewA;
class QQuickItem;
class QQmlEngine;

class ViewManager : public QObject
{
    Q_OBJECT
public:
    explicit ViewManager(QQmlEngine* engine, QObject* topLevelView, QObject *parent = 0);

    QList<ViewA*> listOfViews;
    QQmlEngine* engine;
    QObject* topLevelView;

public slots:
    void pushView();
    void popView();
};

// viewmanager.cpp
#include "viewmanager.h"
#include "viewa.h"
#include <QQmlEngine>
#include <QQmlContext>
#include <QDebug>
#include <QMetaMethod>

ViewManager::ViewManager(QQmlEngine* engine, QObject* topLevelView, QObject *parent) :
    QObject(parent),
    engine(engine),
    topLevelView(topLevelView)
{
    QObject::connect(topLevelView, SIGNAL(signalPushView()), this, SLOT(pushView()));
    QObject::connect(topLevelView, SIGNAL(signalPopView()), this, SLOT(popView()));
}

void ViewManager::pushView()
{
    qDebug() << "[c++] pushView() called";

    // create child context
    QQmlContext* context = new QQmlContext(engine->rootContext());

    auto view = new ViewA(engine, context);
    listOfViews.append(view);

    QMetaObject::invokeMethod(topLevelView, "pushView",
        Q_ARG(QVariant, QVariant::fromValue(view->object)));
}

void ViewManager::popView()
{
    qDebug() << "[c++] popView() called";

    if (listOfViews.count() <= 0) {
        qDebug() << "[c++] popView(): no views are on the stack.";
        return;
    }

    QMetaObject::invokeMethod(topLevelView, "popView");

    qDebug() << "[c++] deleting view";
    auto view = listOfViews.takeLast();
    delete view;
}

// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickView>
#include <QQuickItem>
#include "viewmanager.h"
#include <QDebug>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQuickView view;
    view.setSource(QUrl(QLatin1String("qrc:/main.qml")));

    QObject* item = view.rootObject();
    qDebug() << "root object name = " << item->objectName();
    ViewManager viewManager(view.engine(), item);

    view.show();
    return app.exec();
}
Community
  • 1
  • 1
Matthew Kraus
  • 6,660
  • 5
  • 24
  • 31
  • 1
    What you doing is a big anti pattern. Don't do those things in C++. Do QML GUI in QML. – dtech May 09 '17 at 08:34
  • 1
    There an attached [StackView.removed()](https://doc.qt.io/qt-5/qml-qtquick-controls2-stackview.html#removed-signal) signal (since Qt Quick Controls 2.1 in Qt 5.8), but you're going to have troubles using it from C++. These things are designed to be used from QML. – jpnurmi May 09 '17 at 09:18
  • I added a paragraph about _ownership management_ in my answer on your previous question. Does setting the _object_ parent `QObject::setParent` work for you? Note that your example is quite elaborate. We like a [mcve]. – m7913d May 09 '17 at 09:42
  • @m7913d, thanks for your help on this. The problem is that the object does not have a parent. The object is an item that is a "stand-alone" item in a `StackView`. When an item in the stack is removed, who would be the parent responsible for deleting it? – Matthew Kraus May 09 '17 at 17:05
  • @dtech, do you care to share an example of how to best share data from C++ with QML? My strategy was to use `Q_PROPERTY`s on `QObject`s in C++ and set the context. The issue is that before creating a QML component, the context should be set up. This implies that C++ needs to be used. I'd be happy to see an answer that illustrates how to better solve this problem. I've added a disclaimer to my question to warn people about this "anti-pattern." – Matthew Kraus May 09 '17 at 17:24
  • The `StackView` may stay the _object_ parent (and keep the ownership) even when it is not (anymore) its _visual_ parent. See the [Qt doc](http://doc.qt.io/qt-5/qtquick-visualcanvas-visualparent.html) for the different between both. – m7913d May 09 '17 at 17:30
  • I am actually completely puzzled why you set `this` as a context property of the same object. If you can reference the object from QML, then that's all you need. All that is required for bindings to work is a `QObject` with a property with notification, no context properties are needed. Don't do QML GUI in C++, only core logic, then bind the QML GUI to it, it works with context properties, it works with object properties, it works with plain pointers. You are needlessly complicating a trivial matter. – dtech May 09 '17 at 17:41
  • Also, you may look at this answer for a solution to the "pulling the rug object under the view" problem http://stackoverflow.com/a/41474519/991484 That's a design limitation of Qt that even without waiting on animations, the QML GUI element will "linger" for another event loop cycle after its data object has been destroyed, so you can provide a dummy value so the view can die without compaining. – dtech May 09 '17 at 17:46
  • @dtech, in ViewA()'s constructor, I pass in a child context that should be used when instantiating `ViewA.qml`. I need to call `setContextProperty` and assign it to some `QObject`, which in this simple example is the C++ class `ViewA`, which derives from `QObject`. Setting the context property like this means that the properties exposed by `ViewA` are only available to the `ViewA.qml` instance that is created, and when ViewA is deleted, the associated child context is removed, also. How else would you tell QML about a property without calling `setContextProperty()` or `setContextObject()`? – Matthew Kraus May 09 '17 at 17:53
  • @dtech, I am following Qt documentation here: http://doc.qt.io/qt-5/qqmlcontext.html. Note the first few examples. Regarding your comment, "bind the QML GUI to [C++ core logic]"--what is your strategy for this if you're not using `setContextProperty` or `setContextObject`? How do you bind the QML GUI to C++ logic (and importantly, **bind to C++ data**)? – Matthew Kraus May 09 '17 at 18:04
  • I tend to use a singleton, that's most efficient since it doesn't involve lookups. I'd have an `import Core 1.0` and then simply use `Core.stuff()`. Also, models are a great way to expose "list data" to QML, and they provide reference to the data too. – dtech May 09 '17 at 18:29
  • @dtech, I am going to use C++ models to expose list data to QML, and that's actually the key purpose behind what I am doing. In fact, I'm following the docs for this [Using C++ Models with Qt Quick Views](http://doc.qt.io/qt-5/qtquick-modelviewsdata-cppmodels.html). Note that the example given calls `setContextProperty("myModel", QVariant::fromValue(dataList))`, which is basically the approach I am taking. I still don't understand how you are doing data binding without setting context properties (or context object). Every example I've seen sets context. – Matthew Kraus May 09 '17 at 20:23
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/143816/discussion-between-matthew-kraus-and-dtech). – Matthew Kraus May 09 '17 at 20:27
  • You mistake what is basically a quick and dirty (and possibility inefficient) approach to expose a single object to QML in a trivial example for recommended practice. None of those trivial examples comes even close to modeling the actual design of an actual application. Also where did you see examples where objects expose themselves as context properties in their own contexts? That literally makes no sense. If you have access to the object's context, then you have access to the object, thus you don't need to expose the object as a context property in its own context. – dtech May 09 '17 at 20:27

3 Answers3

1

I'm posting an answer to my own question. If you post an answer, I'll consider accepting your answer instead of this one. But, this is a possible work-around.

The problem is that a QML object that is created in C++ needs to live long enough for the QML engine to complete all transitions. The trick I'm using is to mark the QML object instance for deletion, wait a few seconds for QML to finish the animation, and then delete the object. The "hacky" part here is that I have to guess how many seconds I should wait until I think that QML is completely finished with the object.

First, I make a list of objects that are scheduled to be destroyed. I also make a slot that will be called after a delay to actually delete the object:

class ViewManager : public QObject {
public:
    ...
    QList<ViewA*> garbageBin;
public slots:
    void deleteAfterDelay();
}

Then, when the stack item is popped, I add the item to garbageBin and do a single-shot signal in 2 seconds:

void ViewManager::popView()
{
    if (listOfViews.count() <= 0) {
        qDebug() << "[c++] popView(): no views are on the stack.";
        return;
    }

    QMetaObject::invokeMethod(topLevelView, "popView");

    // schedule the object for deletion in a few seconds
    garbageBin.append(listOfViews.takeLast());
    QTimer::singleShot(2000, this, SLOT(deleteAfterDelay()));
}

After a few seconds, the deleteAfterDelay() slot is called and "garbage collects" the item:

void ViewManager::deleteAfterDelay()
{
    if (garbageBin.count() > 0) {
        auto view = garbageBin.takeFirst();
        qDebug() << "[c++] delayed delete activated for " << view->objectName();
        delete view;
    }
}

Aside from not being 100% confident that waiting 2 seconds will always be long enough, it seems to work extremely well in practice--no more TypeErrors and all objects created by C++ are properly cleaned up.

Matthew Kraus
  • 6,660
  • 5
  • 24
  • 31
1

I believe I have identified a way to ditch the garbage list that @Matthew Kraus recommended. I let QML handle destroying the view while popping out of the StackView.

warning: Snippets are incomplete and only meant to illustrate extension to OP's post

function pushView(item, id) {
    // Attach option to automate the destruction on pop (called by C++)
    rootStackView.push(item, {}, {"destroyOnPop": true})
}

function popView(id) {
    // Pop immediately (removes transition effects) and verify that the view
    // was deleted (null). Else, delete immediately.
    var old = rootStackView.pop({"item": null, "immediate": true})
    if (old !== null) {
        old.destroy() // Requires C++ assigns QML ownership
    }

    // Tracking views in m_activeList by id. Notify C++ ViewManager that QML has
    // done his job
    viewManager.onViewClosed(id)
}

You will quickly find that the interpreter yells at you on delete if the object was created, and still owned, by C++.

m_pEngine->setObjectOwnership(view, QQmlEngine::JavaScriptOwnership);
QVariant arg = QVariant::fromValue(view);

bool ret = QMetaObject::invokeMethod(
            m_pRootPageObj,
            "pushView",
            Q_ARG(QVariant, arg),
            Q_ARG(QVariant, m_idCnt));
Alex Hendren
  • 446
  • 1
  • 5
  • 18
0

Being late to the party, but fighting with the same problem, it seems that the solution is to keep the bound variables alive until the component receives this event:

StackView.onRemoved: { call your destruction sequence here; }

It seems that this delays destruction just so that QML can finish all thats needes and the bindings are no longer active.

flohack
  • 457
  • 1
  • 4
  • 18