QTableView 是我这几年做 Qt 桌面端最常用的控件之一,说它是表格类需求的首选一点也不夸张。只要涉及数据列表展示、批量编辑、右键操作、自定义单元格,QTableView 基本都能兜住。很多人一开始图省事直接拖一个 QTableWidget,等到数据量上来、界面复杂了再想换,就要动不少手术。这篇文章我把自己在实际项目中用 QTableView 的思路、代码、踩坑记录全部整理出来,内容不绕弯子,适合刚接触 Qt 的开发者,也适合已经用过 QTableWidget 想往更灵活方向走的人。
1. 为什么选 QTableView 而不是 QTableWidget
1.1 模型/视图架构的核心认知
先聊一个很多人都没细想的点:QTableWidget 明明也能显示表格,为什么非要选 QTableView?根源在于 Qt 的模型/视图架构。QTableWidget 自带一个内部模型,你往里面塞数据,它自己消化。这个模式在几十行数据、固定列数的小场景下很舒服,但它把数据存储和界面显示绑死了。
QTableView 本身不存数据,它只负责显示,真正的数据放在模型里,模型继承自 QAbstractItemModel。刚开始接触会觉得这层抽象有点绕,但拆开理解其实很简单:模型提供数据,视图负责展示,委托负责“怎么显示怎么编辑”。这种分离带来的直接好处是,同一个模型可以挂多个视图,比如左边一个树形导航,右边一个表格,数据改一个地方,两个界面同步刷新。这在做数据管理类工具时几乎是刚需。
另一个容易被忽略的好处是自定义能力。QTableWidget 如果你想彻底控制某个单元格的绘制逻辑,通常要处理一堆细节,而且很容易碰壁。QTableView 的思路则非常清晰:写一个委托类,重写 paint 或者 createEditor,告诉视图这个列该长什么样。这就像一个渲染流水线,每一环都能插进去改,而不是在一个封闭黑盒里凑合。
1.2 什么时候该用 QTableView
我个人的判断标准很简单:只要表格不是“一次性页面展示”,而是需要跟后端数据联动、需要复杂交互、需要复用布局,就直接上 QTableView。比如:
- 数据从数据库查询出来,要支持排序、筛选、分页
- 某个状态列要根据数值显示不同的颜色、图标或进度条
- 需要对某几列做特殊编辑器,比如日期选择器、下拉联动框、文件选择按钮
- 需要同一个数据在不同窗口里以不同方式展示
- 需要操作大量数据,比如几千行甚至上万行实时刷新
在这些场景里 QTableWidget 也能做,但代码量和维护成本会明显上升。反之,如果你只是做一个设置界面,固定五行三列,放几个可编辑项,那 QTableWidget 确实更快,没必要硬上 QTableView。工具选型从来都是平衡收益和成本,而不是为了技术看起来高级。
2. 快速上手:从零搭建一个可用的表格
2.1 准备模型和视图
我习惯的写法是在代码里手动创建 QTableView,而不是在 UI 文件里拖控件。原因很实际:当你需要动态列、动态数据类型时,代码创建比改 UI 文件灵活得多。界面上只需要放一个空控件或者直接 new 出来添加到布局里。
// 创建一个表格视图 QTableView *tableView = new QTableView(this); // 创建一个标准模型,用来存储表格数据 QStandardItemModel *model = new QStandardItemModel(this); // 设置表头 model->setHorizontalHeaderLabels({"名称", "类型", "大小", "修改时间"}); // 视图绑定模型 tableView->setModel(model);这里用的 QStandardItemModel 是 Qt 自带的通用模型,适合中小数据量场景。它的使用成本最低,大部分入门项目都从这个模型开始。如果你的数据显示量极大,比如几百万行,后续可能需要自定义模型,但核心调用逻辑是一样的,只是模型内部的数据获取方式不同而已。
setModel 是视图和模型建立连接的关键。一旦绑定,视图中显示的内容完全由模型决定,视图自己的职责只是布局和交互。这个设计好处在于,你可以在任意时刻替换整个模型,视图会立刻刷新,不需要手动清空再填充。
2.2 把数据填进表格
往 QStandardItemModel 里塞数据的常规操作是创建 QStandardItem,设置内容,然后放到指定行列。下面是一个简单的示例,演示了字符串、数字和勾选状态三种常见数据形式。
// 第一行 QStandardItem *item0 = new QStandardItem("项目配置"); QStandardItem *item1 = new QStandardItem("配置文件"); item1->setData(1024, Qt::UserRole); // 存额外数据:文件大小字节数 QStandardItem *item2 = new QStandardItem("1 KB"); QStandardItem *item3 = new QStandardItem("2025-01-12 10:30"); model->appendRow({item0, item1, item2, item3}); // 第二行,带勾选框 QStandardItem *checkItem = new QStandardItem("启用日志"); checkItem->setCheckable(true); checkItem->setCheckState(Qt::Checked); model->appendRow({checkItem, new QStandardItem("开关"), new QStandardItem("--"), new QStandardItem("--")});这里有个细节值得注意:item1 我通过 setData 存了真正的文件大小数字 1024,显示上仍然是“配置文件”这个字符串。这是模型/视图架构中非常实用的一招——显示给用户看的是一套数据,背后记录逻辑判断的是另一套数据。QStandardItem 内部支持多种角色,Qt::DisplayRole 控制显示文本,Qt::UserRole 是留给开发者自定义数据的起点。排序的时候,你可以让模型按 UserRole 的大小来排,而不是按显示字符串的字典序排,这样文件大小排序才是有效的。
如果只是 foreach 一堆数据,也可以直接通过 setItem 指定行列添加:
for (int row = 0; row < dataList.size(); ++row) { model->setItem(row, 0, new QStandardItem(dataList[row].name)); model->setItem(row, 1, new QStandardItem(dataList[row].type)); }用 setItem 的好处是模型会自动扩容到足够容纳该行列,省去手动确定行数的烦恼。但对大量数据一次插入,更推荐先用 beginInsertRows 和 endInsertRows 包裹批量插入操作,或者在一次性填充前用 model->setRowCount(rows) 预设行数,能显著减少界面重绘次数。
2.3 表头、列宽、行高这些细节
表格的观感很大程度上由表头、列宽和行高决定。默认情况下 QTableView 的列宽是均分的,这在很多场景下并不合理。名称列应该宽一些,数值列窄一些,时间列居中,这个需要手动调。
// 设置表头拉伸模式 tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); // 名称列拉伸填满 tableView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); // 类型列自适应内容 tableView->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); tableView->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); // 设置默认行高 tableView->verticalHeader()->setDefaultSectionSize(36); // 如果你不想要垂直表头,可以隐藏 tableView->verticalHeader()->setVisible(false);这里要注意 QHeaderView::Interactive 与 Stretch 的区别。Interactive 表示用户可以手动拖拽调整列宽,Stretch 表示自动拉伸填满剩余空间,两者如果混合使用,经常会出现拖拽后列宽被自动拉回的情况。我的建议是:需要用户调整列宽的场景用 Interactive,并给列设置好默认宽度;希望看起来匀称的场景用 Stretch,放弃手动调宽能力。不要两种混在一个表格里,体验很割裂。
行高有个细节,如果字体字号和系统缩放比例有变化,固定行高会显得拥挤或空旷。比较稳妥的做法是用纵向表头的默认行高配置,并给内容加一点上下 padding,视觉上才舒服。
3. 核心能力:排序、选择、编辑与右键菜单
3.1 开启排序和整行选择
表格不像静态文本,用户天然期望点击表头就能按某一列排序。QTableView 开启排序非常容易,但是有个前提:模型的数据存取逻辑要配合。如果用 QStandardItemModel,只要一行代码即可开启排序,前提是你插入的数据类型统一。比如某一列存的都是数字字符串,排序时默认按字符串排,结果就是 10 排在 2 前面,非常反直觉。解决方法是插入时把真正的数值通过 QStandardItem::setData 放入 Qt::EditRole 或 Qt::UserRole。
// 开启点击表头排序 tableView->setSortingEnabled(true); model->setSortRole(Qt::UserRole); // 让排序使用自定义数据角色然后插入数据时:
QStandardItem *sizeItem = new QStandardItem; sizeItem->setData(2048, Qt::UserRole); // 显示数字时用 DisplayRole 格式化 sizeItem->setData(2048, Qt::EditRole); sizeItem->setText("2 KB");这样用户无论点击哪个表头,排序的底层依据都是数字大小而不是字符串字典序。
选择行为也要根据实际需求设置。默认情况下 QTableView 允许选中单个单元格,但在大多数业务中,我们更需要整行选中,并且只能选一行。
tableView->setSelectionBehavior(QAbstractItemView::SelectRows); // 整行选中 tableView->setSelectionMode(QAbstractItemView::SingleSelection); // 单选 tableView->setEditTriggers(QAbstractItemView::DoubleClicked); // 双击进入编辑这行配置做完,配合信号 currentRowChanged 或者 selectionModel 的选择变化信号,就能在右侧面板展示选中行对应详情,或者点击按钮对选中行进行操作。
3.2 右键菜单的完整实现
右键菜单是桌面应用逃不掉的环节。QTableView 里实现右键菜单的常规套路是绑定 customContextMenuRequested 信号,信号会带出鼠标点击位置,通过这个位置定位到行,再弹出菜单。这里有一个很多人容易犯的错:直接用 viewport 坐标做 rowAt,而不是用全局坐标。
connect(tableView, &QTableView::customContextMenuRequested, this, [this](const QPoint &pos) { // 通过视图坐标定位到当前行 QModelIndex index = tableView->indexAt(pos); if (!index.isValid()) { // 点在空白区域,不弹出菜单或弹通用菜单 return; } int row = index.row(); // 记录当前操作的行 m_currentRow = row; QMenu menu(this); QAction *actOpen = menu.addAction("打开"); QAction *actDelete = menu.addAction("删除"); QAction *actRename = menu.addAction("重命名"); QAction *selected = menu.exec(QCursor::pos()); if (selected == actOpen) { // do open } else if (selected == actDelete) { // do delete } });这里务必记得把 tableView 的 setContextMenuPolicy 设置为 Qt::CustomContextMenu,否则信号永远不会触发。
另外一个细节:右键菜单弹出的位置。有人用 mapToGlobal(pos) 获取全局坐标,这没问题,但用 QCursor::pos() 更直接,鼠标在哪菜单就在哪,不会因为表格顶部有工具栏导致偏移。若菜单要对齐到鼠标点击的单元格上,用 mapToGlobal(pos) 更精准。两种我都试过,按交互习惯选即可。
删除行时要注意模型与视图的同步。如果你直接操作视图或者删除行前没有处理模型索引,很容易出现索引失效的问题。正确做法是通过模型来删除:
model->removeRow(m_currentRow);删除完成后 QTableView 会自动刷新,不需要额外通知。
3.3 单元格编辑的控制策略
QTableView 默认支持双击编辑单元格,但控制粒度可以非常细。如果希望整列可编辑、整列只读,或者按行控制,需要操作模型的 flags 方法。QStandardItemModel 中每个 item 的 flags 决定了它是否可编辑、是否可选择。
// 让第 0 列的 item 不可编辑 for (int row = 0; row < model->rowCount(); ++row) { QStandardItem *item = model->item(row, 0); item->setEditable(false); } // 如果需要完全只读模型 tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);具体业务里经常会遇到:这一列是状态列,用户不能改;那一列是备注,允许自由编辑。这里我的经验是用 setIndexWidget 或者委托来控制,而不是粗暴地整表禁止编辑。setIndexWidget 可以往指定单元格塞任意 widget,但要注意它会覆盖默认绘制逻辑,性能比委托差,不适合大表格。真正灵活又高效的方式是写一个自定义委托,控制 createEditor 是否返回编辑器。后续章节我会专门讲委托。
编辑过程中的数据校验也是一个常见需求。默认情况下用户在单元格里输入什么,模型就存什么。如果你需要数值范围校验,可以在委托的 setModelData 里做拦截,或者通过 model 的 setData 方法中检查。最省事的方案是在委托的 validate 函数里有限校验,但实际项目中我发现大多数人最终会在业务层统一校验,因为跨字段联动校验在委托里写会非常啰嗦。
4. 自定义显示:委托(Delegate)实操
4.1 为什么需要委托
默认的 QTableView 只能用文本显示数据,但真实业务需要各种五花八门的内容:进度条、图片、按钮、下拉框、日期选择器、开关。这些用数据源强行拼接字符串也能显示,但要么丑,要么不可交互,要么代码可维护性差。
委托(QStyledItemDelegate) 是 Qt 提供的一个扩展钩子,它把“一个单元格怎么画、怎么编辑、怎么保存”的逻辑从模型和视图中解耦出来,单独封装成类。理解委托的关键是明确几个回调函数各管什么事:
- paint:负责绘制单元格外观
- sizeHint:告诉视图这个单元格需要多大空间
- createEditor:双击/编辑时,创建什么控件来编辑数据
- setEditorData:把模型里的数据填充到编辑器控件上
- setModelData:把编辑器控件里的数据写回模型
这几个函数是委托的全部核心。你做自定义表格控件,只要在这几个函数里按需重写即可。
4.2 一个最简单的自定义委托
假设需求是让某一列的文字在显示时自动变红,并且双击时只允许输入数字。传统做法是生病地遍历所有 item 设置颜色,而委托只需要改 paint 和 createEditor。实现方式如下:
class NumberDelegate : public QStyledItemDelegate { public: using QStyledItemDelegate::QStyledItemDelegate; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); // 只对数值列做红色处理 if (index.column() == 2) { int val = index.data(Qt::DisplayRole).toInt(); opt.palette.setColor(QPalette::Text, val < 0 ? Qt::red : Qt::darkGreen); } QStyledItemDelegate::paint(painter, opt, index); } QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override { QLineEdit *editor = new QLineEdit(parent); QDoubleValidator *validator = new QDoubleValidator(editor); editor->setValidator(validator); return editor; } void setEditorData(QWidget *editor, const QModelIndex &index) const override { QLineEdit *lineEdit = qobject_cast<QLineEdit*>(editor); lineEdit->setText(index.data(Qt::DisplayRole).toString()); } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override { QLineEdit *lineEdit = qobject_cast<QLineEdit*>(editor); model->setData(index, lineEdit->text(), Qt::EditRole); } };然后在视图中指定某列使用这个委托:
tableView->setItemDelegateForColumn(2, new NumberDelegate(tableView));这样就实现了“第 3 列显示红色文本,编辑时只能输入数字”。理解这套流程之后,你会发现 QTableView 的单元格扩展能力几乎是无限的。你想做一个图片缩略图列,只需要在 paint 里画 QPixmap;你想做一个进度条列,也只需要在 paint 里画进度条背景和填充块。
4.3 用委托让图片和进度条直接显示
进度条列在实际项目里非常实用。比如任务列表的完成度、磁盘使用率、下载进度等。用委托画进度条很简单,核心是在 paint 里拿数值,然后用 QStylePainter 绘制 QStyleOptionProgressBar。
class ProgressDelegate : public QStyledItemDelegate { public: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if (index.column() != 3) { QStyledItemDelegate::paint(painter, option, index); return; } int progress = index.data(Qt::DisplayRole).toInt(); QStyleOptionProgressBar bar; bar.initFrom(painter->device()); bar.rect = option.rect.adjusted(6, 8, -6, -8); bar.minimum = 0; bar.maximum = 100; bar.progress = progress; bar.text = QString("%1%").arg(progress); bar.textVisible = true; QStylePainter stylePainter(painter); stylePainter.drawControl(QStyle::CE_ProgressBar, bar); } };这里有个容易踩的坑:自定义绘制时,选中态背景会被你的绘制覆盖掉,导致选中后列出看不清。解决办法是在绘制前调用 initStyleOption 获取标准选项,并用 option.state 判断是否有 State_Selected,然后在绘制颜色里叠加半透明高亮。这样交互和视觉才能统一。
创建 editor 时同理,可以在 createEditor 里返回一个 QProgressBar 作为编辑器(虽然进度条不适合编辑,但你可以返回一个 QSpinBox 或 QSlider,编辑完成就把值写回模型),真正做到可视化编辑。
5. 样式美化:让表格不再是默认的灰底白字
5.1 表头和网格线的样式
默认的 QTableView 长得很素,如果做的是商业项目或者个人作品,审美这关过不去。用 QSS 改表格样式的性价比很高,不需要大动代码。下面是我比较常用的样式片段,兼顾了表头和网格线的呈现效果。
QTableView { background-color: #ffffff; alternate-background-color: #f7f9fc; border: 1px solid #dfe3e8; gridline-color: #e8eaed; selection-background-color: #3478F6; selection-color: #ffffff; font-size: 13px; } QTableView::item { padding: 2px 6px; } QHeaderView::section { background-color: #f2f4f7; color: #2b2f36; padding: 8px; border-right: 1px solid #dfe3e8; border-bottom: 2px solid #cfd4db; font-weight: 600; }QSS 里注意 QTableView::item 的 padding 只对纯文本单元格生效,如果你用了委托自定义绘制,padding 就得自己在 paint 里控制。表头部分如果想做圆角,直接设置 border-radius 在某些 Qt 版本上会导致表头绘制异常,稳妥的做法是用平滑的色块拼接,或者使用 stylesheet 给 QHeaderView 的 segment 设置圆角,但不要太依赖它。
5.2 隔行变色与选中态高亮
隔行变色在数据表格里能有效提升扫描效率。开启方式是在代码里设置:
tableView->setAlternatingRowColors(true);对应的 QSS 里 alternate-background-color 会在这个开关打开后才生效。如果你不想依赖 QSS,也可以在 model 的 data() 方法里根据行号给 BackgroundRole 返回不同颜色。后者更灵活,比如你可以在某行是非活跃用户时,整行变灰;某行是警告状态时,整行淡红色。但这种方案要小心:由于模型不知道当前视图滚动位置,它对所有行都会执行颜色计算,数据量大时可能带来额外开销。
选中态的高亮我个人建议用 QSS 里的 selection-background-color,因为默认的蓝色高亮在部分深色主题里不够明显。也可以让选中整行都有背景色,但内部文字颜色保持黑色,这种效果在需要区分选中行与其他高亮状态的场景里很好用:
QTableView { selection-background-color: rgba(52, 120, 246, 0.2); selection-color: #000000; }一定要记住,QSS 中 rgba 的支持需要 Qt 开启相应编译选项,常规 Qt 默认支持,不用太担心。选中行用什么颜色,要看你的界面主题对比色,不要为了好看选择一个跟主题冲突的颜色,那样反而分散注意力。
6. 常见问题与排查技巧实录
6.1 表格不显示数据或显示空白列
绝大多数刚接触 QTableView 的开发者都会遇到“模型绑定了但界面什么都没有”。排查步骤很简单:
- 是否调用了 setModel
- model 实例是否因为栈上创建过早销毁
- 是否向模型里插入了数据
- 是否设置表头数据(只设置数据不设置表头,不会导致空白,但会默认用数字当表头)
- 是否在数据插入后又调用了某个清空操作,例如 model->clear(),清空会把表头一起删掉
经验里最尴尬的情况是:QStandardItemModel 在函数内部作为局部变量创建,函数结束就释放了,视图自然没了数据源。这个坑出现频率极高。解决方式是保证 model 的生命周期跟随视图,或者由父对象统一管理。
6.2 单元格编辑后数据没变
如果编辑完回车,界面确实更新了,但重新打开窗口又恢复原样,十有八九是数据没有写回底层存储。逻辑上 setModelData 写回模型,但模型只是内存里的容器,没有持久化,一样的道理 QStandardItemModel 的所有修改只存在于内存。要持久化就要自己监听模型数据变更信号,比如 dataChanged,然后同步到数据库或者配置文件。
connect(model, &QStandardItemModel::dataChanged, this, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) { // 根据当前编辑的单元格更新对应数据 QModelIndex index = topLeft; QVariant val = index.data(Qt::EditRole); // 写回到你的业务数据结构中 });6.3 排序后行号对不上
QTableView 开启排序后,界面上看到的行顺序和模型内部的行顺序很可能不一致。此时如果你用 view->currentIndex().row() 去数组里取业务数据,取到的可能是错误的行。经验做法是不要在排序开启后依赖行号,而是依赖 QModelIndex 对应数据里的唯一标识(比如 ID)。使用 index.data(Qt::UserRole) 拿到业务 ID,再去列表里找到对应对象。这个教训我在做文件列表时深刻体会过,表头一排序,点删除就删错对象,非常危险。
6.4 委托编辑器不弹出或无法取值
createEditor 写了却不调用,第一检查有没有设置 setItemDelegate 相关接口,第二检查是否设置了 setEditTriggers 为 NoEditTriggers,第三检查当前单元格模型的 flags 里是否有 ItemIsEditable。这三处是常见拦截点,任何一个没有到位,编辑器都不会出现。
编辑器值不更新的情况,检查 setEditorData 是否没实现里直接用了 index.data(Qt::EditRole),但模型里存的是 UserRole;或者编辑器控件没有被正确 cast。用一个 qobject_cast 调试一把,问题基本立刻定位。
6.5 大数据量加载卡顿
加载几千行数据时 QTableView 不会比 QTableWidget 好到哪里去,关键区别在加载策略。QTableView 的优势是配合自定义模型可以实现按需加载(只加载可视区数据),但 QStandardItemModel 本身还是一个全量容器。大数据场景我的做法是分页加载 + 滚动加载,或者基于自定义抽象模型实时计算值。如果你暂时不需要动态加载,至少把大段数据插入包在 beginInsertRows/endInsertRows 里,避免每一行都通知视图刷新一次,性能能提升数倍。
7. 性能优化与扩展方向
表头排序之外,有经验的人还会关注另一个细节:大量列时的初始渲染。如果你的表头有几十列,QTableView 默认会全部渲染,滚动基本正常,但如果每列都要绘制复杂委托,需要斟酌委托的绘制成本。减少渐变、避免频繁 QFontMetrics 计算,能明显降低滚动时的卡顿。另一个技巧是 setUniformRowHeights(true),告诉视图所有行高相同,这样滚动条计算可以跳跃式进行,性能大幅提升。
tableView->setUniformRowHeights(true); tableView->horizontalHeader()->setCascadingSectionResizes(true);开启这个开关的前提条件是:你的表格每行高度一致,如果有列内容高度不同(比如多行文本换行),这个优化会出错。需要按内容自适应高度的话,这个优化不能开。
响应到大数据的交互,还有一个很多人没用到的功能:滚动到指定行。比如你在另一个窗口选中某种类型,希望主表格自动滚动到对应行。使用 scrollTo 可以达到效果:
QModelIndex idx = model->index(row, 0); tableView->scrollTo(idx, QAbstractItemView::PositionAtCenter);这是非常顺滑的联动体验,比手动算滚动条位置可靠得多。
扩展方向方面,如果你把 QTableView 的路走通了,再去看 QTreeView、QColumnView、QListView 基本无障碍,因为底层全部是同一个模型/视图框架。你可以把同样的 model 挂到 QTreeView 上,瞬间把平面表格变成树形导航,数据源不用换,这是一种非常优雅的功能扩展。
另外 QTableView 也支持拖拽排序、支持从外部拖文件进来,这些无非是设置 dragDropMode、实现 dropEvent、配合 MIME 数据。我建议是:先把基础显示、委托、右键菜单这些主干功能做扎实,再逐步叠加拖拽,不要一开始就复杂度拉满,调试会很痛苦。
8. 最后分享两个实用小技巧
第一个是表头排序和过滤器组合。我常做一个搜索输入框,实时过滤表格内容。用 QSortFilterProxyModel 是最省力的方案:
// 创建代理模型,过滤后再显示到 QTableView QSortFilterProxyModel *proxy = new QSortFilterProxyModel(this); proxy->setSourceModel(model); proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); tableView->setModel(proxy);搜索框的 textChanged 信号里调用:
proxy->setFilterFixedString(text);注意排序启用时,要操作的是 proxy 作为视图模型,而不是直接对 source model 做排序。视图排序开在 proxy 上,才能既过滤又有表头排序。初学者很容易把这两个模型搞混,我在项目里也吃过亏,后来习惯取名的时候写清楚 sourceModel 和 proxyModel 的区别。
第二个技巧是数据展示时结合主题切换。如果你的应用有深色模式和浅色模式,QSS 可以对 QTableView、QHeaderView、QTableView::item 分别定义两套属性,切换时通过 qApp->setStyleSheet 生效。但注意:样式切换不能覆盖委托内部绘制的颜色,所以自定义委托里尽量用 QPalette 取色,而不是写死颜色值。这样主题切换时,委托绘制的内容也会跟着变,不用写两套代码。
我在实际项目里遇到最满意的效果是:表格数据联动数据库、右键操作完整、双击编辑、委托绘制进度条、隔行变色、搜索过滤、树形扩展全走一套模型。这套组合拳适应了很多种业务界面,维护起来也轻松,因为数据逻辑都在模型层,视图层只是表现。
如果你现在正在做一个数据密集型的桌面应用,不妨直接从 QTableView 起步,别再用 QTableWidget 将就了。开始可能多点代码,一旦框架建立起来,后面改界面、加功能会顺畅太多。像表格这种基础能力,越早把模型/视图思维吃透,你后面写的业务界面就越省心。