Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

Qt 与可视化 / Qt & Visualization

1. Qt 6 与科学可视化路线 / Qt 6 and Scientific Visualization Roadmap

2. 信号槽机制深度解析 / Qt Signals and Slots in Depth

3. 对象树与内存管理 / QObject Trees and Memory Management

4. CAE开发常用核心API实战 / Essential Qt APIs for CAE Development

5. Model/View架构与CAE数据处理 / Model-View Architecture and CAE Data Processing

6. 多线程与求解器集成 / Multithreading and Solver Integration

7. CAE软件架构实战模式 / Practical Architecture Patterns for CAE Software

8. 3D可视化与交互 / Interactive 3D Visualization

9. 实战案例:从零拆解一个FEA前后处理器 / Building an FEA Pre- and Post-Processor from Scratch

本页目录

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层

专题扩展 ​

  • Model/View 正确性与性能

第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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

1.2 QModelIndex——定位数据的万能钥匙 ​

cpp
// 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,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

第2部分:QAbstractTableModel——表格数据 ​

2.1 必须实现的虚函数 ​

cpp
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;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

2.2 节点结果表格——完整实现 ​

cpp
// ===== 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;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

2.3 QSortFilterProxyModel——排序与过滤 ​

cpp
// ===== 添加排序和过滤(不改原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);  // 点击表头排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

第3部分:QAbstractItemModel——树形数据 ​

3.1 树形Model的额外要求 ​

cpp
// 树形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;
};
1
2
3
4
5
6
7
8
9
10
11
12

3.2 CAE零件树Model——完整实现 ​

cpp
// ===== 零件树节点 =====
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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

第4部分:自定义Delegate ​

4.1 QStyledItemDelegate基础 ​

cpp
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);
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

4.2 材料属性编辑器Delegate ​

cpp
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);
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

第5部分:性能优化——百万级数据 ​

5.1 QTableView vs QTableWidget ​

┌─────────────────────────────────────────────────────────────┐
│           QTableView vs QTableWidget 性能对比                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  10万行 × 8列(NodalResultModel):                          │
│                                                             │
│  QTableWidget (逐格填充):                                    │
│  ┌──────────────────────────────────────────────────┐      │
│  │ 内存: 随单元格和角色快速增长(应以目标数据实测)    │      │
│  │ 启动: ~8秒(填充所有格子)                        │      │
│  │ 滚动: 卡顿(所有格子在内存中)                    │      │
│  └──────────────────────────────────────────────────┘      │
│                                                             │
│  QTableView + QAbstractTableModel:                           │
│  ┌──────────────────────────────────────────────────┐      │
│  │ 内存: ~15MB(只有裸数据)                         │      │
│  │ 启动: ~0.2秒(不填充,只设Model)                 │      │
│  │ 滚动: 流畅(只有可见行调用 data())               │      │
│  └──────────────────────────────────────────────────┘      │
│                                                             │
│  结论:超过1000行请务必用QTableView+自定义Model              │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

5.2 canFetchMore / fetchMore —— 增量加载 ​

cpp
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;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

5.3 data() 性能——缓存计算值 ​

cpp
// ===== 直接计算 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

核心总结 ​

总结1:MVD分离的核心价值 ​

数据(Model)只负责存储和提供,View只负责显示,Delegate负责编辑和自定义渲染。一个Model可以被QTableView、QTreeView、QChart同时使用——改一处数据,所有视图自动更新。

总结2:CAE数据模型的选择 ​

数据类型ModelView
节点结果(线性表)QAbstractTableModelQTableView
零件树(层级)QAbstractItemModelQTreeView
材料列表QAbstractTableModelQTableView + ComboDelegate
色标云图自定义ModelQTableView + 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 调用与修改时序 ​

text
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 indexes
1
2
3
4
5
6
7

view 从不拥有领域数据;model 是适配器和通知协议维护者。复杂业务修改应进入 Document/Command,model 再发布精确变化。

Index 映射图 ​

text
View index (Proxy B)
 -> mapToSource
Proxy A index
 -> mapToSource
Source model index
 -> stable domain ID
1
2
3
4
5
6

排序过滤后 row 只是展示位置,不能作为永久实体 ID。

面试连续追问 ​

问:为何不能每次变化都 resetModel? 答:它使 index/selection/cache 全失效,破坏用户状态且大模型重建昂贵;局部变化应精确通知。

问:data() 可否做数据库阻塞查询? 答:不应,view 会高频同步调用;应缓存/异步预取并在结果到达时通知。

问:persistent index 的价值? 答:模型按正确结构通知时,它能随插入/移动调整并保持指向同一项;reset/删除仍会失效。

自测与答案 ​

  1. beginInsertRows 与存储修改顺序? 答:begin 后修改,完成后 end。
  2. 后台线程能直接调用 dataChanged 吗? 答:通常不能,应 queued 到 model 亲和线程提交。
  3. internalPointer 指向 vector 元素的风险? 答:vector 重分配使地址失效。

最后更新于:

Pager
上一篇4. CAE开发常用核心API实战 / Essential Qt APIs for CAE Development
下一篇6. 多线程与求解器集成 / Multithreading and Solver Integration

持续记录,持续成长

Copyright © Tidenflow