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());
}
}
}
}
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++;
}
}