QTableView 的 优化 包括两部分
功能的优化
性能的优化
首先,我们来聊聊性能上的优化
如果对 QTableView 的 verticalHeader 或 horizontalHeader 设置 setSectionResizeMode(QHeaderView::ResizeToContents) 的话,QTableView 会遍历整张表,造成卡顿
所以在表格数据量大的时候,
建议将 mode 设置为 QHeaderView::Interactive 或 QHeaderView::Fixed
写了两个简单的 Demo,加载 7 列 20 万行 Student 数据,在我 6700K 的 CPU 上跑 release 版本
用 QAbstractTableModel 写的模型加载完数据需要 90 ms
用 QStandardItemModel 写的模型加载完数据需要 690 ms
接近 8 倍的性能差距
有些同学习惯了使用 QTableWidget(它性能更差)
推荐尝试下继承 QAbstractTableModel 的自定义 Table 模型,用法其实不难
它有 4 个纯虚函数需要实现,rowCount、columnCount、data 和 headerData
(如果是 QAbstractItemModel,其实还有 index 和 parent 两个纯虚函数,这里先按下不表)
rowCount 代表 table 总的行数,columnCount 代表 table 总的列数,data 代表某个单元格的内容(最重要的函数),headerData 代表每一列(或每一行)表头的内容
我们用下面的代码举例:
x1//studenttablemodel.h2struct Student3{4 char name[16];5 char id[24];6 char sex[8];7 char age[8];8 char phone[16];9 char hobby[24];10 char company[16];11};12
13class StudentTableModel : public QAbstractTableModel14{15 Q_OBJECT16public:17 StudentTableModel(QObject* parent = NULL);18 virtual int rowCount(const QModelIndex &) const;19 virtual int columnCount(const QModelIndex &) const;20 virtual QVariant data(const QModelIndex &index, int role) const;21 virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;22 void testData();23private:24 QStringList m_headers;25 QList<Student*> m_itemList;26};27
28//studenttablemodel.cpp29StudentTableModel::StudentTableModel(QObject *parent): QAbstractTableModel(parent) {30 m_headers << "Name" << "ID" << "Sex" << "Age" << "Phone" << "Hobby" << "Company";31}32
33int StudentTableModel::rowCount(const QModelIndex &) const {34 return m_itemList.size();35}36
37int StudentTableModel::columnCount(const QModelIndex &) const {38 return m_headers.size();39}40
41QVariant StudentTableModel::data(const QModelIndex &index, int role) const {42 if (!index.isValid())43 return QVariant();44
45 if (index.row() >= m_itemList.size() || index.row() < 0)46 return QVariant();47
48 if (role == Qt::DisplayRole) {49 Student* data = m_itemList.at(index.row());50 switch(index.column()) {51 case 0: return data->name;52 case 1: return data->id;53 case 2: return data->sex;54 case 3: return data->age;55 case 4: return data->phone;56 case 5: return data->hobby;57 case 6: return data->company;58 }59 }60 return QVariant();61}62
63QVariant StudentTableModel::headerData(int section, Qt::Orientation orientation, int role) const {64 if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {65 return m_headers.at(section);66 }67 return QVariant();68}69
70void StudentTableModel::testData() {71 beginInsertRows(QModelIndex(), m_itemList.size(), m_itemList.size() + 200000 - 1);72 for(int i=0; i<200000; ++i) {73 Student* data = new Student;74 strncpy(data->name, "zhangsan", 16);75 strncpy(data->id, "53302219861001xxxx", 24);76 strncpy(data->sex, "M", 8);77 strncpy(data->age, "33", 8);78 strncpy(data->phone, "18910108888", 16);79 strncpy(data->hobby, "BasketBall, Play", 24);80 strncpy(data->company, "Alibaba", 16);81 m_itemList.append(data);82 }83 endInsertRows();84}第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 中
为了代码简化,每条数据都是一样的内容
如果你有大量数据需要插入 model,推荐将多次 insert 合并为一次
xxxxxxxxxx381//使用 beginInsertRows + endInsertRows2//错误的示范,加载20万条数据,耗时90ms3void StudentTableModel::testData()4{5 for(int i=0; i<200000; ++i)6 {7 Student* data = new Student;8 strncpy(data->name, "zhangsan", 16);9 strncpy(data->id, "53302219861001xxxx", 24);10 strncpy(data->sex, "M", 8);11 strncpy(data->age, "33", 8);12 strncpy(data->phone, "18910108888", 16);13 strncpy(data->hobby, "BasketBall, Play", 24);14 strncpy(data->company, "Alibaba", 16);15 beginInsertRows(QModelIndex(), m_itemList.size(), m_itemList.size() + 1 - 1);16 m_itemList.append(data);17 endInsertRows();18 }19}20//使用 beginInsertRows + endInsertRows21//正确的示范,加载20万条数据,耗时22ms22void StudentTableModel::testData()23{24 beginInsertRows(QModelIndex(), m_itemList.size(), m_itemList.size() + 200000 - 1);25 for(int i=0; i<200000; ++i)26 {27 Student* data = new Student;28 strncpy(data->name, "zhangsan", 16);29 strncpy(data->id, "53302219861001xxxx", 24);30 strncpy(data->sex, "M", 8);31 strncpy(data->age, "33", 8);32 strncpy(data->phone, "18910108888", 16);33 strncpy(data->hobby, "BasketBall, Play", 24);34 strncpy(data->company, "Alibaba", 16);35 m_itemList.append(data);36 }37 endInsertRows();38}我们也可以使用 beginResetModel 来重置模型,缺点是会丢失之前的选中项状态
xxxxxxxxxx191//使用 beginResetModel + endResetModel2//加载20万条数据,耗时22ms3void StudentTableModel::testData()4{5 beginResetModel();6 for(int i=0; i<200000; ++i)7 {8 Student* data = new Student;9 strncpy(data->name, "zhangsan", 16);10 strncpy(data->id, "53302219861001xxxx", 24);11 strncpy(data->sex, "M", 8);12 strncpy(data->age, "33", 8);13 strncpy(data->phone, "18910108888", 16);14 strncpy(data->hobby, "BasketBall, Play", 24);15 strncpy(data->company, "Alibaba", 16);16 m_itemList.append(data);17 }18 endResetModel();19}我们还可以使用 layoutAboutToBeChanged + layoutChanged 来更新模型
一般我们还需要使用 changePersistentIndexList() 更新持久索引(例子中没用到)
xxxxxxxxxx191//使用 layoutAboutToBeChanged + layoutChanged 2//加载20万条数据,耗时22ms3void StudentTableModel::testData()4{5 layoutAboutToBeChanged();6 for(int i=0; i<200000; ++i)7 {8 Student* data = new Student;9 strncpy(data->name, "zhangsan", 16);10 strncpy(data->id, "53302219861001xxxx", 24);11 strncpy(data->sex, "M", 8);12 strncpy(data->age, "33", 8);13 strncpy(data->phone, "18910108888", 16);14 strncpy(data->hobby, "BasketBall, Play", 24);15 strncpy(data->company, "Alibaba", 16);16 m_itemList.append(data);17 }18 layoutChanged();19}如果使用 QContiguousCache,可以优化内存的占用
用 QList<Student*> 存储数据时,程序运行占用 33.8M 内存
而用 QContiguousCache
但加载 20万 条数据,耗时 60ms
一般的Model都是针对固定的数据源,在数据源巨大的情况下, 比如大型数据库每个表可能有数万甚至百万级的数据, 如果是用基本的Model一次性把数据都取出来显示那将是一个恐怖的过程, 搞不好你的系统就玩完了
fetchMore 就是当你需要增量填充模型时,必须重新实现的函数
如下图所示,参考 Qt 的 example,笔者这里就不展开了
To be continued....