编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

      • QML基础入门
      • 嵌入式GUI技术全景
      • QML引擎与渲染原理
      • QML语法与类型系统
      • 属性绑定与响应式原理
      • 可视元素与布局原理
      • 事件处理与传播机制
      • 模型视图架构原理
        • 7.1 案例引入
          • 7.1.1 CAN 卡顿
          • 7.1.2 根因分析
          • 7.1.3 三大问题
        • 7.2 三角色模型
          • 7.2.1 职责边界
          • 7.2.2 数据路径
          • 7.2.3 模型全景
          • 7.2.4 代理过滤器
        • 7.3 视口渲染
          • 7.3.1 复用池
          • 7.3.2 预渲染
          • 7.3.3 CAN 实时列表
        • 7.4 三种视图
        • 7.5 C++ 自定义模型
          • 7.5.1 角色映射
          • 7.5.2 分批插入
          • 7.5.3 线程安全
          • 7.5.4 数据变更
        • 7.6 CAN 监控面板
          • 7.6.1 数据链路追踪
          • 7.6.2 过滤诊断
          • 7.6.3 按优先级分组
        • 7.7 性能与陷阱
          • 7.7.1 Delegate 生命周期完整追踪
          • 7.7.2 性能优化清单
          • 7.7.3 QML vs C++ Model 量化对比
        • 7.8 新手陷阱
        • 7.9 训练题
        • 7.10 思考题
        • 7.11 速查表
      • 动画与状态机原理
      • Canvas与自定义渲染
      • QML与C++集成原理
      • 自定义SceneGraph节点
      • 交叉编译与部署
      • 嵌入式渲染后端
      • 性能优化与真机调试
    • QT核心库实践

    • Linux系统编程

    • 综合项目实战

  • IoT智能硬件开发

  • Apps
  • Linux应用开发
  • QML基础入门
杨充
2025-06-24
目录

模型视图架构原理

# 第 7 章 模型视图架构原理

本章定位:从"写死数据"到"万级列表 60fps"。ListView 能做到 10000 行数据不卡——不是因为它渲染了 10000 个 Item,而是因为它只创建视口内可见的那 12 个。理解 Model / View / Delegate 三角色分工、delegate 复用池机制、以及 C++ 侧 QAbstractListModel 的分批加载——这是嵌入式 QML 处理大量数据的全部秘密。

# 目录介绍

  • 7.1 案例引入
    • 7.1.1 CAN 卡顿
    • 7.1.2 根因分析
    • 7.1.3 三大问题
  • 7.2 三角色模型
    • 7.2.1 职责边界
    • 7.2.2 数据路径
    • 7.2.3 模型全景
  • 7.3 视口渲染
    • 7.3.1 复用池
    • 7.3.2 预渲染
    • 7.3.3 CAN 实时列表
  • 7.4 三种视图
  • 7.5 C++ 自定义模型
    • 7.5.1 角色映射
    • 7.5.2 分批插入
    • 7.5.3 线程安全
  • 7.6 CAN 监控面板
    • 7.6.1 数据链路追踪
    • 7.6.2 过滤诊断
  • 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。

思考题:

  1. 如果 canMessageModel 每秒收到 500 条新数据(CAN 总线高负载),beginInsertRows 每 100ms 调用一次——这个是设计问题还是性能问题?用户在滑动列表的同时新数据插入,体验会怎样?
  2. 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 无变化。诊断链(不是直接给结论):

  1. 检查 C++ 侧:setFilter() 被调用了吗?→ qDebug()
  2. 检查信号:filter 内部调了 beginResetModel 吗?没有 → QQmlListModel 不知道变了
  3. 检查绑定:model: canModel 的 property 有 NOTIFY 吗?CONSTANT → 绑定仅初始化一次
  4. 检查 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 思考题

  1. 为什么 ListModel 要做成每行一个 QObject? 从 Qt 的信号槽 / 属性绑定机制出发,分析这个设计选择的工程意图和代价。如果改成"共享数据 + 单个 Notifier",会失去什么能力?

  2. ListView 的 delegate 复用 = Android RecyclerView 的 ViewHolder? 对比 QML 的 delegate 复用池和 Android RecyclerView 的 ViewHolder 回收机制。为什么 QML 不需要 onBindViewHolder 这样的显式绑定回调?

  3. QAbstractItemModel vs QAbstractListModel:前者是树模型、后者是列表模型。在 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 管样式——
三角色各司其职 = 数据量增长,渲染成本不增长。

下一篇:08. 动画与状态机原理 (opens new window)

上次更新: 2026/07/12, 19:39:51
事件处理与传播机制
动画与状态机原理

← 事件处理与传播机制 动画与状态机原理→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式