【问题标题】:Safely deleting QML component being used in StackView transition安全删除 StackView 转换中使用的 QML 组件
【发布时间】:2017-05-09 07:47:16
【问题描述】:

概述

我的问题涉及QQmlComponent::create() 创建的QObject 的生命周期。 create() 返回的对象是 QQmlComponent 的实例化,我将它添加到 QML StackView。我在 C++ 中创建对象并将其传递给 QML 以显示在 StackView 中。问题是当我从堆栈中弹出一个项目时出现错误。我编写了一个演示应用程序来说明正在发生的事情。

免责声明:是的,我知道从 C++ 进入 QML 并不是“最佳实践”。是的,我知道你应该在 QML 中做 UI 工作。但是,在生产环境中,需要与 UI 共享大量 C++ 代码,因此 C++ 和 CML 之间需要一些互操作性。我使用的主要机制是 Q_PROPERTY 通过在 C++ 端设置上下文来绑定。

这个屏幕是演示开始时的样子:

StackView 位于中心,背景为灰色,其中包含一项(带有文本“默认视图”);此项目由 QML 实例化和管理。现在,如果您按下 Push 按钮,则 C++ 后端会从 ViewA.qml 创建一个对象并将其放置在堆栈中……下面是一个屏幕截图:

此时,我按 Pop 键从StackView 中删除“View A”(上图中红色部分)。 C++ 调用 QML 从堆栈中弹出项目,然后删除它创建的对象。问题是 QML 需要这个对象来制作过渡动画(我使用 StackView 的默认动画),当我从 C++ 中删除它时它会抱怨。所以我想我理解为什么会发生这种情况,但我不确定如何找出 QML 何时完成对象,以便我可以删除它。 如何确保 QML 使用我在 C++ 中创建的对象完成,以便我可以安全地删除它?

总结一下,这里是重现我描述的问题的步骤:

  1. 启动程序
  2. 点击推送
  3. 点击弹出

以下输出显示了在上述步骤 3 中弹出项目时发生的TypeErrors:

输出

在下面的输出中,我按“Push”一次,然后按“Pop”。请注意调用 ~ViewA() 时的两个 TypeErrors。

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_PROPERTYs。

Who owns object returned by QQmlIncubator 上查看我的相关问题。

代码示例

我已经生成了一个最低限度完整的示例来说明问题。下面列出了所有文件。具体看~ViewA()中的代码cmets。


// 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 GUI。
  • 有一个附加的StackView.removed() 信号(从 Qt 5.8 中的 Qt Quick Controls 2.1 开始),但是你会在 C++ 中使用它时遇到麻烦。这些东西旨在从 QML 中使用。
  • 我在回答您之前的问题时添加了一段关于所有权管理的内容。设置 objectQObject::setParent 是否适合您?请注意,您的示例非常详尽。我们喜欢minimal reproducible example
  • @m7913d,感谢您对此的帮助。问题是该对象没有父对象。该对象是StackView 中的“独立”项。当堆栈中的项目被删除时,谁将是负责删除它的父项?
  • @dtech,您愿意分享一个如何最好地使用 QML 从 C++ 共享数据的示例吗?我的策略是在 C++ 中对 QObjects 使用 Q_PROPERTYs 并设置上下文。问题是在创建 QML 组件之前,应该设置上下文。这意味着需要使用 C++。我很高兴看到一个说明如何更好地解决这个问题的答案。我在我的问题中添加了免责声明,以警告人们注意这种“反模式”。

标签: c++ qt qml


【解决方案1】:

我正在发布我自己问题的答案。如果您发布答案,我会考虑接受您的答案而不是这个答案。但是,这是一种可能的解决方法。

问题是用 C++ 创建的 QML 对象需要足够长的时间让 QML 引擎完成所有转换。我使用的技巧是将 QML 对象实例标记为删除,等待几秒钟让 QML 完成动画,然后删除该对象。这里的“hacky”部分是我必须猜测我应该等待多少秒,直到我认为 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 秒总是足够长之外,它在实践中似乎工作得非常好——不再有 TypeErrors 并且所有由 C++ 创建的对象都已正确清理。

【讨论】:

    【解决方案2】:

    我相信我已经找到了一种放弃@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++ 创建并仍然拥有的,您很快就会发现解释器在删除时对您大喊大叫。

    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));
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-05-29
      • 1970-01-01
      • 2023-04-10
      • 1970-01-01
      • 1970-01-01
      • 2017-11-07
      • 2018-02-12
      相关资源
      最近更新 更多