Qt 设计模式:代理模式
系列:Qt / VTK 设计模式 · Qt 08/11
参考:Qt 6 QSortFilterProxyModel、QGraphicsProxyWidget
引子
源数据模型存有 10000 行记录,视图 A 只需要按姓名升序排列,视图 B 只需要显示“状态=进行中”的记录,而原始数据必须保持不变——此时如果在源 Model 中直接增删改数据,会导致所有关联视图混乱甚至破坏物理数据。
解决此问题的经典设计模式是代理模式(Proxy Pattern):为真实对象提供一个代理/替身,以控制对该对象的访问。在 Qt 中,代理模式通过隔离“数据存储”与“展示控制”,实现了极高的解耦与多视图复用。
核心痛点与 GoF 代理结构
痛点场景:直接修改源模型的缺陷
cpp
// 坏方案:直接在源 Model 中删除或重排数据
// 破坏了原始数据的存留,导致其他依赖该源 Model 的视图全部受影响- 数据污染:筛选删除数据会导致物理数据丢失。
- 多视图冲突:不同 View 无法同时展示不同排序或过滤视角。
- 耦合度过高:将展示层的逻辑(排序、筛选、格式转换)硬编码在存储层。
GoF 代理模式结构映射
| GoF 代理角色 | Qt 对应实现 | 职责说明 |
|---|---|---|
| 抽象主体 (Subject) | QAbstractItemModel | 定义了模型的基本数据访问接口 (data, index, rowCount) |
| 真实主体 (RealSubject) | QStandardItemModel / QSqlTableModel | 真实存储与管理物理数据的源模型 |
| 代理 (Proxy) | QSortFilterProxyModel / QIdentityProxyModel | 持有源模型引用,拦截 View 的访问并注入排序/过滤/映射逻辑 |
| 客户端 (Client) | QTableView / QListView | 仅与代理交互,感知不到底层原始索引的变化 |
底层原理:索引映射机制 (Index Mapping)
QSortFilterProxyModel 的核心机制是在内部维护一张 代理索引 ↔ 源索引 的双向映射表。
关键映射函数
mapToSource(proxyIndex):将代理模型中的QModelIndex转换为源模型的物理QModelIndex(写回/修改源数据时必须使用)。mapFromSource(sourceIndex):将源模型的物理QModelIndex转换为代理模型的QModelIndex。invalidateFilter():当过滤规则变化时,通知代理重新生成索引映射表。
Qt 代理体系落点
Qt 在不同层级广泛应用了代理模式:
1. 数据模型代理(Item Model Proxies)
QSortFilterProxyModel:最常用的代理模型,支持按列排序(Sorting)与按正则/文本过滤(Filtering)。QIdentityProxyModel:1:1 保持索引映射不变的代理模型,常用于重写data()格式化显示(如日期转换、数值加单位),而不改动数据与顺序。
2. UI 图形组件代理(Graphics Proxies)
QGraphicsProxyWidget:将传统的QWidget(如QPushButton、QLineEdit)包装代理为QGraphicsItem,使其能无缝嵌入到QGraphicsScene2D 图形场景中并支持 3D 旋转与坐标变换。
完整代码示例
1. 基础排序与过滤代理使用
cpp
#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// 1. 构建源数据模型 (RealSubject)
QStandardItemModel sourceModel(4, 2);
sourceModel.setHeaderData(0, Qt::Horizontal, "Name");
sourceModel.setHeaderData(1, Qt::Horizontal, "Status");
sourceModel.setItem(0, 0, new QStandardItem("Charlie"));
sourceModel.setItem(0, 1, new QStandardItem("Pending"));
sourceModel.setItem(1, 0, new QStandardItem("Alice"));
sourceModel.setItem(1, 1, new QStandardItem("Active"));
sourceModel.setItem(2, 0, new QStandardItem("Bob"));
sourceModel.setItem(2, 1, new QStandardItem("Active"));
// 2. 构建代理模型 (Proxy)
QSortFilterProxyModel proxyModel;
proxyModel.setSourceModel(&sourceModel);
// 设置按第 1 列 (Status) 过滤 "Active" 文本
proxyModel.setFilterKeyColumn(1);
proxyModel.setFilterFixedString("Active");
// 3. 设置代理排序(按第 0 列 Name 升序)
proxyModel.sort(0, Qt::AscendingOrder);
// 4. 将代理模型绑定到视图 (Client)
QTableView view;
view.setModel(&proxyModel);
view.show();
return app.exec();
}2. 自定义高级代理与安全的索引转换写回
cpp
// 自定义多条件过滤代理
class CustomFilterProxyModel : public QSortFilterProxyModel {
Q_OBJECT
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override {
QModelIndex statusIdx = sourceModel()->index(sourceRow, 1, sourceParent);
QString status = sourceModel()->data(statusIdx).toString();
// 仅保留状态为 Active 且有效的数据
return status == "Active";
}
};
// 安全更新源数据示例函数
void updateSourceDataFromView(QAbstractItemView* view, QSortFilterProxyModel* proxy, QAbstractItemModel* source) {
QModelIndex currentProxyIndex = view->currentIndex();
if (!currentProxyIndex.isValid()) return;
// 关键动作:将代理索引映射为源物理索引
QModelIndex realSourceIndex = proxy->mapToSource(currentProxyIndex);
// 修改底层物理数据,改动会自动通过代理同步更新到视图
source->setData(realSourceIndex, "Updated Value", Qt::EditRole);
}易混淆模式对比
| 设计模式 | 核心目的 | 接口关系 | Qt 代表类 |
|---|---|---|---|
| 代理模式 (Proxy) | 控制访问与隔离(排序、筛选、延迟加载) | 与主体保持相同/兼容接口 | QSortFilterProxyModel |
| 装饰模式 (Decorator) | 动态增强职责与功能 | 与主体保持相同接口 | QIdentityProxyModel (修饰 data) |
| 适配器模式 (Adapter) | 接口转换与兼容 | 将旧接口转换为新接口 | QAbstractItemModel 适配底层 C++ 数据结构 |
最佳实践与踩坑避坑
- 绝对禁止直接用代理索引修改源模型:
- 视图中双击选中的
QModelIndex属于代理模型。直接拿着它传给sourceModel->setData()会导致修改错行甚至内存越界崩溃,必须显式调用proxyModel->mapToSource()转换。
- 视图中双击选中的
- 多视图共享同一源模型:
- 可以为一个
QStandardItemModel同时创建多个独立的QSortFilterProxyModel绑定到不同 View 上,实现多视角实时联动。
- 可以为一个
- 动态过滤更新与性能:
- 修改自定义过滤条件后,必须主动调用
invalidateFilter()强制触发代理更新。 - 对几十万级的大数据集进行字符串正则过滤时,应优化
filterAcceptsRow内的数据获取逻辑,避免频繁调用data()。
- 修改自定义过滤条件后,必须主动调用
重点与注意
重点:
QSortFilterProxyModel在不破坏源 Model 的前提下实现排序与筛选,是结构型代理模式的经典落地。
重点:View 交互返回的QModelIndex属于代理 Model;修改底层源数据必须先使用mapToSource()完成映射转换。
注意:QIdentityProxyModel保持 1:1 索引不变,适合做单纯的显示格式化(如单位换算、色彩渲染重写)。
注意:过滤条件变更后务必调用invalidateFilter(),否则界面数据无法及时同步更新。
小结
Qt 代理模式通过 QSortFilterProxyModel 为代表的组件,在数据存储与视图渲染之间建立起一层透明的映射桥梁,解耦了展现控制与物理存储,是 C++ 大型 GUI 界面架构不可或缺的设计基石。
延伸阅读