3
votes

I have a variable number of components, so i'm trying to give each one its own model. In this example, i just create one, but the idea is the same.

GC() is a bit random, so in the example, i force the gc() after a click to flush out the problem. What happens is that the model is destroyed and becomes null. after that the click method cannot use it.

main.qml:

import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.2

import com.example.qml 1.0

ApplicationWindow
{
    visible: true
    width: 640
    height: 480

    // builder of dynamic models
    ModelFactory { id: maker }

    Column
    {
        anchors.fill: parent
        Repeater
        {
            // create dynamic model
            model: maker.makeModel();
            delegate: Label
            {
                id: label
                text: model.name

                MouseArea
                {
                    anchors.fill: parent
                    onClicked:
                    {
                        // works once until gc()
                        console.log("clicked on " + model.name)

                        // wont work anymore. model is destroyed
                        gc();
                    }
                }
            }

        }
    }
}

c++/mymodel.h:

#include <QAbstractListModel>
#include <QQmlApplicationEngine>
#include <QObject>
#include <QString>
#include <QDebug>

class BoxModel : public QAbstractListModel
{
    Q_OBJECT

public:

    ~BoxModel()
    {
        // see that it does get destroyed
        qDebug() << "~BoxModel()";
    }

    int rowCount(const QModelIndex& parent = QModelIndex()) const override
    {
        return 5;
    }  

    QVariant data(const QModelIndex &index, int role) const override
    {
        int ix = index.row();
        if (ix < 1) return "Larry";
        if (ix < 2) return "Barry";
        if (ix < 3) return "Gary";
        if (ix < 4) return "Harry";
        return "Sally";
    }

    QHash<int, QByteArray> roleNames() const override
    {
        QHash<int, QByteArray> roles;
        roles[Qt::UserRole+1] = "name";
        return roles;
    }

};

class ModelFactory: public QObject
{
    Q_OBJECT

public:

    Q_INVOKABLE BoxModel* makeModel()
    {
        return new BoxModel();
    }    
};

main.cpp just registers the types:

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <qqmlcontext.h>
#include <qqml.h>
#include <QtQuick/qquickitem.h>
#include <QtQuick/qquickview.h>
#include "mymodel.h"

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

    qmlRegisterType<BoxModel>("com.example.qml", 1, 0, "BoxModel");
    qmlRegisterType<ModelFactory>("com.example.qml", 1, 0, "ModelFactory");

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    return app.exec();
}

what you see:

runtime

Click on any of the names. it will work once and after that they will be undefined because model becomes null.

eg

qml: clicked on Sally
~BoxModel()
qml: clicked on undefined

My question is why is this, when i still have a reference to it?

In the example, onClicked could be changed to label.text rather than model.name to fix, but the real problem is that, in general, the model is accessed by the object at any time, for any data. For example, when the box needs to redraw. randomly the data is gone, depending on GC.

I've tried making c++ manage the life of the dynamic model. this could work if i know when exactly QML has finished with it.

thanks for info and ideas.

running on windows 8.1/qt5.6mingw

EDIT1: files as a gist, https://gist.github.com/anonymous/86118b67ec804e6149423c14792f312d

3
This looks like a genuine bug, thank you for providing a good, clean test case. Do you still have the problem if you remove the factory, and create the instance directly in QML: model: new BoxModel()?Kuba hasn't forgotten Monica
interesting. if i change model: maker.makeModel() to model: BoxModel {} it works. i'm going to see if this fixes my problem when i have multiple such boxes. thanks.jkj yuio
Yes this way can work, providing some changes can be made to the underlying code mode. it requires the BoxModel to be able to exist on its own without any factory to "know" about it. The problem i had is that i needed to track these internally. had to be done with a singleton, but it can work this way. however, it would be good for the dynamic version to work too. thanks.jkj yuio
What's stopping you from adding the instance to a global list in a constructor, and removing it in the destructor? You don't need a factory for that.Kuba hasn't forgotten Monica

3 Answers

0
votes

As Kuba said, this does indeed seem like a bug. However, you can take another approach and take ownership of the models yourself via QQmlEngine::setObjectOwnership(). Specifically, changing

Q_INVOKABLE BoxModel* makeModel()
{
    return new BoxModel();
}

to

Q_INVOKABLE BoxModel* makeModel()
{
    BoxModel *model = new BoxModel(this);
    QQmlEngine::setObjectOwnership(model, QQmlEngine::CppOwnership);
    return model;
}

will fix this (remember to parent the returned model to BoxModel so it gets deleted appropriately). The reason for the behaviour is explained here:

Generally an application doesn't need to set an object's ownership explicitly. QML uses a heuristic to set the default ownership. By default, an object that is created by QML has JavaScriptOwnership. The exception to this are the root objects created by calling QQmlComponent::create() or QQmlComponent::beginCreate(), which have CppOwnership by default. The ownership of these root-level objects is considered to have been transferred to the C++ caller.

Objects not-created by QML have CppOwnership by default. The exception to this are objects returned from C++ method calls; their ownership will be set to JavaScriptOwnership. This applies only to explicit invocations of Q_INVOKABLE methods or slots, but not to property getter invocations.

0
votes

I just had the same problem with a ComboBox.

As a workaround, you may create your own property to keep a strong reference to it:

Repeater {
    property QtObject myModel: maker.makeModel();
    model: myModel
    // …
}
0
votes

I know this is an old question, but I've just faced similar issue, and found your question in process of writing mine. See QObject gets destroyed after being put into QML variable for full story, and I'll cite it here.

What I've figured out that if I set the parent of that QObject before I pass it into QML, then it doesn't get deleted. So, I've concluded that passing unparented QObject into QML scope makes that scope become a parent of QObject and call its destructor after scope ends.