c++qtqmlqt5qqmlapplicationengine

TableView and QAbstracTableModel when calls QQmlApplicationEngine from another class


I am trying to make the model QAbstractTableModel in cpp and connect to qml.

This code works well.

MyModel.h

#ifndef MYMODEL_H
#define MYMODEL_H

#include <QAbstractTableModel>

class MyModel : public QAbstractTableModel
{
    Q_OBJECT

public:
    enum AnimalRoles {
        TypeRole = Qt::UserRole + 1,
        SizeRole
    };

    explicit MyModel(QObject *parent = nullptr);

    // Basic functionality:
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;

protected:
    QHash<int, QByteArray> roleNames() const;

private:
    QList<Animal> m_animals;
};

#endif // MYMODEL_H

MyModel.cpp

#include "MyModel.h"
#include <QDebug>

MyModel::MyModel(QObject *parent)
    : QAbstractTableModel(parent)
{
        qDebug() << __FUNCTION__;
        addAnimal(Animal("Wolf", "Medium"));
        addAnimal(Animal("Polar bear", "Large"));
        addAnimal(Animal("Quoll", "Small"));

}

int MyModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return m_animals.size();
}

int MyModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return 2;
}

QVariant MyModel::data(const QModelIndex &index, int role) const
{
    qDebug() << __FUNCTION__ << index.row() << index.column() << role;
    if (!index.isValid())
        return QVariant();
    const Animal &animal = m_animals[index.row()];
    switch (role) {
    case TypeRole:
        return animal.type();
    case SizeRole:
        return animal.size();
    default:
        break;
    }

    return QVariant();
}

void MyModel::addAnimal(const Animal &animal)
{
    qDebug() << __FUNCTION__;
    beginInsertRows(QModelIndex(), rowCount(), rowCount());
    m_animals << animal;
    endInsertRows();
}

QHash<int, QByteArray> MyModel::roleNames() const
{
    qDebug() << __FUNCTION__;
    QHash<int, QByteArray> roles;
    roles[TypeRole] = "type";
    roles[SizeRole] = "size";
    return roles;
}

main.cpp

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

        MyModel model;

    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("myModel", &model);
    engine.load(QUrl(QStringLiteral("qrc:/resources/qmls/main.qml")));
    if (engine.rootObjects().isEmpty())
        return -1;

    return app.exec();
}

main_test.qml

import QtQuick 2.0
import QtQuick 2.6
import QtQuick.Window 2.2
import QtQuick.Controls 1.4

Window {
    id: main_view
    width: 250
    height: 600
    visible: true

    ListView {
        id: list_view
        width: 200; height: 250

        model: myModel
        delegate: Text { text: "Animal Test: " + type + ", " + size }
    }


    TableView {
        id: table_view
        objectName: "tableView"
        width: 250; height: 250
        anchors.top:  list_view.bottom
        model: myModel

        TableViewColumn {
            id: type_col
            role: "type"
            title: "Type"
            width: 100
        }
        TableViewColumn {
            id: size_col
            role: "size"
            title: "Size"
            width: 100
        }
    }

}

It looks like this

enter image description here

But, if I change the main.cpp a little bit, the list view dispaly as normal, but not the table view.

main.cpp

#include "MainView.h"

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

    MainView mainView;
    return app.exec();
}

MainView.h

#ifndef MAINVIEW_H
#define MAINVIEW_H

#include <QObject>
#include <QQmlApplicationEngine>

class MainView: public QObject
{
    Q_OBJECT
public:
   explicit  MainView(QObject *parent=nullptr);
    void initializeView();

private:
    QQmlApplicationEngine m_engine;
};

#endif // MAINVIEW_H

MainView.cpp

#include "MainView.h"
#include "MyModel.h"
#include <QQmlContext>

MainView::MainView(QObject *parent)
    : QObject(parent)
{
    initializeView();
}

void MainView::initializeView()
{
    MyModel model;
    m_engine.rootContext()->setContextProperty("myModel", &model);
    m_engine.load(QUrl(QStringLiteral("qrc:/resources/qmls/main_test.qml")));

}

It looks like this.

enter image description here

I really don't understand why this happens. What the difference between ListView and TableView in the second case? And how to fix it, i.e. make data display in the second case? Thank in advance.


Solution

  • The problem is in initializeView, model is a local variable in that scope, and every local variable is deleted from the memory when the function finishes executing. In the first case model will be eliminated when the application is closed, in the second case it will be eliminated when initializeView ends that is when the window is displayed, there are 2 possible solutions:


    void MainView::initializeView()
    {
        MyModel *model = new MyModel(this);
        m_engine.rootContext()->setContextProperty("myModel", model);
        m_engine.load(QUrl(QStringLiteral("qrc:/resources/qmls/main_test.qml")));
    
    }
    

    *.h

    #ifndef MAINVIEW_H
    #define MAINVIEW_H
    
    #include <QObject>
    #include <QQmlApplicationEngine>
    
    class MainView: public QObject
    {
        Q_OBJECT
    public:
       explicit  MainView(QObject *parent=nullptr);
        void initializeView();
    
    private:
        QQmlApplicationEngine m_engine;
        MyModel model{this};
    };
    
    #endif // MAINVIEW_H
    

    *.cpp

    ...
    
    void MainView::initializeView()
    {
        m_engine.rootContext()->setContextProperty("myModel", &model);
        m_engine.load(QUrl(QStringLiteral("qrc:/resources/qmls/main_test.qml")));
    
    }
    

    To understand the behavior I have added more points of impression:

    MyModel::~MyModel()
    {
        qDebug()<<"destructor";
    }
    

    TableView {
        ...
        Component.onCompleted: {
            console.log("completed table")
            if(!timer.running)
                timer.running = true
        }
    }
    
    ListView {
        ...
        Component.onCompleted: {
            console.log("completed list")
            if(!timer.running)
                timer.running = true
        }
    }
    
    
    Timer {
        id: timer
        interval: 0;
        onTriggered:  console.log(myModel, table_view.model, list_view.model)
    }
    

    And I get the following:

    MyModel
    addAnimal
    addAnimal
    addAnimal
    roleNames
    data 0 0 257
    data 0 0 258
    data 1 0 257
    data 1 0 258
    data 2 0 257
    data 2 0 258
    roleNames
    qml: completed list
    data 0 0 257
    data 0 0 258
    qml: completed table
    destructor
    qml: null null null
    

    We note that ListView manages to load the data, whereas TableView in the middle of the load is called the destructor.

    Possible explanation: I think that the ListView stores the data making a copy of them and only updates when the model notifies, should also be notified when the model is deleted to clean the data, it seems that this is a bug. On the other hand, TableView, being at the moment of loading and deleting the model, is null, so it is notified and cleans the data.

    Doing another test:

    void MainView::initializeView()
    {
        MyModel *model = new MyModel;
        m_engine.rootContext()->setContextProperty("myModel", model);
        m_engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    
        QTimer::singleShot(1000, model, &MyModel::deleteLater);
    }
    

    It is observed that both load the data correctly, and after a second the model is destroyed, but the one that is only notified is the TableView since it is the only one that cleans the data shown, and ListView does not.

    my conclusion that it is a ListView bug.