Model/View架构与CAE数据处理 / Model-View Architecture and CAE Data Processing
📅 创建时间:2026-07-13 🏷️ 标签:#Qt #ModelView #QAbstractItemModel #QTreeView #QTableView #CAE 📚 前置知识:Qt Cpp Overview、Core Api Essentials
📋 本章目标
- 理解Qt Model/View/Delegate设计模式的分离哲学
- 掌握QAbstractTableModel在CAE结果数据中的实战应用
- 掌握QAbstractItemModel构建零件树的完整技巧
- 掌握自定义Delegate渲染色标和材料属性
- 理解百万级数据行的性能优化策略
- 能够为CAE数据类型设计正确的Model层
专题扩展
第1部分:Model/View/Delegate设计哲学
1.1 为什么需要MVD——与MFC的对比
┌─────────────────────────────────────────────────────────────┐
│ Qt Model/View vs 传统控件设计 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 传统方法(如MFC的CListCtrl): │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 控件内同时持有"数据"和"显示逻辑" │ │
│ │ 数据和视图耦合 → 无法复用数据,只能复制 │ │
│ │ 百万行数据 → 必须全部加载到控件 → 内存爆炸 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Qt Model/View: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ┌─────────┐ ┌─────────┐ ┌───────────┐ │ │
│ │ │ Model │ ←→ │ View │ → │ Delegate │ │ │
│ │ │ (数据) │ │ (展示) │ │ (编辑渲染)│ │ │
│ │ └─────────┘ └─────────┘ └───────────┘ │ │
│ │ │ │
│ │ 一个Model可以给多个View用(树+表+图表同时显示) │ │
│ │ 数据按需加载(fetchMore),不必要的不进内存 │ │
│ │ 自定义Delegate控制每个单元格的渲染和编辑 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1.2 QModelIndex——定位数据的万能钥匙
// QModelIndex 用 (row, column, parent) 三元组定位任何数据
// 对于表格:parent = invalid (没有层级)
// 对于树:parent 指向父节点的 QModelIndex
// 创建索引
QModelIndex idx = model->index(row, column, parent);
// 从索引提取数据
QVariant value = idx.data(Qt::DisplayRole); // 显示文本
QVariant fg = idx.data(Qt::ForegroundRole); // 前景色
QVariant bg = idx.data(Qt::BackgroundRole); // 背景色
// 自定义角色(CAE专用)
enum CaeDataRole {
NodeIdRole = Qt::UserRole + 1,
CoordinateRole, // QVector3D
StressTensorRole, // 6个分量的数组
MaterialIdRole,
IsSelectedRole,
};第2部分:QAbstractTableModel——表格数据
2.1 必须实现的虚函数
class CaeTableModel : public QAbstractTableModel {
Q_OBJECT
public:
// ★ 必须实现
int rowCount(const QModelIndex& parent = {}) const override;
int columnCount(const QModelIndex& parent = {}) const override;
QVariant data(const QModelIndex& idx, int role) const override;
// ★ 推荐实现(表头)
QVariant headerData(int section, Qt::Orientation o, int role) const override;
// ★ 可选:可编辑
bool setData(const QModelIndex& idx, const QVariant& val, int role) override;
Qt::ItemFlags flags(const QModelIndex& idx) const override;
};2.2 节点结果表格——完整实现
// ===== CAE节点位移/应力结果表 =====
class NodalResultModel : public QAbstractTableModel {
Q_OBJECT
public:
enum Column {
Col_NodeId, Col_X, Col_Y, Col_Z,
Col_DispX, Col_DispY, Col_DispZ,
Col_StressVM, // von Mises 等效应力
COLUMN_COUNT
};
struct NodeResult {
int nodeId;
double x, y, z; // 坐标
double dx, dy, dz; // 位移
double vonMisesStress; // 等效应力
};
explicit NodalResultModel(QObject* parent = nullptr)
: QAbstractTableModel(parent) {}
// ★ 核心方法:设置数据
void setResults(const QVector<NodeResult>& results) {
beginResetModel();
m_results = results;
endResetModel(); // 通知所有View全部刷新
}
// ★ 必须实现:行数/列数
int rowCount(const QModelIndex& = {}) const override {
return m_results.size();
}
int columnCount(const QModelIndex& = {}) const override {
return COLUMN_COUNT;
}
// ★ 必须实现:数据访问(最频繁调用,注意性能)
QVariant data(const QModelIndex& idx, int role) const override {
if (!idx.isValid()) return {};
const auto& node = m_results[idx.row()];
if (role == Qt::DisplayRole) {
switch (idx.column()) {
case Col_NodeId: return node.nodeId;
case Col_X: return node.x;
case Col_Y: return node.y;
case Col_Z: return node.z;
case Col_DispX: return node.dx;
case Col_DispY: return node.dy;
case Col_DispZ: return node.dz;
case Col_StressVM: return node.vonMisesStress;
}
}
// ★ 自定义角色:色标映射
if (role == Qt::BackgroundRole && idx.column() == Col_StressVM) {
return colorForStress(node.vonMisesStress);
}
// ★ 自定义角色:数值格式化
if (role == Qt::TextAlignmentRole) {
return Qt::AlignRight + Qt::AlignVCenter;
}
return {};
}
// ★ 表头
QVariant headerData(int section, Qt::Orientation o, int role) const override {
if (o != Qt::Horizontal || role != Qt::DisplayRole) return {};
static const char* headers[] = {
"Node ID", "X", "Y", "Z",
"DX", "DY", "DZ", "Von Mises"
};
return headers[section];
}
private:
QColor colorForStress(double stress) const {
// 从蓝(低)到红(高)的渐变
double t = qBound(0.0, (stress - m_stressMin) / (m_stressMax - m_stressMin), 1.0);
return QColor::fromRgbF(t, 0.2, 1.0 - t); // 简化版
}
QVector<NodeResult> m_results;
double m_stressMin = 0, m_stressMax = 100;
};2.3 QSortFilterProxyModel——排序与过滤
// ===== 添加排序和过滤(不改原Model)=====
QTableView* nodeTableView = new QTableView();
// 原始Model
auto* sourceModel = new NodalResultModel(this);
sourceModel->setResults(allNodes);
// 代理Model——在source和view之间
auto* proxy = new QSortFilterProxyModel(this);
proxy->setSourceModel(sourceModel);
proxy->setFilterKeyColumn(NodalResultModel::Col_NodeId); // 按NodeID过滤
proxy->setFilterRegularExpression(QRegularExpression("^1")); // ID以1开头的
// View 连到 proxy(不是source)
nodeTableView->setModel(proxy);
nodeTableView->setSortingEnabled(true); // 点击表头排序第3部分:QAbstractItemModel——树形数据
3.1 树形Model的额外要求
// 树形Model比表格Model多实现两个方法:
class PartTreeModel : public QAbstractItemModel {
// ★ 必须额外实现:
QModelIndex index(int row, int column,
const QModelIndex& parent) const override;
QModelIndex parent(const QModelIndex& child) const override;
// ★ 表格Model的方法同样需要(但parent参与定位)
int rowCount(const QModelIndex& parent) const override;
int columnCount(const QModelIndex& parent) const override;
QVariant data(const QModelIndex& idx, int role) const override;
};3.2 CAE零件树Model——完整实现
// ===== 零件树节点 =====
class TreeNode {
public:
explicit TreeNode(const QString& name, TreeNode* parent = nullptr)
: m_name(name), m_parent(parent) {}
~TreeNode() { qDeleteAll(m_children); }
void appendChild(TreeNode* child) {
child->m_parent = this;
m_children.append(child);
}
TreeNode* child(int row) const { return m_children.value(row); }
int childCount() const { return m_children.size(); }
int row() const {
return m_parent ? m_parent->m_children.indexOf(const_cast<TreeNode*>(this)) : 0;
}
TreeNode* parent() const { return m_parent; }
QString name() const { return m_name; }
private:
QString m_name;
TreeNode* m_parent;
QList<TreeNode*> m_children;
};
// ===== PartTreeModel =====
class PartTreeModel : public QAbstractItemModel {
Q_OBJECT
public:
explicit PartTreeModel(QObject* parent = nullptr)
: QAbstractItemModel(parent) {
m_root = new TreeNode("Root");
}
~PartTreeModel() { delete m_root; }
// ★ 核心:从指针创建QModelIndex
QModelIndex index(int row, int col,
const QModelIndex& parent = {}) const override {
if (!hasIndex(row, col, parent)) return {};
TreeNode* parentNode = parent.isValid()
? static_cast<TreeNode*>(parent.internalPointer())
: m_root;
TreeNode* childNode = parentNode->child(row);
return childNode ? createIndex(row, col, childNode) : QModelIndex();
}
// ★ 核心:从QModelIndex找回父节点
QModelIndex parent(const QModelIndex& child) const override {
if (!child.isValid()) return {};
TreeNode* childNode = static_cast<TreeNode*>(child.internalPointer());
TreeNode* parentNode = childNode->parent();
if (!parentNode || parentNode == m_root) return {};
return createIndex(parentNode->row(), 0, parentNode);
}
int rowCount(const QModelIndex& parent = {}) const override {
TreeNode* node = parent.isValid()
? static_cast<TreeNode*>(parent.internalPointer()) : m_root;
return node->childCount();
}
int columnCount(const QModelIndex&) const override { return 1; }
QVariant data(const QModelIndex& idx, int role) const override {
if (!idx.isValid() || role != Qt::DisplayRole) return {};
auto* node = static_cast<TreeNode*>(idx.internalPointer());
return node->name();
}
// ★ 添加/删除节点的方法
void addPart(const QString& name, const QModelIndex& parent = {}) {
TreeNode* parentNode = parent.isValid()
? static_cast<TreeNode*>(parent.internalPointer()) : m_root;
int row = parentNode->childCount();
beginInsertRows(parent, row, row); // ★ 必须先通知View
parentNode->appendChild(new TreeNode(name));
endInsertRows(); // ★ 然后插入数据
}
private:
TreeNode* m_root;
};
// ===== 使用 =====
auto* model = new PartTreeModel(this);
// 构建层次
QModelIndex fuselage = model->index(0, 0); // 先插入Fuselage
model->addPart("Fuselage");
model->addPart("MainBody", model->index(0, 0));
model->addPart("OuterSkin", model->index(0, 0, model->index(0, 0)));
QTreeView* treeView = new QTreeView();
treeView->setModel(model);
treeView->expandAll();第4部分:自定义Delegate
4.1 QStyledItemDelegate基础
class StressColorDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
void paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& idx) const override
{
// ★ 先让基类画标准内容
QStyledItemDelegate::paint(painter, option, idx);
// ★ 在应力列加色标条
if (idx.column() != NodalResultModel::Col_StressVM) return;
QStyleOptionViewItem opt = option;
initStyleOption(&opt, idx);
double stress = idx.data(Qt::DisplayRole).toDouble();
double t = qBound(0.0, (stress - 0) / 500.0, 1.0); // 归一化
// 画一个彩色条在单元格右侧
QRect barRect = opt.rect.adjusted(
opt.rect.width() - 30, 4, -4, -4);
painter->fillRect(barRect,
QColor::fromRgbF(t, 0.2, 1.0 - t));
}
QSize sizeHint(const QStyleOptionViewItem& option,
const QModelIndex& idx) const override
{
return QStyledItemDelegate::sizeHint(option, idx) + QSize(40, 0);
}
};4.2 材料属性编辑器Delegate
class MaterialComboDelegate : public QStyledItemDelegate {
public:
QWidget* createEditor(QWidget* parent,
const QStyleOptionViewItem&,
const QModelIndex&) const override
{
QComboBox* combo = new QComboBox(parent);
combo->addItems({"Aluminum 6061", "Steel A36", "Titanium Ti6Al4V",
"CFRP", "ABS Plastic"});
combo->setEditable(true); // 允许自定义
return combo;
}
void setEditorData(QWidget* editor,
const QModelIndex& idx) const override
{
QString current = idx.data(Qt::EditRole).toString();
QComboBox* combo = static_cast<QComboBox*>(editor);
combo->setCurrentText(current);
}
void setModelData(QWidget* editor, QAbstractItemModel* model,
const QModelIndex& idx) const override
{
QComboBox* combo = static_cast<QComboBox*>(editor);
model->setData(idx, combo->currentText(), Qt::EditRole);
}
};第5部分:性能优化——百万级数据
5.1 QTableView vs QTableWidget
┌─────────────────────────────────────────────────────────────┐
│ QTableView vs QTableWidget 性能对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 10万行 × 8列(NodalResultModel): │
│ │
│ QTableWidget (逐格填充): │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 内存: 随单元格和角色快速增长(应以目标数据实测) │ │
│ │ 启动: ~8秒(填充所有格子) │ │
│ │ 滚动: 卡顿(所有格子在内存中) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ QTableView + QAbstractTableModel: │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 内存: ~15MB(只有裸数据) │ │
│ │ 启动: ~0.2秒(不填充,只设Model) │ │
│ │ 滚动: 流畅(只有可见行调用 data()) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 结论:超过1000行请务必用QTableView+自定义Model │
│ │
└─────────────────────────────────────────────────────────────┘5.2 canFetchMore / fetchMore —— 增量加载
class LargeNodalModel : public QAbstractTableModel {
public:
// ★ 告诉View"还有更多数据"
bool canFetchMore(const QModelIndex&) const override {
return m_loadedCount < m_totalCount;
}
// ★ View请求加载更多时调用
void fetchMore(const QModelIndex&) override {
int remainder = m_totalCount - m_loadedCount;
int toFetch = qMin(remainder, 50000); // 每次加载5万
beginInsertRows({}, m_loadedCount, m_loadedCount + toFetch - 1);
// 从文件/数据库加载下一批数据到 m_results
loadChunk(m_loadedCount, toFetch);
m_loadedCount += toFetch;
endInsertRows();
}
int rowCount(const QModelIndex&) const override {
return m_loadedCount; // 只返回已加载的
}
private:
int m_loadedCount = 0;
int m_totalCount;
};5.3 data() 性能——缓存计算值
// ===== 直接计算 vs 缓存(当data()被频繁调用时)=====
// ❌ 慢:每次data()都重新计算应力色标
QVariant data(const QModelIndex& idx, int role) const override {
if (role == Qt::BackgroundRole) {
const auto& r = m_results[idx.row()];
double stress = computeVonMises(r.stressTensor); // 每次都算!
return colorForStress(stress);
}
// ...
}
// ✅ 快:预计算并缓存
void setResults(const QVector<NodeResult>& results) {
beginResetModel();
m_results = results;
m_cachedColors.resize(results.size());
for (int i = 0; i < results.size(); i++) {
m_cachedColors[i] =
colorForStress(computeVonMises(results[i].stressTensor));
}
endResetModel();
}
QVariant data(const QModelIndex& idx, int role) const override {
if (role == Qt::BackgroundRole) {
return m_cachedColors[idx.row()]; // O(1)直接返回
}
// ...
}核心总结
总结1:MVD分离的核心价值
数据(Model)只负责存储和提供,View只负责显示,Delegate负责编辑和自定义渲染。一个Model可以被QTableView、QTreeView、QChart同时使用——改一处数据,所有视图自动更新。
总结2:CAE数据模型的选择
| 数据类型 | Model | View |
|---|---|---|
| 节点结果(线性表) | QAbstractTableModel | QTableView |
| 零件树(层级) | QAbstractItemModel | QTreeView |
| 材料列表 | QAbstractTableModel | QTableView + ComboDelegate |
| 色标云图 | 自定义Model | QTableView + ColorDelegate |
总结3:性能铁律
数据量较大、需要多视图或按需加载时,优先
QTableView+ 自定义 Model;不存在统一的 1000 行硬阈值,应以角色数量、单元复杂度和实测结果判断。 百万级数据应评估虚拟化、fetchMore/canFetchMore、分页和缓存;不要仅因为 View 不创建百万个控件就忽略底层数据与data()调用成本。
章节测试
测试1:Model vs Widget
QTableView + QAbstractTableModel 相比 QTableWidget 的优势是? A. 更美观的UI B. 数据与视图分离,百万行数据内存占用低,滚动流畅 C. 更多的内置单元格样式 D. 自动支持排序和过滤
测试2:树形Model
QAbstractItemModel(树形)相比 QAbstractTableModel(表格)额外需要实现哪两个方法? A. rowCount() 和 columnCount() B. data() 和 setData() C. index() 和 parent() D. canFetchMore() 和 fetchMore()
测试3:Delegate
QStyledItemDelegate 的主要用途是? A. 代替Model存储数据 B. 自定义单元格的渲染和编辑行为 C. 提高QTableView的滚动性能 D. 管理Model和View之间的信号连接
参考答案
测试1答案
答案:B。QTableView+Model分离了数据和视图:Model只存裸数据(每节点几十字节),View按需调用data()获取可见行。QTableWidget每格分配一个Widget item,十万行=十万个堆对象。
测试2答案
答案:C。index(row, col, parent)从行列和父节点创建索引;parent(child)从子节点找回父节点——这是树形结构遍历所必需的,表格不需要。
测试3答案
答案:B。Delegate的paint()控制渲染(色标、图标、进度条),createEditor()+setModelData()控制编辑(combo、spin、color picker)。
相关笔记
- Core Api Essentials - QVariant(data()返回的核心类型)
- Qt Cpp Overview - Qt模块总览
- Cae Architecture Patterns - CAE软件架构(Model/View在其中的位置)
下一步学习
- [ ] 阅读 05 - 多线程与求解器集成
- [ ] 阅读 06 - CAE软件架构实战模式
- [ ] 用QAbstractTableModel包装你自己的CAE数据并显示在QTableView中
学习状态:🟡 开始学习
Model/View 调用与修改时序
View asks rowCount/index/data
-> Model reads domain snapshot/state
User edits
-> delegate/editor calls setData
-> Model validates command
-> begin/modify/end or dataChanged
-> View/proxy re-query affected indexesview 从不拥有领域数据;model 是适配器和通知协议维护者。复杂业务修改应进入 Document/Command,model 再发布精确变化。
Index 映射图
View index (Proxy B)
-> mapToSource
Proxy A index
-> mapToSource
Source model index
-> stable domain ID排序过滤后 row 只是展示位置,不能作为永久实体 ID。
面试连续追问
问:为何不能每次变化都 resetModel? 答:它使 index/selection/cache 全失效,破坏用户状态且大模型重建昂贵;局部变化应精确通知。
问:data() 可否做数据库阻塞查询? 答:不应,view 会高频同步调用;应缓存/异步预取并在结果到达时通知。
问:persistent index 的价值? 答:模型按正确结构通知时,它能随插入/移动调整并保持指向同一项;reset/删除仍会失效。
自测与答案
- beginInsertRows 与存储修改顺序? 答:begin 后修改,完成后 end。
- 后台线程能直接调用 dataChanged 吗? 答:通常不能,应 queued 到 model 亲和线程提交。
- internalPointer 指向 vector 元素的风险? 答:vector 重分配使地址失效。