做了这么多年Qt,几乎每个从入门往进阶走的同学都会在表格这关卡一下。刚开始用 QTableWidget 觉得真香,setItem、setText 一行行写就能跑;等数据量到了几千行、几万行,界面开始卡顿、内存开始膨胀,才意识到 Qt 的表格远不是"往里塞数据"这么简单。我见过太多帖子问"为什么我的 Qt 表格 1 万行数据就卡成 PPT",其实问题往往不在硬件,而在切入姿势不对。
这篇文章定位成"入门之后、优化之前"必读的一篇。主线非常明确:从 QTableWidget 换到 QTableView 加自定义模型,再配合刷新策略、委托绘制、数据缓存这些手段,把表格从"能显示"拉到"抗得住"的级别。适合用过基础控件、写过简单增删改查、但没搞明白性能瓶颈在哪的 Qt 新手,也适合项目里表格已经开始拖垮界面、想从架构层重新梳理一遍的开发者。文章里的代码我都基于 Qt 5.15.2 验证过,后面会放一组这台环境下的实测数据给你参考。
1. 项目概述:先搞清楚Qt表格优化的边界
1.1 你卡在哪一步:QTableWidget和QTableView差在哪
很多新手不知道,Qt 其实提供了两套表格接口。QTableWidget 是给人快速上手的"便捷款",底层是 QTableView + 内部自带的模型,你每写一行 setItem(row, col, new QTableWidgetItem(...)),它就在内部帮你创建了一个 QTableWidgetItem 对象,把数据、文字颜色、背景色、图标、对齐方式全部挂在这个对象身上。
听起来很方便,对吧?但在数据量上来之后,这种方便就是原罪。一张 1000 行 × 20 列的表格,意味着你要创建 2 万个 QTableWidgetItem,每个对象带自己的 flags、data、background,加上 QString 的深拷贝,轻松吃掉几 MB 甚至十几 MB。更麻烦的是,插入 item 的过程中表格会频繁触发布局和重绘,数据一多,插入阶段就开始肉眼可见地卡。
QTableView 不一样。它本身不保管数据,只负责"按需绘制"。你给它一个模型,它滚动到哪一行就只问模型要那一行的数据,屏幕上不可见的行根本不会去创建任何对象。同样是 100 万行数据,QTableView 只关心当前视口里能看见的那几十行,这才是它能扛大数据的根本原因。
1.2 模型/视图架构为什么是性能分水岭
模型/视图(Model/View)架构听起来高大上,其实道理很朴素:数据和显示分离。你的业务数据放在自己的内存结构里(比如 QVector<QVector<QVariant>>),模型负责给视图提供"标准格式"的数据访问接口,视图负责把数据画出来。视图完全不需要知道你底层是数组、数据库还是文件,它只调 data() 这个方法。
理解这一点之后,你也就理解了优化的大方向:所有优化,都要围绕"如何让模型更快地回答问题"和"如何让视图更少地问问题"来做。比如数据加载慢,就优化模型的查询逻辑;滚动卡,就减少视图的重复计算(固定行高、缓存);界面闪烁,就是刷新粒度太大,把 reset 改成精准的 dataChanged。这篇文章后面的所有内容,本质都是在反复实践这两句话。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 第一步落地:从QTableWidget换到QTableView加自定义模型
2.1 自定义模型的核心代码长什么样
直接上代码。这个模型是我平时项目里最常用的骨架,只读、数据存在内存二维数组里,够应付绝大多数展示场景:
cpp复制#include <QAbstractTableModel>
#include <QVector>
class SampleTableModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit SampleTableModel(QObject *parent = nullptr)
: QAbstractTableModel(parent)
{
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
if (parent.isValid())
return 0;
return m_data.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
if (parent.isValid())
return 0;
return m_cols;
}
QVariant data(const QModelIndex &index, int role) const override
{
if (!index.isValid())
return QVariant();
if (role == Qt::DisplayRole)
return m_data.value(index.row()).value(index.column());
return QVariant();
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
{
if (role != Qt::DisplayRole)
return QVariant();
if (orientation == Qt::Horizontal)
return QStringLiteral("第 %1
