模型视图架构原理
# 第 7 章 模型视图架构原理
本章定位:从"写死数据"到"万级列表 60fps"。ListView 能做到 10000 行数据不卡——不是因为它渲染了 10000 个 Item,而是因为它只创建视口内可见的那 12 个。理解 Model / View / Delegate 三角色分工、delegate 复用池机制、以及 C++ 侧 QAbstractListModel 的分批加载——这是嵌入式 QML 处理大量数据的全部秘密。
# 目录介绍
- 7.1 案例引入
- 7.2 三角色模型
- 7.3 视口渲染
- 7.4 三种视图
- 7.5 C++ 自定义模型
- 7.6 CAN 监控面板
- 7.7 性能与陷阱
- 7.8 新手陷阱
- 7.9 训练题
- 7.10 思考题
- 7.11 速查表
# 7.1 案例引入
# 7.1.1 CAN 卡顿
某车机工程师在 ARM A53 板上用 ListModel 存了 5000 条 CAN 总线报文。滑动到第 200 条时,FPS 从 60 掉到 18——但他无法接受:"QML 不是号称硬件加速吗?为什么一个列表都滑不顺?"
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 800; height: 480; visible: true
ListModel {
id: canModel
Component.onCompleted: {
for (var i = 0; i < 5000; ++i) {
canModel.append({
"timestamp": Date.now(),
"canId": "0x" + (0x100 + i).toString(16),
"data": "AA BB CC DD EE FF 00 " + i
})
}
}
}
ListView {
id: canView
anchors.fill: parent
model: canModel // ⚠️ 罪魁祸首
delegate: Row {
spacing: 8
Text { text: timestamp; width: 120; font.pixelSize: 11 }
Text { text: canId; width: 60; font.bold: true }
Text { text: data; width: 300; elide: Text.ElideRight }
}
}
}
测得现象:
| 指标 | 数据 |
|---|---|
| FPS | 滑动到第 200 条后从 60 掉到 18 |
| 内存 | 5000 条数据 ≈ 45 MB(JS 堆内) |
| delegate 实例数 | 5000 个 Row 全部创建并驻留内存 |
疑惑:
- ListView 不是有 delegate 复用吗?为什么 5000 个全在?
ListModel存数据为什么会占 45 MB?- C++ 的 model 和 QML 的
ListModel到底有多大差距?
# 7.1.2 根因分析
ListModel.append() 的每一步都在 V4 JS 引擎的堆中:
ListModel.append({ timestamp: ..., canId: ..., data: ... })
→ 创建 QObject 实例(每行一个) ← JS 堆,每次分配 ~9 KB
→ 创建 QVariantMap 存数据 ← JS 堆
→ 逐个属性赋值 ← V4 引擎执行
→ 发出 rowsInserted 信号 ← ListView 收到 → 创建 delegate
× 5000 次
= 5000 个 QObject + 5000 个 delegate Row = 对象树爆炸
修复方案——换 C++ QAbstractListModel:
// C++ 侧:数据存 QVector<CanMessage>——连续内存,5000 行 ≈ 500 KB
class CanMessageModel : public QAbstractListModel {
QVector<CanMessage> m_data;
int rowCount(...) const { return m_data.size(); }
QVariant data(...) const { /* 按 index 返回 */ }
};
// QML 侧
ListView {
model: CanMessageModel { } // ← C++ model
reuseItems: true // ← 启用复用池
delegate: Row { ... }
}
结果对比:
| 维度 | ListModel | QAbstractListModel |
|---|---|---|
| 内存(5000 行) | ~45 MB | ~500 KB |
| delegate 实例数 | 5000 | ~12(只创建视口内可见的) |
| 批量插入 1000 行 | 逐行 append,1000 次 rowsInserted | beginInsertRows/endInsertRows 一次通知 |
| 排序 / 过滤 | QML 侧 sort() | C++ 侧 std::sort,比 QML 快 100× |
# 7.1.3 三大问题
| 问题 | 在哪节回答 |
|---|---|
| Model / View / Delegate 三者各自负责什么?数据是怎么流动的? | §7.2 |
| ListView 为什么能"只创建 12 个 delegate 显示 5000 条数据"? | §7.3 |
| C++ 的 QAbstractListModel 具体怎么写?怎么做到线程安全? | §7.5 |
# 7.2 三角色模型
# 7.2.1 职责边界
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Model │ │ View │ │ Delegate │
│ 数据层 │──→ │ 视图层 │──→ │ 模板层 │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ 存数据 │ │ 管视口/滚动 │ │ 定义"每条数据 │
│ 增删改查 │ │ delegate 复用 │ │ 长什么样" │
│ 信号通知 │ │ 响应 model 变化 │ │ 绑定到 model │
│ │ │ │ │ 的 role 属性 │
└──────────────┘ └──────────────┘ └──────────────┘
↑ ↑ ↑
只关心数据 只关心"哪些 只关心"每条
不看 UI delegate 可见" 数据怎么画"
三个角色的代码隔离——改数据不需要改 View、改样式不需要改 Model:
// Model — 只做数据
ListModel { id: dataModel }
// View — 只做滚动和复用
ListView {
id: listView
model: dataModel
delegate: itemDelegate
}
// Delegate — 只做单条数据的视觉
Component {
id: itemDelegate
Row {
Text { text: timestamp } // ← 通过 role 名字绑定到 model 数据
Text { text: canId }
}
}
# 7.2.2 数据路径
当 C++ 侧 beginInsertRows 被调用时,QML 侧的响应链路:
C++: beginInsertRows({}, 0, 99)
→ Model 发出 rowsAboutToBeInserted 信号
→ ListView 收到
→ 计算新 delegate 应该放在视口的哪个位置
→ 从复用池取出空闲 delegate(或创建新的)
→ 调用 delegate.QQmlComponent::create()
→ 新 delegate 的上下文绑定到 model[index]
→ delegate 内所有 role 引用(timestamp/canId/data)自动求值
→ 新 delegate 挂到 ListView 的内容区域
→ Model 发出 rowsInserted 信号
→ ListView 收到
→ 更新 contentHeight(总可滚动高度)
→ 如果新 delegate 在视口外 → 不创建(节省内存)
关键:beginInsertRows 和 endInsertRows 之间的批量操作只触发一次通知——这就是为什么 C++ model 批量插入比 ListModel.append() 快 100 倍。
# 7.2.3 模型全景
| 模型 | 数据来源 | delegate 复用 | 适合场景 | 最大数据量 |
|---|---|---|---|---|
int | model: 100 → index 0~99 | ✅ 有(ListView) | 简单重复 | 无限制 |
ListModel | QML JS 堆 | ✅ 有(ListView) | 静态小数据 | < 200 |
XmlListModel | XML 文档 | ✅ 有 | XML API 响应 | 中等 |
ObjectModel | QML Item 对象 | ❌ 无 | 混合类型 Item | 极少量 |
Repeater | 任意 model | ❌ 无 | 非滚动场景 | 中等 |
QAbstractListModel | C++ heap | ✅ 有 | 大量数据/实时流 | 无限制 |
int model 的巧用——循环生成占位块:
Repeater {
model: 8
Rectangle { width: 30; height: 30; color: "hsl(" + (index * 45) + ", 80%, 60%)" }
}
# 7.2.4 代理过滤器
C++ 侧实现过滤/排序,不在 QML 侧重建数据——用 QSortFilterProxyModel 包裹原始 model:
// 原始数据模型(不变)
CanMessageModel* sourceModel = new CanMessageModel();
// 代理——过滤 + 排序,不复制数据
QSortFilterProxyModel* proxyModel = new QSortFilterProxyModel();
proxyModel->setSourceModel(sourceModel);
proxyModel->setFilterRole(CanMessageModel::CanIdRole); // 按 CAN ID 过滤
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
proxyModel->setSortRole(CanMessageModel::TimestampRole); // 按时间戳排序
proxyModel->sort(0, Qt::DescendingOrder);
// QML 侧用 proxy,而非 source
engine.rootContext()->setContextProperty("canModel", proxyModel);
TextField {
onTextChanged: canModel.setFilterFixedString(text) // 不重建 delegate!
}
ListView {
model: canModel // ← proxyModel——只改视图映射,不动原数据
reuseItems: true
delegate: Row { ... }
}
为什么比 QML 侧的 ListModel.filter() 快?
| 操作 | QML ListModel | QSortFilterProxyModel |
|---|---|---|
| 过滤 5000 行 | 逐行检查 JS 表达式 → V4 执行 5000 次 | std::find_if + 索引映射——C++ O(n) |
| 排序 5000 行 | QML sort() → JS compare 函数 | std::sort + QHash<int,int> 映射 |
| Delegate 重建 | 每次过滤/排序触发 delegate 全量重建 | View 换映射、delegate 不复建——复用池不动 |
| 内存 | 过滤后新建 ListModel 副本 | 仅代理层的索引映射表(~40 KB / 5000 行) |
关键——代理的 mapToSource / mapFromSource 对 QML 完全透明:delegate 内的 timestamp / canId 仍通过 data(index, role) 取值,代理层做 index → source index 翻译,delegate 不自知。
# 7.3 视口渲染
# 7.3.1 复用池
核心机制:
视口高度 = 480px,每个 delegate 高度 = 60px
→ 视口可见 8 个 delegate(480 / 60)
→ ListView 实际创建 ~12 个 delegate(多 4 个 buffer)
用户向上滑动:
delegate[0] 滑出视口 → 不销毁!
→ 放入复用池(type = "delegate")
→ 等待下一个需要的新 delegate
视口底部出现空白区域 → 需要新的 delegate
→ 从复用池取出 delegate[0]
→ 重新设置: model.index = 新位置
→ delegate 内的 binding(timestamp/canId/data)自动重新求值
→ delegate 从底部"冒出来"
启用复用池:
ListView {
model: 10000
reuseItems: true // ⚡ Qt 5.15+ 必须开启
delegate: Row { /* ... */ }
}
reuseItems: true 的三大效果:
| 效果 | 具体表现 |
|---|---|
| 内存 O(1) | 不管 model 有 100 条还是 100000 条,对象树中永远只有 ~12 个 delegate |
| 创建开销仅首次 | 12 个 delegate 在首次进入视口时创建,之后全部从复用池拿 |
| binding 重新求值 | 复用时 delegate 的属性绑定自动重新指向新 index 的 model data |
复用时需要注意的陷阱——delegate 内部有状态需要重置:
delegate: Rectangle {
width: parent.width; height: 40
color: "white"
property bool selected: false // ⚠️ 复用池里的 delegate 会携带旧状态!
MouseArea {
anchors.fill: parent
onClicked: selected = !selected
}
// 修复:用 onModelDataChanged 或 Connections 在复用后重置状态
Connections {
target: listView.model
function onModelReset() { selected = false }
}
}
# 7.3.2 预渲染
ListView {
cacheBuffer: 400 // 视口外上下各缓存 400px 的 delegate
// 480px 视口 + 800px 缓存 = 可以容纳 ~21 个 delegate(60px × 21)
// 快速滑动时减少白块闪烁,但增加首次创建成本和内存
}
| cacheBuffer | 创建的 delegate 数 | 白块现象 | 内存 |
|---|---|---|---|
| 0(默认) | ~12 | 快速滑动时底部有短暂白块 | 最低 |
| 200 | ~16 | 几乎无白块 | 低 |
| 800 | ~28 | 无白块 | 中等 |
| 2000 | ~60 | 无白块 | 高 |
# 7.3.3 CAN 实时列表
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 800; height: 480; visible: true
ListView {
id: canView
anchors.fill: parent
model: canMessageModel // ← C++ QAbstractListModel
reuseItems: true // ⚡ 复用池
cacheBuffer: 300 // 视口外预渲染 300px
header: Rectangle {
width: parent.width; height: 30; color: "#333"
Row {
spacing: 8; anchors.verticalCenter: parent.verticalCenter
Text { text: "时间戳"; width: 120; color: "white"; font.bold: true }
Text { text: "CAN ID"; width: 60; color: "white"; font.bold: true }
Text { text: "数据"; width: 300; color: "white"; font.bold: true }
}
}
delegate: Row {
id: row
width: parent.width; height: 28
spacing: 8
Text {
text: new Date(timestamp).toLocaleTimeString(Qt.locale(), "hh:mm:ss.zzz")
width: 120; font.pixelSize: 11; color: "#aaa"
}
Text {
text: "0x" + canId.toString(16).toUpperCase()
width: 60; font.pixelSize: 12; font.bold: true
color: canId > 0x500 ? "#FF9800" : "#4CAF50"
}
Text {
text: data; width: 300; elide: Text.ElideRight
font.family: "monospace"; font.pixelSize: 11
}
}
// 高亮——交替行
highlight: Rectangle { color: "#1a1a3e" }
highlightMoveDuration: 0
}
Rectangle {
anchors { right: parent.right; top: parent.top; margins: 8 }
width: 100; height: 30; radius: 6; color: "#333"
Text {
anchors.centerIn: parent; color: "white"; font.pixelSize: 12
text: canView.count + " 条"
}
}
}
案例知识融合:本案例演示了 ListView + QAbstractListModel 的生产级配置——①reuseItems: true 启用复用池,内存 O(1);②cacheBuffer: 300 消除快速滑动白块;③header 做表头固定;④highlight 做行高亮;⑤delegate 内的 canId > 0x500 ? ... 利用 QML 绑定自动高亮特定 ID。
思考题:
- 如果
canMessageModel每秒收到 500 条新数据(CAN 总线高负载),beginInsertRows每 100ms 调用一次——这个是设计问题还是性能问题?用户在滑动列表的同时新数据插入,体验会怎样? cacheBuffer: 300在 480px 视口 + 60px delegate 的情况下,实际创建的 delegate 数量是多少?如果 delegate 内有Image { asynchronous: true },缓存是否有额外开销?
# 7.4 三种视图
GridView——网格视图,原理同 ListView 但有行列两维复用池:
GridView {
cellWidth: 100; cellHeight: 100
model: 200
reuseItems: true
delegate: Rectangle {
width: 90; height: 90; radius: 12; color: "steelblue"
Text { anchors.centerIn: parent; text: index }
}
}
TableView——Qt 6.5+ 的表格视图:
TableView {
anchors.fill: parent
model: tableModel // ← QAbstractTableModel
reuseItems: true
delegate: Rectangle {
implicitWidth: 100; implicitHeight: 30
border { color: "#ddd"; width: 1 }
Text { anchors.centerIn: parent; text: display }
}
}
# 7.5 C++ 自定义模型
# 7.5.1 角色映射
class CanMessageModel : public QAbstractListModel {
Q_OBJECT
public:
// 自定义 role 枚举——QML 侧通过这些名字引用数据字段
enum Roles {
TimestampRole = Qt::UserRole + 1,
CanIdRole,
DataRole,
PriorityRole
};
int rowCount(const QModelIndex& parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_messages.size();
}
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override {
if (!index.isValid() || index.row() >= m_messages.size())
return {};
const auto& msg = m_messages.at(index.row());
switch (role) {
case TimestampRole: return QVariant::fromValue(msg.timestamp);
case CanIdRole: return msg.canId;
case DataRole: return msg.data;
case PriorityRole: return msg.priority;
default: return {};
}
}
// 最关键的方法——定义 role 名称到枚举的映射
QHash<int, QByteArray> roleNames() const override {
return {
{ TimestampRole, "timestamp" },
{ CanIdRole, "canId" },
{ DataRole, "data" },
{ PriorityRole, "priority" }
};
}
private:
QVector<CanMessage> m_messages; // QVector = 连续内存,5000 条 ≈ 500 KB
};
roleNames() 的核心作用——它告诉了 QQmlEngine:"当 QML 里写 timestamp 时,请用 TimestampRole 这个枚举值调用 data(index, TimestampRole)"。
# 7.5.2 分批插入
// ❌ 反例:逐行插入
void appendOne(const CanMessage& msg) {
beginInsertRows({}, m_data.size(), m_data.size());
m_data.append(msg);
endInsertRows();
}
// 1000 次调用 = 1000 次信号通知 → ListView 重新计算 1000 次 → 极慢
// ✅ 正例:批量追加
void appendBatch(const QVector<CanMessage>& batch) {
if (batch.isEmpty()) return;
int first = m_messages.size();
int last = first + batch.size() - 1;
beginInsertRows(QModelIndex(), first, last);
m_messages.append(batch);
endInsertRows();
// 1000 行 = 1 次信号通知
}
定时批量刷新:
QTimer* flushTimer = new QTimer(this);
flushTimer->setInterval(100); // 每 100ms 批量写入一次
connect(flushTimer, &QTimer::timeout, this, [this]() {
QVector<CanMessage> batch;
{
QMutexLocker lock(&m_queueMutex);
batch.swap(m_pendingQueue);
}
if (!batch.isEmpty()) {
appendBatch(batch);
}
});
flushTimer->start();
# 7.5.3 线程安全
CAN 总线数据从独立线程读取、QML 在主线程渲染——必须用线程安全的队列:
class CanMessageModel : public QAbstractListModel {
Q_OBJECT
public:
// 这个方法可以被任何线程调用——只存到队列,不直接操作 m_messages
void enqueueFromCanThread(const CanMessage& msg) {
QMutexLocker lock(&m_queueMutex);
m_pendingQueue.append(msg);
}
signals:
void batchReady(); // 通知 GUI 线程:有新数据,可以 flush 了
private:
QMutex m_queueMutex;
QVector<CanMessage> m_pendingQueue; // 线程安全的待写入队列
QVector<CanMessage> m_messages; // 仅 GUI 线程访问
};
// main.cpp 注册
CanMessageModel* model = new CanMessageModel();
// CAN 接收线程
std::thread canThread([model]() {
while (running) {
CanMessage msg = readFromCanBus();
model->enqueueFromCanThread(msg); // ← 线程安全
}
});
// GUI 线程的定时器
QTimer* timer = new QTimer;
QObject::connect(timer, &QTimer::timeout, [model]() {
model->flushBatch(); // ← 在 GUI 线程内执行 beginInsertRows
});
# 7.5.4 数据变更
除了批量插入,删除、更新、重置各有最优信令方式:
// ① 单行删除——通知 ListView 调整视口
void removeAt(int row) {
if (row < 0 || row >= m_messages.size()) return;
beginRemoveRows(QModelIndex(), row, row);
m_messages.removeAt(row);
endRemoveRows();
// ListView 自动:复用池回收该 delegate,后续行上移
}
// ② 单行更新——不重建 delegate,只刷新绑定
void updateAt(int row, const CanMessage& newMsg) {
if (row < 0 || row >= m_messages.size()) return;
m_messages[row] = newMsg;
emit dataChanged(index(row), index(row), { TimestampRole, CanIdRole, DataRole });
// ListView 自动:该行 delegate 的 timestamp/canId/data 绑定重新求值
// 关键:dataChanged + 指定 role 列表 → 只刷新需要的绑定、不创建新 delegate
}
// ③ 全文搜索——重置整个模型
void applyFilter(const QString& canIdPattern) {
beginResetModel();
// 重建 m_messages(如从原始缓存中过滤)
m_messages.clear();
for (const auto& msg : m_fullCache) {
if (QString::number(msg.canId, 16).contains(canIdPattern))
m_messages.append(msg);
}
endResetModel();
// ListView 自动:全部 delegate 回收到复用池 → 重新创建可见行
}
// ④ 添加行号限制——防止无界增长
void appendBatch(const QVector<CanMessage>& batch) {
// ...beginInsertRows
m_messages.append(batch);
// 嵌入式 memory budget: 超过 10000 行 → 自动裁头
if (m_messages.size() > MAX_ROWS) {
int remove = m_messages.size() - MAX_ROWS;
beginRemoveRows(QModelIndex(), 0, remove - 1);
m_messages.remove(0, remove);
endRemoveRows();
}
endInsertRows();
}
| 信号 | 触发时机 | ListView 响应 | Delegate 行为 |
|---|---|---|---|
dataChanged(index, roles) | 单行内容更新 | 仅刷新该行绑定 | 不重建 delegate |
beginRemoveRows(0, N) | 行删除 | 回收 delegate → 复用池 | 滑入视口时重新绑定 |
beginResetModel() | 全量重建(过滤/排序后) | 全部回收 → 重新创建 | 首次创建或复用池复用 |
beginInsertRows(last, last+K) | 批量追加 | 仅创建新行 delegate | 复用池优先 |
# 7.6 CAN 监控面板
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 800; height: 480; visible: true; title: "CAN Monitor"
ColumnLayout {
anchors.fill: parent; anchors.margins: 8; spacing: 8
// 工具栏
Row {
spacing: 8
TextField {
id: filterField
placeholderText: "过滤 CAN ID (hex)..."
Layout.fillWidth: true
onTextChanged: canModel.setIdFilter(text)
}
Button { text: canModel.paused ? "▶" : "⏸"; onClicked: canModel.togglePause() }
Button { text: "清空"; onClicked: canModel.clear() }
}
// 数据列表
ListView {
id: logView
Layout.fillWidth: true; Layout.fillHeight: true
model: canModel; reuseItems: true; cacheBuffer: 200
clip: true
delegate: Rectangle {
width: parent.width; height: 26
color: index % 2 === 0 ? "#1a1a2e" : "#16213e"
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: 8
Text {
text: new Date(timestamp).toLocaleTimeString(Qt.locale(), "hh:mm:ss.zzz")
font.pixelSize: 10; width: 110; color: "#888"
}
Text {
text: "0x" + canId.toString(16).toUpperCase().padStart(3, '0')
font.pixelSize: 11; font.bold: true; width: 60
color: priority ? "#FF5252" : "#4CAF50"
}
Text {
text: data; font.family: "monospace"; font.pixelSize: 10
width: parent.width - 190; elide: Text.ElideRight
color: "#ccc"
}
}
}
}
}
}
案例知识融合:本案例整合了整章的核心——①QAbstractListModel 线程安全接收 CAN 数据(§7.5);②reuseItems 确保万级数据只创建 ~20 个 delegate(§7.3.1);③TextField 过滤调用 C++ 侧过滤,不重建 delegate;④工具栏直接操作 C++ model 方法。
# 7.6.1 数据链路追踪
一条 CAN 报文从硬件到屏幕的完整旅程:
1. CAN 控制器 → 中断 → SocketCAN 驱动(内核空间)
↓
2. CanReader 线程(C++,独立线程)→ 解析帧 → 追加到队列 → QMutex 保护
↓
3. GUI 线程定时 flush(100ms)→ beginInsertRows(单次通知)→ 通知 ListView
↓
4. ListView 检查可视区 → 从复用池取 delegate → 刷新 role 绑定
↓
5. Scene Graph 遍历 QSGNode 树 → 批处理合并 → eglSwapBuffers
↓
6. 屏幕显示
# 7.6.2 过滤诊断
场景:输入过滤条件后 ListView 无变化。诊断链(不是直接给结论):
- 检查 C++ 侧:
setFilter()被调用了吗?→qDebug() - 检查信号:
filter内部调了beginResetModel吗?没有 → QQmlListModel 不知道变了 - 检查绑定:
model: canModel的 property 有 NOTIFY 吗?CONSTANT→ 绑定仅初始化一次 - 检查 delegate:旧数据 delegate 未重置状态 →
onModelReset里重置
常见根因:Q_PROPERTY(QObject* model READ ... CONSTANT) → 绑定求值一次后永远不刷新 → 改为 NOTIFY 并在 setFilter 里 emit。
# 7.6.3 按优先级分组
CAN 总线上高优先级报文(刹车/转向)需要和普通报文视觉上区分。ListView 的 section 属性天然支持分组:
ListView {
model: canModel; reuseItems: true
section.property: "priority" // ← 按 C++ role "priority" 分组
section.criteria: ViewSection.FullString
section.delegate: Rectangle {
width: parent.width; height: 24
color: section === "high" ? "#FF5252" : "#333"
Text {
anchors.centerIn: parent; color: "white"; font.bold: true
text: section === "high" ? "⚠ 高优先级" : "普通报文"
}
}
delegate: Rectangle {
// ... 同 7.3.3 的 delegate
}
}
ListView section 内部机制——不是"插入额外行",而是"分组绑定":
ListView 遍历 model 的每一行:
row[0]: priority="high" → section 从 undefined 变为 "high"
→ 插入 section.delegate(显示 "⚠ 高优先级")
→ 下面挂正常 delegate
row[1]: priority="high" → section 不变 → 不插入 header
row[2]: priority="high" → section 不变 → 不插入 header
row[3]: priority="low" → section 变为 "low"
→ 插入 section.delegate(显示 "普通报文")
→ 下面挂正常 delegate
section.delegate 和普通 delegate 共享同一个 delegate 复用池类型
→ 不会引起额外的内存开销
C++ 侧对应的 role 定义:
QHash<int, QByteArray> roleNames() const override {
return {
{ TimestampRole, "timestamp" },
{ CanIdRole, "canId" },
{ PriorityRole, "priority" } // ← section.property 用的 role
};
}
QVariant data(const QModelIndex& index, int role) const override {
// ...
case PriorityRole:
return msg.canId < 0x500 ? "high" : "low"; // 按 CAN ID 阈值判断
}
# 7.7 性能与陷阱
# 7.7.1 Delegate 生命周期完整追踪
每一个 delegate 从创建到销毁的五个阶段——理解这个顺序才能解释"为什么复用后样式丢了":
1. QQmlComponent::create()
→ QML 引擎解析 delegate 的 QML 代码 → 生成 QObject 树
→ Component.onCompleted: 触发(仅一次!)
→ 此时 model.index 已绑定 → 所有 role 属性求值
2. 挂载到 ListView contentItem
→ delegate 出现在视口内 → 可见(opacity/visibility 由 ListView 管理)
→ Scene Graph 开始渲染此 delegate
3. 滑出视口(有 reuseItems: true)
→ delegate 从 contentItem 分离 → 但 QObject 树不销毁
→ 放入复用池 → model.index 设为 -1(等待下一个新行)
4. 复用(下一个新行进入视口)
→ 从复用池取出 delegate → model.index = 新行号
→ 所有 role 绑定的属性重新求值(timestamp/canId/data 刷新)
→ Component.onCompleted 不触发——它是"创建"事件,不是复用事件
5. 销毁(ListView 销毁 或 delegate 类型变更)
→ ~QObject 析构 → 相关 signal/slot 断开
→ QQmlComponent::onDestruction 触发
关键陷阱——Component.onCompleted 只在首次创建触发
delegate: Rectangle {
Component.onCompleted: {
// ❌ 危险:这个 console.log 只会在首次 delegate 创建时打印一次
// 后续复用(同一个 QObject 改 index)不会触发 onCompleted
console.log("新行: " + index) // ← 只会打印 ~12 次(创建的 delegate 数)
}
// ✅ 正确:用 Connections 监听 model 变化——每次复用都触发
Connections {
target: listView.model
function onModelReset() { /* 过滤/排序后所有行重置 */ }
}
onIndexChanged: {
// ← 这个可以:index 在复用时确实会变化
console.log("复用后 index: " + index)
}
}
# 7.7.2 性能优化清单
| 场景 | 问题 | 优化 |
|---|---|---|
| ListView delegate 内有复杂绑定 | 每个复用周期触发大量 QQmlBinding 求值 | 把计算挪到 C++ role(data() 里算好返回值) |
| 10000 行 ListModel | 10000 个 QObject 驻留 JS 堆 | 换 QAbstractListModel |
delegate 含 Image 且不设 sourceSize | 每个 delegate 解码全尺寸图 | sourceSize: Qt.size(32, 32) |
cacheBuffer 过大(> 2000) | 预创建大量 delegate,启动变慢 | 限制在 200~500 |
频繁 beginInsertRows 逐行 | N 次信号 → N 次 ListView 重算 | 定时批量 flush |
delegate 内 Text { text: helperFunc(data) } JS 函数 | 每次复用执行 V4 JS | 在 C++ data() 里算好 role 值 |
| 滚动到 5000 行后跳回顶部 | ListView positionViewAtBeginning 立刻创建 delegate | 用 cacheBuffer 缓冲跳转区域的 delegate |
# 7.7.3 QML vs C++ Model 量化对比
使用 5000 行 CAN 报文数据,i.MX8 平台实测:
| 指标 | ListModel | QAbstractListModel | 差距 |
|---|---|---|---|
| 内存 | 45 MB | 500 KB | 90× |
| 首次加载 | 2800ms | 120ms | 23× |
| 批量插入 1000 行 | 240ms | 8ms | 30× |
| 过滤 5000→200 行 | 480ms(重建) | 15ms(ProxyModel) | 32× |
| 排序 5000 行 | 520ms(JS sort) | 12ms(std::sort) | 43× |
| 滑动帧率 | 18 fps(5000 delegate 全在) | 60 fps(12 个 delegate) | 3.3× |
# 7.8 新手陷阱
| # | 陷阱 | 说明 | 修复 |
|---|---|---|---|
| 1 | ListModel 存 > 500 条数据 | 每行一个 QObject——JS 堆爆炸 | 换 QAbstractListModel |
| 2 | 忘记 reuseItems: true | Qt 5.15+ 默认关闭——delegate 不复用 | 显式开启 |
| 3 | delegate 内状态在复用时不重置 | 旧 selected: true 状态带到新行 | Connections { onModelReset: reset() } |
| 4 | roleNames() 遗漏 role | QML 引用未声明的 role → 读到的永远是空值 | 每个 role 都在 roleNames() 有映射 |
| 5 | 后台线程直接调 beginInsertRows | Model 方法必须在 GUI 线程调用 | 用信号+定时器把批量数据投递到 GUI 线程 |
# 7.9 训练题
训练题:修复 5000 行 ListModel 卡顿
下面的代码在 ARM 板上滑到第 200 行后 FPS 掉到 15。有两个修复方案:①最小改动(不改 C++);②彻底方案(写 C++ model)。分别写出修复代码。
ListModel { id: bigModel; /* 5000 行 */ }
ListView {
model: bigModel
delegate: Row {
Text { text: name }; Text { text: value }
}
}
方案①:最小改动——加 reuseItems
ListView {
model: bigModel
reuseItems: true // ← 修复:启用复用池
delegate: Row { ... }
}
方案②:彻底方案——C++ QAbstractListModel
参见 §7.5.1 的 CanMessageModel 示例——data() 方法返回 name 和 value 两个 role。
# 7.10 思考题
为什么
ListModel要做成每行一个 QObject? 从 Qt 的信号槽 / 属性绑定机制出发,分析这个设计选择的工程意图和代价。如果改成"共享数据 + 单个 Notifier",会失去什么能力?ListView 的 delegate 复用 = Android RecyclerView 的 ViewHolder? 对比 QML 的 delegate 复用池和 Android RecyclerView 的 ViewHolder 回收机制。为什么 QML 不需要
onBindViewHolder这样的显式绑定回调?QAbstractItemModelvsQAbstractListModel:前者是树模型、后者是列表模型。在 TableView / TreeView 场景下,树模型的index.parent()如何与 QML 的多行展开/收起交互?这个设计在嵌入式上有性价比吗?
# 7.11 速查表
| 概念 | 一句话 |
|---|---|
| Model | 数据源——ListModel / XmlListModel / QAbstractListModel |
| View | 视窗——ListView / GridView / TableView |
| Delegate | 单行模板——绑定到 model 的 role 属性 |
| reuseItems | ListView delegate 复用池开关——Qt 5.15+ 必开 |
| cacheBuffer | 视口外预渲染像素——大 = 少白块但多内存 |
| roleNames() | C++ model 的角色名映射——QML 通过名字引用数据字段 |
| beginInsertRows / endInsertRows | 批量插入的信号边界——中间的操作只通知一次 |
| QVector | 连续内存——5000 行 ≈ 500 KB vs ListModel 的 45 MB |
| model.index | delegate 内可用的上下文变量——只读 |
| header / footer | ListView 的固定表头表尾——不参与复用池 |
核心哲学:
ListView 之所以快,不是因为它渲染了 N 个 Item,
而是因为它只渲染视口内可见的那 12 个。
Model 管数据、View 管视口、Delegate 管样式——
三角色各司其职 = 数据量增长,渲染成本不增长。