QTableView Optimization

QTableView 的 优化 包括两部分

  • 功能的优化
  • 性能的优化

首先,我们来聊聊性能上的优化

performance optimization


1. ResizeToContents

如果对 QTableView 的 verticalHeader 或 horizontalHeader 设置setSectionResizeMode(QHeaderView::ResizeToContents) 的话,QTableView 会遍历整张表,造成卡顿
所以在表格数据量大的时候,建议将 mode 设置为 QHeaderView::Interactive 或 QHeaderView::Fixed

2. QAbstractTableModel

写了两个简单的 Demo,加载 7 列 20 万行 Student 数据,在我 6700K 的 CPU 上跑 release 版本
用 QAbstractTableModel 写的模型加载完数据需要 90 ms
用 QStandardItemModel 写的模型加载完数据需要 690 ms
接近 8 倍的性能差距

测试代码1

有些同学习惯了使用 QTableWidget(它性能更差)
推荐尝试下继承 QAbstractTableModel 的自定义 Table 模型,用法其实不难
它有 4 个纯虚函数需要实现,rowCount、columnCount、data 和 headerData
(如果是 QAbstractItemModel,其实还有 index 和 parent 两个纯虚函数,这里先按下不表)
rowCount 代表 table 总的行数,columnCount 代表 table 总的列数,data 代表某个单元格的内容(最重要的函数),headerData 代表每一列(或每一行)表头的内容

我们用下面的代码举例:

//studenttablemodel.h
struct Student
{
    char name[16];
    char id[24];
    char sex[8];
    char age[8];
    char phone[16];
    char hobby[24];
    char company[16];
};

class StudentTableModel : public QAbstractTableModel
{
    Q_OBJECT
public:
    StudentTableModel(QObject* parent = NULL);
    virtual int rowCount(const QModelIndex &) const;
    virtual int columnCount(const QModelIndex &) const;
    virtual QVariant data(const QModelIndex &index, int role) const;
    virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
    void testData();
private:
    QStringList m_headers;
    QList<Student*> m_itemList;
};

//studenttablemodel.cpp
StudentTableModel::StudentTableModel(QObject *parent): QAbstractTableModel(parent) {
    m_headers << "Name" << "ID" << "Sex" << "Age" << "Phone" << "Hobby" << "Company";
}

int StudentTableModel::rowCount(const QModelIndex &) const {
    return m_itemList.size();
}

int StudentTableModel::columnCount(const QModelIndex &) const {
    return m_headers.size();
}

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

    if (index.row() >= m_itemList.size() || index.row() < 0)
        return QVariant();

    if (role == Qt::DisplayRole) {
        Student* data = m_itemList.at(index.row());
        switch(index.column()) {
        case 0: return data->name;
        case 1: return data->id;
        case 2: return data->sex;
        case 3: return data->age;
        case 4: return data->phone;
        case 5: return data->hobby;
        case 6: return data->company;
        }
    }
    return QVariant();
}

QVariant StudentTableModel::headerData(int section, Qt::Orientation orientation, int role) const {
    if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
        return m_headers.at(section);
    }
    return QVariant();
}

void StudentTableModel::testData() {
    beginInsertRows(QModelIndex(), m_itemList.size(), m_itemList.size() + 200000 - 1);
    for(int i=0; i<200000; ++i) {
        Student* data = new Student;
        strncpy(data->name, "zhangsan", 16);
        strncpy(data->id, "53302219861001xxxx", 24);
        strncpy(data->sex, "M", 8);
        strncpy(data->age, "33", 8);
        strncpy(data->phone, "18910108888", 16);
        strncpy(data->hobby, "BasketBall, Play", 24);
        strncpy(data->company, "Alibaba", 16);
        m_itemList.append(data);
    }
    endInsertRows();
}

第24行 QStringList m_headers 定义了一个表示所有列名称的变量
第25行 QList<Student*> m_itemList 定义了一个记录所有单元格内容的变量
所以 int rowCount(const QModelIndex &) 函数只要用 m_itemList 的大小就能确定
int columnCount(const QModelIndex &) 函数只要用 m_headers 的大小就能确定
QVariant headerData(int section, Qt::Orientation orientation, int role) 函数有3个参数
section 代表第几列,orientation 代表表头在最上边还是在最左边(Qt::Horizontal 代表上边,Qt::Vertical 代表左边)
role 有很多值,在这里我们只判断它是否等于 Qt::DisplayRole,即表头显示的文字
返回值因为是 QVariant,所以你返回 int,QString,char* 都可以
data(const QModelIndex &index, int role) 函数有2个参数,QModelIndex 代表一个单元格
index.column() 可以得到该单元格在第几列,index.row() 可以得到该单元格在第几行

我们可以想象一下,table 开始绘制时,每个单元格都通过这个函数去访问它的数据,它的数据包括单元格的背景色、前景色、显示的文字、鼠标放上去时弹出的tip框文字、单元格的字体、是否包含一个图标、显示的勾选框的状态、单元格的大小等等

这里我们只判断当 role == Qt::DisplayRole 时应该返回什么,即单元格的文本文字
通过 m_itemList 的索引,我们很容易就能获得任意单元格的文本文字,即代码第 48-57 行
第70行是自定义的函数,外部每次调用它,可以插入20万条数据,append 到 m_itemList 中
为了代码简化,每条数据都是一样的内容

3. beginInsertRows

如果你有大量数据需要插入 model,推荐将多次 insert 合并为一次

//使用 beginInsertRows + endInsertRows
//错误的示范,加载20万条数据,耗时90ms
void StudentTableModel::testData()
{
    for(int i=0; i<200000; ++i)
    {
        Student* data = new Student;
        strncpy(data->name, "zhangsan", 16);
        strncpy(data->id, "53302219861001xxxx", 24);
        strncpy(data->sex, "M", 8);
        strncpy(data->age, "33", 8);
        strncpy(data->phone, "18910108888", 16);
        strncpy(data->hobby, "BasketBall, Play", 24);
        strncpy(data->company, "Alibaba", 16);
        beginInsertRows(QModelIndex(), m_itemList.size(), m_itemList.size() + 1 - 1);
        m_itemList.append(data);
        endInsertRows();
    }
}
//使用 beginInsertRows + endInsertRows
//正确的示范,加载20万条数据,耗时22ms
void StudentTableModel::testData()
{
    beginInsertRows(QModelIndex(), m_itemList.size(), m_itemList.size() + 200000 - 1);
    for(int i=0; i<200000; ++i)
    {
        Student* data = new Student;
        strncpy(data->name, "zhangsan", 16);
        strncpy(data->id, "53302219861001xxxx", 24);
        strncpy(data->sex, "M", 8);
        strncpy(data->age, "33", 8);
        strncpy(data->phone, "18910108888", 16);
        strncpy(data->hobby, "BasketBall, Play", 24);
        strncpy(data->company, "Alibaba", 16);
        m_itemList.append(data);
    }
    endInsertRows();
}

我们也可以使用 beginResetModel 来重置模型,缺点是会丢失之前的选中项状态

//使用 beginResetModel + endResetModel
//加载20万条数据,耗时22ms
void StudentTableModel::testData()
{
    beginResetModel();
    for(int i=0; i<200000; ++i)
    {
        Student* data = new Student;
        strncpy(data->name, "zhangsan", 16);
        strncpy(data->id, "53302219861001xxxx", 24);
        strncpy(data->sex, "M", 8);
        strncpy(data->age, "33", 8);
        strncpy(data->phone, "18910108888", 16);
        strncpy(data->hobby, "BasketBall, Play", 24);
        strncpy(data->company, "Alibaba", 16);
        m_itemList.append(data);
    }
    endResetModel();
}

我们还可以使用 layoutAboutToBeChanged + layoutChanged 来更新模型
一般我们还需要使用 changePersistentIndexList() 更新持久索引(例子中没用到)

//使用 layoutAboutToBeChanged + layoutChanged 
//加载20万条数据,耗时22ms
void StudentTableModel::testData()
{
    layoutAboutToBeChanged();
    for(int i=0; i<200000; ++i)
    {
        Student* data = new Student;
        strncpy(data->name, "zhangsan", 16);
        strncpy(data->id, "53302219861001xxxx", 24);
        strncpy(data->sex, "M", 8);
        strncpy(data->age, "33", 8);
        strncpy(data->phone, "18910108888", 16);
        strncpy(data->hobby, "BasketBall, Play", 24);
        strncpy(data->company, "Alibaba", 16);
        m_itemList.append(data);
    }
    layoutChanged();
}

测试代码2

4. QContiguousCache

如果使用 QContiguousCache,可以优化内存的占用
用 QList<Student*> 存储数据时,程序运行占用 33.8M 内存
而用 QContiguousCache<Student> 存储数据,程序运行占用 8M 内存,
但加载 20万 条数据,耗时 60ms

测试代码3

5. fetchMore

一般的Model都是针对固定的数据源,在数据源巨大的情况下, 比如大型数据库每个表可能有数万甚至百万级的数据, 如果是用基本的Model一次性把数据都取出来显示那将是一个恐怖的过程, 搞不好你的系统就玩完了

fetchMore 就是当你需要增量填充模型时,必须重新实现的函数
如下图所示,参考 Qt 的 example,笔者这里就不展开


To be continued....

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  •   将一个单元格中的多个元素拆分成多行,如图。
    NAome阅读 5,732评论 0 6
  • C#子窗体给父窗体传值的几种方法 一、我们举个小例子,分别用几种不同的方法来实现。比如:我们现在有教师、学生两个类...
    千年的小妖阅读 1,479评论 0 1
  • 界面 主窗口界面设计 标题栏:直接设Window-Title属性;Window-icon属性可加图标。底部状态栏:...
    码园老农阅读 3,799评论 1 13
  • 索引: 三个核心思维问题 1. 果断执行、完整模块、抢时间 2. 核心事务的选取与动机清晰化,责检意识 3. 取舍...
    极目Studio阅读 175评论 0 0
  • 把人生所有的点连成线,是一种能力。 乔布斯:人生就是一个连点成线的过程。有些经历,也许一开始看不到它的意义所在,但...
    91838c0cf1b8阅读 529评论 0 0