Search code examples
c++qtqmlqabstractlistmodel

QT 5.11.3 - C++ - QAbstractListModel - TableView doesn't refresh itself automatically


I've been struggling for five days straight to make real time updating of a TableView through a model inheriting QAbstractListModel, with no success.

I created a QAbstractListModel QDocumentList which is supposed to be a list of a QDocuments (A QDocument just has a name and an ID). I've then created a class DocManager which contains my QDocumentList and a thread that generates a new document every second.

On the QML side of things, I've just made a TableView using my QDocumentList as a model, and added a small feature to "manually" add a new document to the list on click with a mousearea.

Here's my problem : my TableView doesn't update when a new QDocument is created from the C++ backend (a thread). They only appear when I click on my TableView thanks to the mousearea.

What am I doing wrong ?

Here's what my code looks like :

Main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "DocManager.h"

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

    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.rootContext()->setContextProperty("docManager", &dm);
    engine.load(url);


    return app.exec();
}

QDocument.h

#pragma once
#ifndef QDOCUMENT_H
#define QDOCUMENT_H

#include <QObject>
#include <QLibrary>
#include <QString>
#include <qvariant.h>
#include <QVariantList>
#include <QAbstractListModel>
#include <QList>

class QDocument : public QObject
{
    Q_OBJECT
//  QML_ELEMENT

    private:

    public:
        QString m_docName;
        int m_docId;

        QDocument();
        QDocument(QString docName, int docId);
        QDocument(QDocument* doc);

        Q_PROPERTY(QString docName READ getDocName WRITE setDocName NOTIFY docNameChanged)
        QString getDocName();
        void setDocName(QString name);

        Q_PROPERTY(int docId READ getDocId WRITE setDocId NOTIFY docIdChanged)
        int getDocId();
        void setDocId(int id);

    signals:
        void docNameChanged();
        void docIdChanged();
};

Q_DECLARE_METATYPE(QDocument *)

class QDocumentList : public QAbstractListModel
{
    Q_OBJECT

    public:

        enum Roles
        {
            doc_name = Qt::UserRole + 1,
            doc_state
        };

        explicit QDocumentList(QDocument *parent = nullptr) : QAbstractListModel(parent)
        {

        }
        ~QDocumentList();

        //Q_INVOKABLE void addDocument(QDocument doc)
        Q_INVOKABLE int rowCount(const QModelIndex& parent = QModelIndex()) const;
        QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
        QHash<int, QByteArray> roleNames() const override;


    public slots:
        QDocument* getQDocument(int index);
        Q_INVOKABLE void addDocument(QString docName, int docId);

    private:
        QList<QDocument*> m_docs;
        int m_docCount;

};


#endif

QDocument.cpp

#include "QDocument.h"

QDocument::QDocument()
{

}

QDocument::QDocument( QString docName, int docId)
{
    m_docName = docName;
    m_docId = docId;
}

QDocument::QDocument(QDocument * doc)
{
    m_docId = doc->getDocId();
    m_docName = doc->getDocName();
    m_docId = doc->getDocId();
}

QString QDocument::getDocName()
{
    return m_docName;
}

void QDocument::setDocName(QString name)
{
    if (m_docName != name)
    {
        m_docName = name;
        emit docNameChanged();
    }
}

int QDocument::getDocId()
{
    return m_docId;
}

void QDocument::setDocId(int id)
{
    if (m_docId != id)
    {
        m_docId = id;
        emit docIdChanged();
    }
}

QDocumentList::~QDocumentList()
{
    qDeleteAll(m_docs);
}

int QDocumentList::rowCount(const QModelIndex & parent) const
{
    return m_docs.count();
}


QVariant QDocumentList::data(const QModelIndex & index, int role) const
{
    switch (role)
    {
        case doc_name:
            return QVariant::fromValue<QString>(m_docs.at(index.row())->getDocName());
            break;

        case doc_state:
            return QVariant::fromValue<int>(m_docs.at(index.row())->getDocId());
            break;

        default:
            return QVariant();
            break;
    }
}

QDocument* QDocumentList::getQDocument(int index)
{
    if (index < 0 || index >= m_docs.size())
        return nullptr;

    return m_docs[index];
}


QHash<int, QByteArray> QDocumentList::roleNames() const
{
    QHash<int, QByteArray> roleNames;
    roleNames[doc_name] = "doc_name";
    roleNames[doc_state] = "doc_state";
    return roleNames;
}

Q_INVOKABLE void QDocumentList::addDocument(QString docName, int docId)
{
    this->beginInsertRows(this->index(m_docs.count()), m_docs.count(), m_docs.count());
    m_docs.append(new QDocument(docName, docId));
    this->endInsertRows();
}

DocManager.h

#ifndef DOCMANAGER_H
#define DOCMANAGER_H

#include <QObject>
#include <QLibrary>
#include <QString>
#include <QVariantList>
#include <thread>
#include <condition_variable>
#include <mutex>
#include <iostream>
#include "QDocument.h"

class DocManager : public QObject
{
    Q_OBJECT
    private:
        QDocumentList *docs;

    public:
        Q_PROPERTY(QDocumentList* docList READ getDocList NOTIFY docListChanged)

        DocManager();

        //*** Afficheur thread ***
        std::thread thread_doc;

        //Message thread functions
        void func_doc(); //TODO : logs
        QDocumentList* getDocList();

    signals:
        void docListChanged();

};

#endif // DOCMANAGER_H

DocManager.cpp

#include "DocManager.h"

DocManager::DocManager()
{
    docs = new QDocumentList();
    thread_doc = std::thread([this] {func_doc();});
}

QDocumentList* DocManager::getDocList()
{
    return docs;
}

void DocManager::func_doc()
{
    int i=0;
    while (i<100)
    {
        std::cout << "Creating doc " << i << std::endl;
        docs->addDocument("dummy_", i);
        _sleep(1000);
        i++;
    }

}

main.qml

import QtQuick 2.6
import QtQuick.Window 2.2
import QtQuick.Layouts 1.3
import QtQuick.Dialogs 1.2
import QtLocation 5.3
import QtQuick.Controls 2.1

import QtQuick.Controls 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQml.Models 2.1
import QtQml 2.0

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")

    Rectangle
    {
        anchors.fill: parent

        TableView
        {
            property int updateCounter: 0
            id: docGrid
            model: docManager.docList
            anchors.fill: parent
            anchors.centerIn: parent
            focus: true

            TableViewColumn
            {
                role: "doc_name"
                title: "Document name"
                width: 600
            }

            TableViewColumn
            {
                role: "doc_state"
                title: "Document status"
                width: 300
            }

            style: TableViewStyle {
                minimumHandleLength : 12
                handle: Rectangle {
                    implicitHeight: 13
                    implicitWidth: 13
                    color: "grey"
                }
            }

            rowDelegate: Rectangle
            {
                height: parent.height
                SystemPalette
                {
                    id: myPalette;
                    colorGroup: SystemPalette.Active
                }
                color:
                {
                    var baseColor = styleData.alternate ? myPalette.alternateBase:myPalette.base
                    return styleData.selected ? myPalette.highlight:baseColor
                }

            }

            MouseArea
            {
                anchors.fill: parent
                onClicked: docManager.docList.addDocument("click_dummy", docManager.docList.rowCount());
            }
        }
    }
}


Solution

  • The problem is that the QObjects are not thread-safe and even less those that interact with the GUI, so you should not update the GUI from another thread, instead you must send information from the secondary thread to the main one and in the main thread it should be update the GUI. Qt offers several options for this, such as signals, QMetaObject::invokedMethod(), and QEvents. In this case I will use the second option to create the addDocument method in DocManager:

    #ifndef DOCMANAGER_H
    #define DOCMANAGER_H
    
    #include "QDocument.h"
    
    #include <QObject>
    #include <thread>
    
    class DocManager : public QObject
    {
        Q_OBJECT
    private:
        QDocumentList *docs;
    
    public:
        Q_PROPERTY(QDocumentList* docList READ getDocList NOTIFY docListChanged)
    
        DocManager();
    
        //*** Afficheur thread ***
        std::thread thread_doc;
    
        //Message thread functions
        void func_doc(); //TODO : logs
        QDocumentList* getDocList();
    private slots:
        void addDocument(const QString & docName, int docId);
    signals:
        void docListChanged();
    
    };
    
    #endif // DOCMANAGER_H
    
    #include "DocManager.h"
    
    #include <iostream>
    
    DocManager::DocManager()
    {
        docs = new QDocumentList();
        thread_doc = std::thread([this] {func_doc();});
    }
    
    QDocumentList* DocManager::getDocList()
    {
        return docs;
    }
    
    void DocManager::addDocument(const QString &docName, int docId)
    {
        docs->addDocument(docName, docId);
    }
    
    void DocManager::func_doc()
    {
        int i=0;
        while (i<100)
        {
            std::cout << "Creating doc " << i << std::endl;
            QMetaObject::invokeMethod(this, "addDocument",
                                      Qt::QueuedConnection,
                                      Q_ARG(QString, "dummy_"),
                                      Q_ARG(int, i));
            std::this_thread::sleep_for(std::chrono::seconds(1));
            i++;
        }
    }