14. 模型/视图架构:MVC模式、QAbstractItemModel、QStandardItemModel、QSortFilterProxyModel、自定义委托
好,咱们今天聊一个真正让 Qt 开发上档次的东西——模型/视图架构。说实话,我刚开始接触 Qt 那会儿,觉得这玩意儿太绕了。明明一个 QTableWidget 就能搞定的事,干嘛非要搞什么 model、view、delegate?直到后来我在一个项目中要处理 10 万行实时数据,QTableWidget 直接卡成幻灯片……嗯,从那以后,我再也不敢小看 MVC 了。
14.1 什么是 MVC 模式?
MVC 全称是 Model-View-Controller,说白了就是把数据、显示、交互这三件事拆开干。在 Qt 里,我们通常叫它模型/视图架构,Controller 的角色被委托(Delegate)和选择模型(SelectionModel)分担了。
你想想看,如果所有代码都揉在一个 QTableWidget 里,数据一变就要刷新整个表格,性能能好吗?MVC 的核心思想就是:数据存一份,视图可以有多个,互不干扰。
核心三要素:
- 模型(Model):负责存取数据,提供接口给视图查询。比如 QAbstractItemModel 就是所有模型的爸爸。
- 视图(View):负责把模型里的数据画出来。QTableView、QTreeView、QListView 都是视图。
- 委托(Delegate):负责编辑和渲染细节。比如你想让某一列显示成进度条,就得靠自定义委托。
下面这张图是我自己总结的 MVC 数据流向,你看一眼就明白了:
14.2 QAbstractItemModel——一切模型的根基
QAbstractItemModel 是一个抽象类,你不能直接用它,必须继承它。它定义了模型必须实现的接口:
rowCount():返回行数columnCount():返回列数data():返回指定索引的数据setData():设置指定索引的数据index():创建模型索引parent():返回父索引(树形结构用)
我个人习惯是:如果数据量不大(几千行以内),直接用 QStandardItemModel 就够了。但如果你要对接数据库、实时数据流,或者数据量上百万,那就得自己写一个 QAbstractItemModel 子类。
小技巧: 自定义模型时,记得重写 headerData(),否则表头会显示成 "1, 2, 3...",看着很业余。
14.3 QStandardItemModel——开箱即用的模型
QStandardItemModel 是 QAbstractItemModel 的一个现成实现。它内部用 QStandardItem 来存储每个单元格的数据。用法非常简单:
// 创建一个 5 行 3 列的模型
QStandardItemModel *model = new QStandardItemModel(5, 3, this);
// 设置表头
model->setHeaderData(0, Qt::Horizontal, "姓名");
model->setHeaderData(1, Qt::Horizontal, "年龄");
model->setHeaderData(2, Qt::Horizontal, "城市");
// 填充数据
model->setItem(0, 0, new QStandardItem("张三"));
model->setItem(0, 1, new QStandardItem("28"));
model->setItem(0, 2, new QStandardItem("北京"));
// 绑定到视图
QTableView *view = new QTableView(this);
view->setModel(model);
你看,就这么几行代码,一个表格就出来了。但注意:QStandardItemModel 把所有数据都存在内存里。我曾经在一个项目里用它加载了 50 万行日志,结果内存直接飙到 2GB……后来老老实实改成了自定义模型。
避坑指南: 我曾经在循环里用 setItem() 插入大量数据时,忘记调用 beginInsertRows() 和 endInsertRows(),结果视图一直不刷新。记住:批量插入数据前,一定要通知视图。
14.4 QSortFilterProxyModel——排序和过滤的利器
这个类太实用了。它本身不存数据,而是作为一个"代理"夹在模型和视图之间。你可以对它设置排序规则、过滤条件,而原始数据纹丝不动。
为什么需要它?你想想看,如果用户想按年龄排序,你总不能把原始数据重新排一遍吧?用 QSortFilterProxyModel 就优雅多了:
// 创建代理模型
QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel(this);
proxyModel->setSourceModel(model); // 绑定原始模型
proxyModel->setFilterKeyColumn(1); // 按第1列过滤
proxyModel->setFilterRegularExpression(QRegularExpression("2[0-9]")); // 过滤出20-29岁
// 视图绑定代理模型
QTableView *view = new QTableView(this);
view->setModel(proxyModel);
view->setSortingEnabled(true); // 允许点击表头排序
这里有个坑:视图的索引和原始模型的索引不一样。如果你通过视图获取了一个 QModelIndex,想拿到原始数据,必须调用 proxyModel->mapToSource(index)。我刚开始就老忘这步,结果取出来的数据全是错的。
常用功能一览:
| 方法 | 作用 |
|---|---|
| setFilterKeyColumn() | 设置按哪一列过滤 |
| setFilterRegularExpression() | 设置正则过滤条件 |
| setSortRole() | 设置排序依据的角色(默认 Qt::DisplayRole) |
| setDynamicSortFilter() | 设置为 true 时,数据变化自动重新排序/过滤 |
14.5 自定义委托——让表格不再千篇一律
默认的 QTableView 只能显示文本,太单调了。如果你想在某一列显示进度条、复选框、下拉框,甚至自定义控件,就得写委托。
自定义委托需要继承 QStyledItemDelegate(推荐)或 QItemDelegate,然后重写以下几个方法:
paint():自定义绘制逻辑createEditor():创建编辑控件setEditorData():把模型数据设置到编辑器setModelData():把编辑器数据写回模型updateEditorGeometry():设置编辑器位置和大小
举个例子,我想让"进度"这一列显示成进度条:
class ProgressDelegate : public QStyledItemDelegate
{
public:
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override
{
// 获取进度值(0-100)
int progress = index.data(Qt::DisplayRole).toInt();
// 绘制背景
QStyleOptionProgressBar progressBarOption;
progressBarOption.rect = option.rect.adjusted(4, 4, -4, -4);
progressBarOption.minimum = 0;
progressBarOption.maximum = 100;
progressBarOption.progress = progress;
progressBarOption.text = QString("%1%").arg(progress);
progressBarOption.textVisible = true;
QApplication::style()->drawControl(
QStyle::CE_ProgressBar, &progressBarOption, painter);
}
};
// 使用委托
view->setItemDelegateForColumn(2, new ProgressDelegate(this));
你看,这样第2列就变成进度条了,而且完全不影响其他列。我曾在项目里用自定义委托做了一个"操作"列,里面放了编辑和删除按钮,用户体验直接拉满。
个人经验: 如果你只是想让某一列显示复选框,不用写委托。直接用 model->setData(index, Qt::Checked, Qt::CheckStateRole) 就行,视图会自动渲染成复选框。
14.6 综合示例:一个可排序、可过滤、带进度条的任务列表
咱们把今天学的串起来,做一个任务管理表格:
// 1. 创建模型
QStandardItemModel *model = new QStandardItemModel(0, 3, this);
model->setHeaderData(0, Qt::Horizontal, "任务名称");
model->setHeaderData(1, Qt::Horizontal, "负责人");
model->setHeaderData(2, Qt::Horizontal, "进度");
// 2. 填充数据
model->appendRow({
new QStandardItem("登录模块"),
new QStandardItem("张三"),
new QStandardItem("80") // 进度 80%
});
model->appendRow({
new QStandardItem("支付接口"),
new QStandardItem("李四"),
new QStandardItem("45")
});
// 3. 创建代理模型(支持排序和过滤)
QSortFilterProxyModel *proxy = new QSortFilterProxyModel(this);
proxy->setSourceModel(model);
proxy->setFilterKeyColumn(0); // 按任务名称过滤
// 4. 创建视图
QTableView *view = new QTableView(this);
view->setModel(proxy);
view->setSortingEnabled(true);
// 5. 应用自定义委托(进度列)
view->setItemDelegateForColumn(2, new ProgressDelegate(this));
// 6. 添加搜索框(过滤功能)
QLineEdit *searchBox = new QLineEdit(this);
searchBox->setPlaceholderText("搜索任务...");
connect(searchBox, &QLineEdit::textChanged, proxy,
&QSortFilterProxyModel::setFilterFixedString);
这个例子虽然简单,但已经涵盖了模型/视图架构的核心用法。你可以在搜索框里输入"登录",表格会自动过滤出相关任务;点击表头可以排序;进度列显示的是进度条而不是纯数字。
注意: 如果你在代理模型上调用 setData(),数据会通过代理自动写入原始模型。但如果你直接操作原始模型,记得调用 proxyModel->invalidate() 刷新代理的排序/过滤结果。
14.7 总结
模型/视图架构是 Qt 里最值得花时间掌握的设计模式之一。它让数据和显示解耦,代码更清晰,性能更好。我个人建议:
- 小项目用 QStandardItemModel,省事
- 大项目自己写 QAbstractItemModel 子类,可控
- 排序过滤一律用 QSortFilterProxyModel,别自己写排序算法
- 自定义委托能让界面瞬间高大上,但别过度设计
嗯,今天就到这儿。记住一句话:模型是灵魂,视图是皮囊,委托是化妆师。把这三者的关系理清了,Qt 的表格、树、列表开发对你来说就是小菜一碟。