Skip to content

Qt 设计模式:MVC 与 Model/View 架构

系列:Qt / VTK 设计模式 · Qt 11/11
参考:Qt 6 Model/View ProgrammingQAbstractItemModel


引子

如果要在界面表格中展示数据库里十万行记录,传统的做法是为每个单元格 new 一个 QTableWidgetItem 控件——这将产生上百万个 GUI 对象,直接导致内存暴塌与界面卡死。

Qt 提出的解决方案是 Model/View/Delegate 架构(经典 MVC 的变体):将物理数据存储(Model)、视图绘制(View)与交互编辑控制(Delegate)彻底分离。视图只在滚动渲染时向模型索取“当前可见区域”的数据,实现了极致的内存虚拟化与万人级海量数据流畅展示。


核心痛点与经典 MVC 映射

痛点场景:数据与 UI 强绑定的后果

cpp
// 坏方案:Widget 一体化(如 QTableWidget)强行保存数据
for (int r = 0; r < 100000; ++r) {
  tableWidget->setItem(r, 0, new QTableWidgetItem("Data")); // O(N) 巨量内存开销
}
  • 内存开销爆炸:数据与 UI 控件绑定,无谓消耗 GUI 渲染资源。
  • 无法多视图共享:同一份物理数据无法同时在树状图、列表图、表格图中灵活展示。

经典 MVC 与 Qt Model/View/Delegate 映射

传统 GoF MVC 角色Qt 架构实现职责说明
模型 (Model)QAbstractItemModel / QAbstractTableModel管理物理数据,提供统一的层级索引 (QModelIndex) 访问协议
视图 (View)QTableView / QListView / QTreeView负责界面布局与可见区域的虚拟化渲染(不持久化保存数据项)
控制器 (Controller) / 代理QStyledItemDelegate + QItemSelectionModel负责单元格的自定义渲染、创建编辑器控件以及管理选中状态

底层原理:虚拟化渲染与信号机制

Model/View 架构的核心在于 按需索取(Lazy Rendering)数据变更通知

  1. 虚拟化查询:当 QTableView 滚动时,它只调用可见范围内单元格的 data(index, role) 函数。
  2. 多角色支持(Roles):同一个 QModelIndex 可以根据不同 Qt::ItemDataRole 提供不同视角的属性:
    • Qt::DisplayRole:显示的文本字符串。
    • Qt::DecorationRole:图标/颜色。
    • Qt::ToolTipRole:悬停提示信息。
    • Qt::UserRole:底层绑定的自定义 C++ 业务对象/指针。

完整代码示例

1. 自定义高性能 QAbstractTableModel 实现

cpp
#include <QApplication>
#include <QTableView>
#include <QAbstractTableModel>
#include <QVector>
#include <QColor>

struct Employee {
  int id;
  QString name;
  QString department;
};

// 1. 实现自定义 Model
class EmployeeTableModel : public QAbstractTableModel {
  Q_OBJECT

public:
  explicit EmployeeTableModel(QObject* parent = nullptr) : QAbstractTableModel(parent) {}

  void setEmployees(const QVector<Employee>& employees) {
    beginResetModel();
    m_employees = employees;
    endResetModel();
  }

  // 必须实现的三个核心纯虚函数:
  int rowCount(const QModelIndex& parent = QModelIndex()) const override {
    if (parent.isValid()) return 0;
    return m_employees.size();
  }

  int columnCount(const QModelIndex& parent = QModelIndex()) const override {
    if (parent.isValid()) return 0;
    return 3; // ID, Name, Department
  }

  QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override {
    if (!index.isValid() || index.row() >= m_employees.size()) return QVariant();

    const auto& emp = m_employees.at(index.row());

    // 针对不同角色返回不同的渲染数据
    if (role == Qt::DisplayRole || role == Qt::EditRole) {
      switch (index.column()) {
        case 0: return emp.id;
        case 1: return emp.name;
        case 2: return emp.department;
      }
    } else if (role == Qt::BackgroundRole) {
      if (emp.department == "R&D") return QColor(230, 247, 255); // 研发部背景高亮
    }

    return QVariant();
  }

  // 表头数据
  QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override {
    if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
      switch (section) {
        case 0: return "ID";
        case 1: return "Name";
        case 2: return "Department";
      }
    }
    return QAbstractTableModel::headerData(section, orientation, role);
  }

  // 动态追加数据的正确规范写法
  void addEmployee(const Employee& emp) {
    int newRow = m_employees.size();
    beginInsertRows(QModelIndex(), newRow, newRow); // 通知 View 准备增加行
    m_employees.append(emp);
    endInsertRows(); // 通知 View 增加完成,局部刷新
  }

private:
  QVector<Employee> m_employees;
};

int main(int argc, char *argv[]) {
  QApplication app(argc, argv);

  EmployeeTableModel model;
  QVector<Employee> list = {
    { 101, "Alice", "R&D" },
    { 102, "Bob", "HR" },
    { 103, "Charlie", "R&D" }
  };
  model.setEmployees(list);

  QTableView view;
  view.setModel(&model);
  view.resize(400, 200);
  view.show();

  // 动态增加一行
  model.addEmployee({ 104, "David", "Finance" });

  return app.exec();
}
#include "main.moc"

易混淆概念对比

框架组件适用场景内存与性能特点
QTableWidget / QListWidget简单静态界面,几百行以内小数据一体化保存:每个单元格持有 QTableWidgetItem 对象,内存开销大
QTableView + QAbstractItemModel海量数据展示(数万行级)、多 View 共享虚拟化按需渲染:不保存物理 UI 单元格,极大节省内存

最佳实践与踩坑避坑

  1. 增删改数据必须严格成对调用信号指示通知
    • 增加数据:beginInsertRows()endInsertRows()
    • 删除数据:beginRemoveRows()endRemoveRows()
    • 重置整个模型:beginResetModel()endResetModel()
    • 坑点:如果不调用这些函数直接修改底层数据结构, View 将无法同步刷新,甚至直接产生 QModelIndex 错位崩溃!
  2. 切忌在 data() 函数中执行耗时或 IO 操作
    • data() 在界面重绘和鼠标划过时会被极高频调用。如果在 data() 里读取数据库或进行复杂计算,界面会立刻卡顿。耗时数据必须预先缓存或异步加载。
  3. QModelIndex 短期有效
    • QModelIndex 是临时生成的索引句柄,只要 Model 结构发生增删改,之前的 QModelIndex 就会失效,绝对不要长期全局保存 QModelIndex(若需长期保存,请使用 QPersistentModelIndex)。

重点与注意

重点:Qt Model/View 架构通过虚拟化索取 (data()) 彻底解耦了 UI 控件数量与底层数据规模。
重点:修改 Model 内部数据时,必须严格使用 beginInsertRows / endInsertRowsdataChanged 信号,确保 View 同步刷新。
注意QModelIndex 随 Model 结构变动而失效,不能长期持久化持有;长期引用需用 QPersistentModelIndex


小结

Qt Model/View/Delegate 架构是 C++ 处理复杂海量数据的经典范式,也是构建高性能界面系统的基石。

延伸阅读

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