Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I populate a QTableView with asynchronous fetched data?

I want to populate a QTableView with some fetched data (using for example a database- or network-request) upon its creation. Because the request takes some time - thus blocking the GUI - I came to the conclusion to use another thread for fetching.

My current setup looks like this (obviously simplified):

class MyTable : public QTableView {
    QFutureWatcher<QAbstractItemModel*>* watcher;

    void init() {
        watcher = new QFutureWatcher<QAbstractItemModel*>();
        connect(watcher, SIGNAL(finished()), this, SLOT(onResult()));
        watcher->setFuture(QtConcurrent::run(...))
    }

    void onResult() {
        setModel(watcher->result());
    }
}

The init()-methode is called after the object has been added to the GUI. Because I am quite new to C++ / Qt / Multithreading, I wanted to ask if this code behaves as expected or if I may run into some kind of race condition or the like. I'm especially concerned about the onResult()-method, because I fear "setModel" might not be thread safe.

like image 497
Jonas Möller Avatar asked Mar 10 '17 22:03

Jonas Möller


1 Answers

You don't need to subclass the view if the model is loading its data asynchronously. This has nothing to do with the view's behavior.

The whole purpose of the Model/View pattern is to decouple the model and the view components to increase flexibility and reuse. By subclassing the view like that, You are coupling them again.

I'm especially concerned about the onResult()-method, because I fear "setModel" might not be thread safe.

You are right, setModel is not thread-safe. You shouldn't touch any QWidget from a thread other than the main thread, see docs. But, onResult method is guaranteed to be called in the thread where the view lives (that should be the main thread). So, there is nothing wrong here. . .

But, it seems that you are creating the model in a function that is called from the thread pool. If you don't move the model to the main thread at the end of your function (and most likely you aren't doing that), your model will be living in a thread that doesn't run an event loop. It will not be able to receive events, this is just asking for trouble. Generally you should avoid passing QObjects between threads (when this is possible), and only pass data structures that you need.


I'll start from scratch, and implement the whole thing by subclassing QAbstractTableModel, Here is a complete minimal example:

screenshot

#include <QtWidgets>
#include <QtConcurrent>
#include <tuple>

class AsyncTableModel : public QAbstractTableModel{
    Q_OBJECT
    //type used to hold the model's internal data in the variable m_rows
    using RowsList = QList<std::tuple<QString, QString, QString> >;
    //model's data
    RowsList m_rows;
    QFutureWatcher<RowsList>* m_watcher;
public:
    explicit AsyncTableModel(QObject* parent= nullptr):QAbstractTableModel(parent){
        //start loading data in the thread pool as soon as the model is instantiated 
        m_watcher = new QFutureWatcher<RowsList>(this);
        connect(m_watcher, &QFutureWatcher<RowsList>::finished,
                this, &AsyncTableModel::updateData);
        QFuture<RowsList> future = QtConcurrent::run(&AsyncTableModel::retrieveData);
        m_watcher->setFuture(future);
    }
    ~AsyncTableModel() = default;
    
    //this is a heavy function that returns data you want the model to display
    //this is called in the thread pool using QtConcurrent::run
    static RowsList retrieveData(){
        //the function is heavy that it blocks the calling thread for 2 secs
        QThread::sleep(2);
        RowsList list;
        for(int i=0; i<10; i++){
            list.append(std::make_tuple(QString("A%0").arg(i),
                                        QString("B%0").arg(i),
                                        QString("C%0").arg(i)));
        }
        return list;
    }
    //this is the slot that is called when data is finished loading
    //it resets the model so that it displays new data
    Q_SLOT void updateData(){
        beginResetModel();
        m_rows = m_watcher->future().result();
        endResetModel();
    }
    
    int rowCount(const QModelIndex &parent) const {
        if(parent.isValid()) return 0;
        return m_rows.size(); 
    }
    int columnCount(const QModelIndex &parent) const {
        if(parent.isValid()) return 0;
        return 3; 
    }
    
    QVariant data(const QModelIndex &index, int role) const {
        QVariant value= QVariant();
        switch(role){
        case Qt::DisplayRole: case Qt::EditRole:
            switch(index.column()){
            case 0:
                value= std::get<0>(m_rows[index.row()]);
                break;
            case 1:
                value= std::get<1>(m_rows[index.row()]);
                break;
            case 2:
                value= std::get<2>(m_rows[index.row()]);
            }
            break;
        }
        return value;
    }
};

int main(int argc, char* argv[]){
    QApplication a(argc, argv);
    
    QTableView tv;
    AsyncTableModel model;
    tv.setModel(&model);
    tv.show();
    
    
    return a.exec();
}

#include "main.moc"

Note:

The example above shows how to load data from a function that blocks the thread for a long time into a model asynchronously. This is the case for functions that perform heavy computations. If your objective is to load data over the network, You should use the asynchronous API provided in QTcpSocket/QNetworkAccessManager, there is no need to use the thread pool in these cases at all, but other than that, everything should be similar.

like image 103
Mike Avatar answered Nov 02 '22 18:11

Mike