安全删除 StackView 转换中使用的 QML 组件
概述
我的问题涉及由
QQmlComponent::create()
创建的
QObject
的生命周期。由
create()
返回的对象是
QQmlComponent
的实例,我将它添加到 QML
StackView
。我正在使用 C++ 创建对象并将其传递给 QML 以显示在
StackView
中。问题是我从堆栈中弹出一个项目时出现错误。我编写了一个演示应用程序来说明发生了什么。
免责声明:是的,我知道从 C++ 进入 QML 不是“最佳实践”。是的,我知道您应该在 QML 中执行 UI 操作。然而,在生产环境中,有大量的 C++ 代码需要与 UI 共享,因此 C++ 和 CML 之间需要进行一些互操作。我使用的主要机制是通过在 C++ 端设置上下文来实现
Q_PROPERTY
绑定。
此屏幕是演示启动时的样子:
StackView 位于中心,背景为灰色,其中有一个项目(带有文本“默认视图”);该项目由 QML 实例化和管理。现在,如果您按下
Push
按钮,C++ 后端将从
ViewA.qml
创建一个对象并将其放置在堆栈上...下面是显示此情况的屏幕截图:
此时,我按下
Pop
以从
StackView
中删除“View A”(上图中的红色部分)。C++ 调用 QML 从堆栈中弹出该项目,然后删除它创建的对象。问题是 QML 需要此对象用于过渡动画(我使用的是
StackView
的默认动画),当我从 C++ 中删除它时,它会发出抱怨。所以我想我明白为什么会发生这种情况,但我不确定如何找出 QML 何时完成对象处理以便我可以删除它。
如何确保 QML 已完成我在 C++ 中创建的对象处理以便我可以安全地删除它?
总结一下,以下是重现我所描述的问题的步骤:
- 启动程序
- 单击 Push
- 单击 Pop
以下输出显示了在上述步骤 3 中弹出项目时发生的
TypeError
:
输出
在下面的输出中,我按了一次“Push”,然后按了“Pop”。请注意,在调用
~ViewA()
时会出现两个
TypeError
。
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
必须从 C++ 设置上下文
显然,发生的事情是
StackView
正在使用的对象(项)被 C++ 删除,但 QML 仍然需要此项用于过渡动画。我想我可以在 QML 中创建对象并让 QML 引擎管理生命周期,但我需要设置对象的
QQmlContext
以将 QML 视图绑定到 C++ 端的
Q_PROPERTY
。
请参阅我关于 谁拥有 QQmlIncubator 返回的对象 的相关问题。
代码示例
我生成了一个最不完整的示例来说明该问题。所有文件都列在下面。特别是,请查看
~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();
}
我正在发布我自己问题的答案。如果您发布了答案,我会考虑接受您的答案而不是这个答案。但是,这是一种可能的解决方法。
问题是,用 C++ 创建的 QML 对象需要存活足够长的时间,以便 QML 引擎完成所有转换。我使用的技巧是将 QML 对象实例标记为删除,等待几秒钟让 QML 完成动画,然后删除该对象。这里的“黑客”部分是我必须猜测我应该等待多少秒,直到我认为 QML 完全完成了该对象。
首先,我列出计划销毁的对象。我还创建了一个插槽,在延迟一段时间后调用该插槽来实际删除对象:
class ViewManager : public QObject {
public:
...
QList<ViewA*> garbageBin;
public slots:
void deleteAfterDelay();
}
然后,当堆栈项弹出时,我将该项目添加到
garbageBin
并在 2 秒内发出一次信号:
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()));
}
几秒钟后,将调用
deleteAfterDelay()
插槽并“垃圾收集”该项目:
void ViewManager::deleteAfterDelay()
{
if (garbageBin.count() > 0) {
auto view = garbageBin.takeFirst();
qDebug() << "[c++] delayed delete activated for " << view->objectName();
delete view;
}
}
除了不能 100% 确信等待 2 秒是否足够长之外,它在实践中似乎运行得非常好 - 不再出现
TypeError
,并且 C++ 创建的所有对象都得到了正确清理。
我相信我已经找到了一种抛弃@Matthew Kraus 推荐的垃圾列表的方法。我让 QML 处理在弹出 StackView 时销毁视图。
警告:代码片段不完整,仅用于说明对 OP 帖子的扩展
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)
}
如果对象是由 C++ 创建且仍由 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));
虽然迟到了,但是却在与同样的问题作斗争,看来解决方案是保持绑定变量处于活动状态,直到组件收到此事件:
StackView.onRemoved: { call your destruction sequence here; }
看来,这会延迟销毁,以便 QML 可以完成所有需要的操作,并且绑定不再处于活动状态。