安全删除StackView转换中使用的QML组件

时间:2017-05-09 07:47:16

标签: c++ qt qml

概述

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

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

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

Screen at start-up

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

After 'Push' is pressed

此时,我按弹出StackView删除“查看A”(上图中的红色)。 C ++调用QML从堆栈中弹出项目,然后删除它创建的对象。问题是QML需要这个对象用于过渡动画(我正在使用StackView的默认动画),当我从C ++中删除它时它会抱怨。所以我想我明白为什么会这样,但我不知道如何找出QML完成对象的时间,以便我可以删除它。 如何确保使用我在C ++中创建的对象完成QML,以便我可以安全地删除它?

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

  1. 启动程序
  2. 点击推送
  3. 点击弹出
  4. 以下输出显示了在上面的步骤3中弹出项目时发生的TypeError

    输出

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

    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。 / p>

    Who owns object returned by 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();
    }
    

2 个答案:

答案 0 :(得分:1)

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

问题在于,用C ++创建的QML对象需要足够长的时间才能使QML引擎完成所有转换。我使用的技巧是将QML对象实例标记为删除,等待几秒钟让QML完成动画,然后删除该对象。 &#34; hacky&#34;这里的一部分是我必须猜测我应该等待多少秒,直到我认为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()个插槽,&#34;垃圾收集&#34;该项目:

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

除了没有100%确信等待2秒总是足够长的时候,它似乎在实践中工作得非常好 - 不再有TypeError s并且C ++创建的所有对象都被正确清理。< / p>

答案 1 :(得分:1)

我相信我已经找到了一种摆脱@Matthew Kraus建议的垃圾清单的方法。当弹出StackView时,我让QML处理破坏视图。

警告:代码段不完整,仅用于说明对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));