Qt 设计模式:MVC 与 Model/View 架构
系列:Qt / VTK 设计模式 · Qt 11/11
参考:Qt 6 Model/View Programming、QAbstractItemModel
引子
如果要在界面表格中展示数据库里十万行记录,传统的做法是为每个单元格 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) 与 数据变更通知:
- 虚拟化查询:当
QTableView滚动时,它只调用可见范围内单元格的data(index, role)函数。 - 多角色支持(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 单元格,极大节省内存 |
最佳实践与踩坑避坑
- 增删改数据必须严格成对调用信号指示通知:
- 增加数据:
beginInsertRows()与endInsertRows()。 - 删除数据:
beginRemoveRows()与endRemoveRows()。 - 重置整个模型:
beginResetModel()与endResetModel()。 - 坑点:如果不调用这些函数直接修改底层数据结构, View 将无法同步刷新,甚至直接产生
QModelIndex错位崩溃!
- 增加数据:
- 切忌在
data()函数中执行耗时或 IO 操作:data()在界面重绘和鼠标划过时会被极高频调用。如果在data()里读取数据库或进行复杂计算,界面会立刻卡顿。耗时数据必须预先缓存或异步加载。
QModelIndex短期有效:QModelIndex是临时生成的索引句柄,只要 Model 结构发生增删改,之前的QModelIndex就会失效,绝对不要长期全局保存QModelIndex(若需长期保存,请使用QPersistentModelIndex)。
重点与注意
重点:Qt Model/View 架构通过虚拟化索取 (
data()) 彻底解耦了 UI 控件数量与底层数据规模。
重点:修改 Model 内部数据时,必须严格使用beginInsertRows/endInsertRows或dataChanged信号,确保 View 同步刷新。
注意:QModelIndex随 Model 结构变动而失效,不能长期持久化持有;长期引用需用QPersistentModelIndex。
小结
Qt Model/View/Delegate 架构是 C++ 处理复杂海量数据的经典范式,也是构建高性能界面系统的基石。
延伸阅读