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 数据流向,你看一眼就明白了:

Qt 模型/视图架构数据流 模型 (Model) QAbstractItemModel 存储数据 + 提供接口 视图 (View) QTableView / QTreeView 显示数据 + 响应用户操作 委托 (Delegate) 自定义渲染 + 编辑 data() / rowCount() / columnCount() 委托渲染请求 setData() 更新数据 选择模型

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 的表格、树、列表开发对你来说就是小菜一碟。