如何使用异步获取的数据填充QTableView?

6

我希望在QTableView创建时使用获取到的数据(例如使用数据库或网络请求)进行填充。由于请求需要一些时间 - 因此阻塞了GUI - 我得出结论,为获取数据使用另一个线程。

我的当前设置如下所示(显然是简化的):

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

在对象被添加到GUI后,init() 方法将被调用。由于我对 C++/Qt/多线程比较陌生,所以我想问一下这段代码是否按照预期运行,或者我是否可能遇到某种竞态条件之类的问题。我特别担心 onResult() 方法,因为我担心 "setModel" 可能不是线程安全的。

1个回答

7

如果模型正在异步加载其数据,则不需要对视图进行子类化。这与视图的行为无关。

模型/视图 模式的整个目的是将模型和视图组件解耦,以增加灵活性和重用性。像那样对视图进行子类化,又将它们耦合在一起了。

我特别担心 onResult() 方法,因为我担心 "setModel" 可能不是线程安全的。

你是对的setModel 不是线程安全的。您不应该从主线程以外的线程触摸任何 QWidget,请参见 文档。但是,onResult 方法保证在视图所在的线程中调用(应该是主线程)。所以这里没有任何问题...

但是,看起来你正在从线程池调用的函数中创建模型。 如果你不在函数末尾将模型移动到主线程(很可能你没有这样做),你的模型将存在于一个不运行事件循环的线程中。它将无法接收事件,这只会引发问题。通常情况下,应避免在线程之间传递 QObject (如果可能的话),并仅传递需要的数据结构。


我将从零开始,通过子类化QAbstractTableModel来实现整个内容,这里是一个完整的最小示例:

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"

注意:

上面的示例展示了如何异步地将长时间阻塞线程的函数中加载数据到模型中。这适用于执行重型计算的函数。如果您的目标是通过网络加载数据,您应该使用QTcpSocket/QNetworkAccessManager提供的异步API,在这些情况下完全不需要使用线程池,但除此之外,一切都应该类似。


2
谢谢你的指正,+1 是因为你注意到了在一个次线程中创建模型和视图/模型回顾。 - Adrien Leravat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接