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:
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:
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:
- Start program
- Click Push
- Click Pop
The following output shows the TypeError
s 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 TypeError
s 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_PROPERTY
s 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();
}
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
You will quickly find that the interpreter yells at you on delete if the object was created, and still owned, by C++.
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:
Then, when the stack item is popped, I add the item to
garbageBin
and do a single-shot signal in 2 seconds:After a few seconds, the
deleteAfterDelay()
slot is called and "garbage collects" the item: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
TypeError
s and all objects created by C++ are properly cleaned up.