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 倍的性能差距
有些同学习惯了使用 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();
}
4. QContiguousCache
如果使用 QContiguousCache,可以优化内存的占用
用 QList<Student*> 存储数据时,程序运行占用 33.8M 内存
而用 QContiguousCache<Student> 存储数据,程序运行占用 8M 内存,
但加载 20万 条数据,耗时 60ms
5. fetchMore
一般的Model都是针对固定的数据源,在数据源巨大的情况下, 比如大型数据库每个表可能有数万甚至百万级的数据, 如果是用基本的Model一次性把数据都取出来显示那将是一个恐怖的过程, 搞不好你的系统就玩完了
fetchMore 就是当你需要增量填充模型时,必须重新实现的函数
如下图所示,参考 Qt 的 example,笔者这里就不展开
To be continued....