Skip to content

Qt 设计模式:代理模式

系列:Qt / VTK 设计模式 · Qt 08/11
参考:Qt 6 QSortFilterProxyModelQGraphicsProxyWidget


引子

源数据模型存有 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(如 QPushButtonQLineEdit)包装代理为 QGraphicsItem,使其能无缝嵌入到 QGraphicsScene 2D 图形场景中并支持 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++ 数据结构

最佳实践与踩坑避坑

  1. 绝对禁止直接用代理索引修改源模型
    • 视图中双击选中的 QModelIndex 属于代理模型。直接拿着它传给 sourceModel->setData() 会导致修改错行甚至内存越界崩溃,必须显式调用 proxyModel->mapToSource() 转换
  2. 多视图共享同一源模型
    • 可以为一个 QStandardItemModel 同时创建多个独立的 QSortFilterProxyModel 绑定到不同 View 上,实现多视角实时联动。
  3. 动态过滤更新与性能
    • 修改自定义过滤条件后,必须主动调用 invalidateFilter() 强制触发代理更新。
    • 对几十万级的大数据集进行字符串正则过滤时,应优化 filterAcceptsRow 内的数据获取逻辑,避免频繁调用 data()

重点与注意

重点QSortFilterProxyModel不破坏源 Model 的前提下实现排序与筛选,是结构型代理模式的经典落地。
重点:View 交互返回的 QModelIndex 属于代理 Model;修改底层源数据必须先使用 mapToSource() 完成映射转换。
注意QIdentityProxyModel 保持 1:1 索引不变,适合做单纯的显示格式化(如单位换算、色彩渲染重写)。
注意:过滤条件变更后务必调用 invalidateFilter(),否则界面数据无法及时同步更新。


小结

Qt 代理模式通过 QSortFilterProxyModel 为代表的组件,在数据存储与视图渲染之间建立起一层透明的映射桥梁,解耦了展现控制与物理存储,是 C++ 大型 GUI 界面架构不可或缺的设计基石。

延伸阅读

基于 VitePress 强力驱动 | 记录技术与生活