Search code examples
c++qtqmlqthreadqabstractlistmodel

Qml ListView from C++ QAbstractListModel with QThread processing


I found a QML ListView sample using a C++ QAbstractListModel. However, it took a while to fetch the list model's data and waiting popup was freezing. So, I tried to use QThread samples (a, b, c) in the cpu intensive task sample.

Then, I got the below errors, when a different thread (ThreadHandler::process()) tried to fetch the model data in the main thread (PersonModel::requestPersonData()).

QObject::connect: Cannot queue arguments of type 'QQmlChangeSet' (Make sure 'QQmlChangeSet' is registered using qRegisterMetaType().)

My question is how to add data into the QAbstractListModel from the different QThread's function. Or is there any way to handle the time consuming list model due to the large data?

Here is the reproducible code. (Qt 5.12.10 MSVC2015 64bit, Qt Creator 4.14.2. windows 10)

Thanks in advance.

personmodel.h

#ifndef PERSONMODEL_H
#define PERSONMODEL_H

#include <QAbstractListModel>

struct Person
{
    QString First;
    QString Last;
};

class PersonModel : public QAbstractListModel
{
    Q_OBJECT
    Q_PROPERTY(int count READ count NOTIFY countChanged)

public:
    explicit PersonModel(QObject *parent = nullptr);
    virtual ~PersonModel();

    enum PersonRoles {
        FirstRole = Qt::UserRole + 1,
        LastRole
    };
    Q_ENUM(PersonRoles)

    void addPerson(Person *p);
    Person* getPerson(int index);
    void clear();
    int count() const;

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

    virtual QHash<int, QByteArray> roleNames() const override;

public slots:
    void requestPersonData(void);
    void requestDeleteAll();
    bool remove(const int inIndex);

signals:
    void countChanged();

private:
    QHash<int, QByteArray> _roles;
    QList<Person *> _people;
};

#endif // PERSONMODEL_H

personmodel.cpp

#include "personmodel.h"
#include "threadhandler.h"
#include <QThread>
#include <QWaitCondition>
#include <QDebug>
#include <time.h>

PersonModel::PersonModel(QObject *parent)
    : QAbstractListModel(parent)
{
    _roles[FirstRole] = "first";
    _roles[LastRole]  = "last";
}

PersonModel::~PersonModel()
{
    _people.clear();
}

int PersonModel::count() const
{
    return rowCount();
}

int PersonModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;

    return _people.count();
}

QVariant PersonModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    if (index.row() >= _people.count())
        return QVariant();

    Person *p = _people.at(index.row());

    switch (role) {
    case FirstRole:
        return QVariant::fromValue(p->First);
    case LastRole:
        return QVariant::fromValue(p->Last);
    default:
        break;
    }

    return QVariant();
}

QHash<int, QByteArray> PersonModel::roleNames() const
{
    return _roles;
}

void PersonModel::clear()
{
    qDeleteAll(_people);
    _people.clear();
}

Person *PersonModel::getPerson(int index)
{
    return _people.at(index);
}

void PersonModel::addPerson(Person *p)
{
    int row = _people.count();

    beginInsertRows(QModelIndex(), row, row);
    _people.append(p);
    endInsertRows();
}

bool PersonModel::remove(const int inIndex)
{
    if ((rowCount() <= 0) || (inIndex < 0) || (rowCount() <= inIndex))
        return false;

    beginRemoveRows(QModelIndex(), inIndex, inIndex);
    _people.removeAt(inIndex);
    endRemoveRows();

    return true;
}

void PersonModel::requestPersonData()
{
    QThread* pPThread = new QThread();
    ThreadHandler* pHandler = new ThreadHandler();
    pHandler->setListModel(this);

    pHandler->moveToThread(pPThread);

    connect(pPThread, SIGNAL(started()), pHandler, SLOT(process()));
    connect(pHandler, SIGNAL(finished()), this, SIGNAL(countChanged()));

    // Automatically delete pPThread and pHandler after the work is done.
    connect(pHandler, SIGNAL(finished()), pHandler, SLOT(deleteLater()), Qt::QueuedConnection);
    connect(pPThread, SIGNAL(finished()), pPThread, SLOT(deleteLater()), Qt::QueuedConnection);

    pPThread->start();
}

void PersonModel::requestDeleteAll()
{
    for (int i = rowCount() - 1; i >= 0; i--)
    {
        remove(i);
    }
}

threadhandler.h

#ifndef THREADHANDLER_H
#define THREADHANDLER_H

#include <QObject>

class PersonModel;

class ThreadHandler : public QObject
{
    Q_OBJECT
public:
    explicit ThreadHandler(QObject *parent = nullptr);

    void setListModel(PersonModel* personModel);

public slots:
    void process();

signals:
    void finished();

private:
    void doPrimes();
    int calculatePrimes(int inRepeat);

private:
    PersonModel* m_personModel;
};

#endif // THREADHANDLER_H

threadhandler.cpp

#include "threadhandler.h"
#include "personmodel.h"
#include <QDebug>
#include "time.h"

ThreadHandler::ThreadHandler(QObject *parent) : QObject(parent)
{
}

void ThreadHandler::setListModel(PersonModel *personModel)
{
    m_personModel = personModel;
}

void ThreadHandler::process()
{
    qDebug() << Q_FUNC_INFO << "thread handler starts";

    // simulate the cpu intensive procedure such as db query or 3rd party library call
    calculatePrimes(3);

    int row = 5;//rowCount();

    QString strLastNameIndex;
    for (int i = 0; i < row; i++)
    {
        strLastNameIndex = QString::number(i);

        Person* person3 = new Person();
        person3->First = "Bob" + strLastNameIndex;
        person3->Last  = "LastName" + strLastNameIndex;

        m_personModel->addPerson(person3);
    }

    qDebug() << Q_FUNC_INFO << "row count: " << row;

    emit finished();

    qDebug() << Q_FUNC_INFO << "thread handler ends";
}

#define MAX_PRIME 100000

void ThreadHandler::doPrimes()
{
    unsigned long i, num, primes = 0;
    for (num = 1; num <= MAX_PRIME; ++num) {
        for (i = 2; (i <= num) && (num % i != 0); ++i);
        if (i == num)
            ++primes;
    }
    printf("Calculated %ld primes.\n", primes);
}

int ThreadHandler::calculatePrimes(int inRepeat)
{
    time_t start, end;
    time_t run_time;

    start = time(NULL);
    for (int i = 0; i < inRepeat; ++i) {
        doPrimes();
    }
    end = time(NULL);
    run_time = (end - start);
    printf("This machine calculated all prime numbers under %d %d times "
           "in %lld seconds\n", MAX_PRIME, inRepeat, run_time);

    return run_time;
}

main.qml

import QtQuick 2.6
import QtQuick.Window 2.2
import QtQml 2.0

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

    MouseArea {
        anchors.fill: parent
        onClicked: {
            timer.start()
//            loadingDialog.is_running = true
            console.log("personListView click to request data from qml to C++")
            PersonModel.requestDeleteAll();
            PersonModel.requestPersonData();
        }
    }

    ListView {
        id: personListView
        width: 150; height: 400
        visible: loadingDialog.is_running === true ? false : true;

        model: PersonModel
        delegate: personDelegate

        Component.onCompleted: {
            console.log(PersonModel, model)
            console.log(PersonModel.count, model.count)
        }

        onCountChanged: {
//            console.log("personListView onCountChanged getting person data is finished.")
//            console.log("personListView onCountChanged after search model count: " + PersonModel.count)
            loadingDialog.is_running = false
        }
    }

    Component {
        id: personDelegate
        Rectangle {
            width: personListView.width
            height: 30
            color: "lightgreen"

            Text {
                text: model.first + " " + model.last
            }

            MouseArea {
                anchors.fill: parent
                propagateComposedEvents: true
                onClicked: {
                    personListView.currentIndex = model.index
                    console.log("personListView ListView item" + model.index + " is clicked.")
                    PersonModel.remove(model.index);
                }
            }
        }
    }

    Timer {
        id: timer
        interval: 100
        repeat: false
        running: false
        triggeredOnStart: false

        onTriggered: {
            loadingDialog.is_running = true
            timer.stop()
        }
    }

    Rectangle {
        id: loadingDialog
        width: 50; height: 50
        color: "red"

        property bool is_running: false

        visible: is_running;

        NumberAnimation on x {
            from: 0
            to: 250;
            duration: 3000
            loops: Animation.Infinite
            running: loadingDialog.is_running
        }
    }
}

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext> // setContextProperty()

#include "personmodel.h"

int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;

    PersonModel mymodel;

    // Add initial data for list model
    Person person1;
    person1.First = "Bob";
    person1.Last  = "One";

    Person person2;
    person2.First = "Bob2";
    person2.Last  = "Two";

    mymodel.addPerson(&person1);
    mymodel.addPerson(&person2);

    engine.rootContext()->setContextProperty("PersonModel", &mymodel);

    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.load(url);

    return app.exec();
}

Solution

  • Since the model is related to the view then you cannot modify it directly from another thread. In this case it is better to create a signal that sends the information to the model:

    threadhandler.h

    signals:
        void finished();
        void sendPerson(Person *person);
    

    threadhandler.cpp

    void ThreadHandler::process()
    {
        qDebug() << Q_FUNC_INFO << "thread handler starts";
    
        // simulate the cpu intensive procedure such as db query or 3rd party library call
        calculatePrimes(3);
    
        int row = 5;//rowCount();
    
        QString strLastNameIndex;
        for (int i = 0; i < row; i++)
        {
            strLastNameIndex = QString::number(i);
    
            Person* person3 = new Person;
            person3->First = "Bob" + strLastNameIndex;
            person3->Last  = "LastName" + strLastNameIndex;
    
            Q_EMIT sendPerson(person3);
        }
    
        qDebug() << Q_FUNC_INFO << "row count: " << row;
    
        emit finished();
    
        qDebug() << Q_FUNC_INFO << "thread handler ends";
    }
    

    personmodel.cpp

    void PersonModel::requestPersonData()
    {
        QThread* pPThread = new QThread();
        ThreadHandler* pHandler = new ThreadHandler();
        // pHandler->setListModel(this);
    
        pHandler->moveToThread(pPThread);
    
        connect(pPThread, &QThread::started, pHandler, &ThreadHandler::process);
        connect(pHandler, &ThreadHandler::finished, this, &PersonModel::countChanged);
        connect(pHandler, &ThreadHandler::sendPerson, this, &PersonModel::addPerson);
    
        // Automatically delete pPThread and pHandler after the work is done.
        connect(pHandler, &ThreadHandler::finished, pHandler, &QObject::deleteLater, Qt::QueuedConnection);
        connect(pPThread, &QThread::finished, pPThread, &QObject::deleteLater, Qt::QueuedConnection);
    
        pPThread->start();
    }
    

    Remove the setListModel method and everything related to that method.