编程进阶网 编程进阶网
首页
  • 在线工具
  • 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语法与类型系统
      • 属性绑定与响应式原理
      • 可视元素与布局原理
      • 事件处理与传播机制
      • 模型视图架构原理
      • 动画与状态机原理
      • Canvas与自定义渲染
        • 9.1 案例引入
          • 9.1.1 心电图掉帧
          • 9.1.2 根因分析
          • 9.1.3 三大问题
        • 9.2 Canvas 引擎
          • 9.2.1 API 全景
          • 9.2.2 FBO 缓冲
          • 9.2.3 性能与缓存
          • 9.2.4 频谱波形
        • 9.3 Shape 矢量路径
          • 9.3.1 矢量渲染
          • 9.3.2 Shape 对比
        • 9.4 Painted 命令式
          • 9.4.1 QPainter 路径
          • 9.4.2 Widget 迁移
        • 9.5 自定义节点
          • 9.5.1 零拷贝路径
          • 9.5.2 顶点缓冲
          • 9.5.3 脏标同步
        • 9.6 实时心电
        • 9.7 路径选型
        • 9.8 新手陷阱
        • 9.9 训练题
        • 9.10 速查表
      • QML与C++集成原理
      • 自定义SceneGraph节点
      • 交叉编译与部署
      • 嵌入式渲染后端
      • 性能优化与真机调试
    • QT核心库实践

    • Linux系统编程

    • 综合项目实战

  • IoT智能硬件开发

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

Canvas与自定义渲染

# 第 9 章 Canvas 与自定义渲染

本章定位:当标准控件不够用时。前面 8 章教你用 Rectangle/Text/ListView 这些 QML 标准控件——它们底层走 Scene Graph 批处理、性能很好。但现在你遇到一个实时心电波形、一个复杂路径图表、或一个需要逐像素操作的渲染场景——标准控件做不到了。本章讲三条自定义渲染路径:Canvas 2D 绘制引擎、QQuickPaintedItem 命令式 QPainter、以及 QQuickItem + QSGNode 零拷贝 GPU 渲染——它们的原理、性能差距与选择策略。

# 目录介绍

  • 9.1 案例引入
    • 9.1.1 心电图掉帧
    • 9.1.2 根因分析
    • 9.1.3 三大问题
  • 9.2 Canvas 引擎
    • 9.2.1 API 全景
    • 9.2.2 FBO 缓冲
    • 9.2.3 性能与缓存
    • 9.2.4 频谱波形
  • 9.3 Shape 矢量路径
    • 9.3.1 矢量渲染
    • 9.3.2 Shape 对比
  • 9.4 Painted 命令式
    • 9.4.1 QPainter 路径
    • 9.4.2 Widget 迁移
  • 9.5 自定义节点
    • 9.5.1 零拷贝路径
    • 9.5.2 顶点缓冲
    • 9.5.3 脏标同步
  • 9.6 实时心电
  • 9.7 路径选型
  • 9.8 新手陷阱
  • 9.9 训练题
  • 9.10 速查表

# 9.1 案例引入

# 9.1.1 心电图掉帧

某医疗设备工程师用 QML Canvas 画实时心电波形——200Hz 采样率的数据流。在 x86 桌面跑得很流畅,部署到 ARM Cortex-A53 板上后波形从 30fps 骤降到 5fps:

import QtQuick

Canvas {
    id: ekgCanvas
    width: 600; height: 300
    property var dataPoints: []
    property int maxPoints: 500

    function pushSample(value) {
        dataPoints.push(value)
        if (dataPoints.length > maxPoints) dataPoints.shift()
        requestPaint()                     // ⚠️ 每 5ms 一次 → Canvas 重绘 → 爆炸
    }

    onPaint: {
        var ctx = getContext("2d")
        ctx.clearRect(0, 0, width, height)    // ← 每次重绘先清空
        ctx.strokeStyle = "#00ff00"
        ctx.lineWidth = 2
        ctx.beginPath()
        for (var i = 0; i < dataPoints.length; i++) {   // ← 500 个点到 JS 循环
            var x = i * width / maxPoints
            var y = height / 2 - dataPoints[i] * 100
            if (i === 0) ctx.moveTo(x, y)
            else         ctx.lineTo(x, y)
        }
        ctx.stroke()
    }
}

测得现象:

指标 x86 桌面 ARM A53
波形刷新率 30 fps 5 fps
CPU 占用 15% 95%+
每帧耗时 ~2ms ~180ms
JS 循环开销 V4 JIT 优化后很低 V4 JIT 在 ARM 上弱很多

疑惑:

  • Canvas 不是硬件加速的吗?为什么比 Rectangle 慢这么多?
  • requestPaint() 到底走了什么路径?和 Scene Graph 批处理有关吗?
  • 有没有不走 JS 循环的绘制方案?

# 9.1.2 根因分析

每次 requestPaint() 触发 Canvas 完整重绘流程:

requestPaint() → Canvas 标记为 dirty
  → Render Thread Sync 阶段:
    ① Canvas::updatePaintNode()
       → 创建一个 FBO(Frame Buffer Object,离屏缓冲)
       → 调用 V4 引擎执行 onPaint 里的 JS 代码 ← 在 GUI 线程!
       → V4 执行 Context2D 命令:
           beginPath / moveTo / lineTo × 500 / stroke
         → 生成 500 个顶点的路径
         → CPU 光栅化到 FBO
       → FBO → 纹理上传(glTexImage2D) ← 又慢又占带宽
    ② 纹理作为 QSGTextureMaterial 挂到 QSGGeometryNode
    ③ Scene Graph 渲染这个纹理

关键:整个 onPaint 里的 JS + CPU 光栅化都在 GUI 线程的 Sync 阶段执行!
     这意味着 Scene Graph 的 Sync 阶段从 1ms 膨胀到 180ms。

修复方案——换 QQuickItem + QSGNode:

  • 数据在 C++ 侧直接写入 QSGGeometry 的顶点缓冲
  • 不经过 V4 JS、不经过 FBO → 纹理上传
  • Scene Graph 批处理直接渲染这些顶点 → 零拷贝路径
  • 200Hz 数据流 → 60fps 波形刷新

# 9.1.3 三大问题

问题 在哪节回答
Canvas 的 requestPaint() 到底走了什么渲染路径?为什么慢? §9.2
QQuickPaintedItem 和 Canvas 的性能差别在哪?什么时候选前者? §9.4
零拷贝 QSGNode 路径具体怎么写?200Hz 数据不丢帧? §9.5 + §9.6

读完本章你会把 ARM 板上的心电波形从 5fps 优化到 60fps,并亲手写一个 QQuickItem + QSGNode 零拷贝渲染节点。


# 9.2 Canvas 引擎

# 9.2.1 API 全景

QML Canvas 的 API 与 HTML5 Canvas 几乎一致——Context2D 提供 2D 绘图的完整接口:

Canvas {
    width: 400; height: 300
    id: canvas

    onPaint: {
        var ctx = getContext("2d")

        // ───── 填充与描边 ─────
        ctx.fillStyle = "steelblue"
        ctx.fillRect(20, 20, 100, 50)
        ctx.strokeStyle = "red"
        ctx.lineWidth = 2
        ctx.strokeRect(150, 20, 100, 50)

        // ───── 路径绘制 ─────
        ctx.beginPath()
        ctx.moveTo(50, 100)
        ctx.lineTo(100, 200)
        ctx.lineTo(200, 150)
        ctx.closePath()              // 闭合路径
        ctx.fillStyle = "#4CAF50"
        ctx.fill()                   // 填充闭合区域

        // ───── 圆形与弧线 ─────
        ctx.beginPath()
        ctx.arc(300, 100, 40, 0, Math.PI * 2)
        ctx.strokeStyle = "orange"
        ctx.stroke()

        // ───── 文字 ─────
        ctx.font = "16px sans-serif"
        ctx.fillStyle = "black"
        ctx.fillText("Hello Canvas", 20, 280)

        // ───── 变换 ─────
        ctx.save()
        ctx.translate(300, 250)
        ctx.rotate(Math.PI / 6)
        ctx.fillStyle = "purple"
        ctx.fillRect(-30, -15, 60, 30)
        ctx.restore()                // 恢复变换矩阵

        // ───── 图片 ─────
        ctx.drawImage("qrc:/icon.png", 10, 10)           // 简单绘制
        ctx.drawImage("qrc:/sprite.png", 0,0,64,64, 100,100,64,64)  // 裁剪区域
    }

    // 手动触发重绘
    property real data: 0
    onDataChanged: requestPaint()
}

Context2D 的状态机机制——save() / restore() 维护一个状态栈:

ctx.save()     → 当前变换矩阵 + 描边样式 + 填充样式 + 字体 全部入栈
ctx.restore()  → 弹出栈顶 ← 回退到 save() 之前的状态

这个机制让嵌套变换(如刻度盘上的指针旋转后回归正常坐标)变得简单。

# 9.2.2 FBO 缓冲

Canvas 每次 onPaint 执行的完整链路:

GUI Thread (在 Sync 阶段被 Render Thread 短暂阻塞):
  ① QQmlEngine 调用 V4 引擎 → 执行 onPaint 内的 JS 代码
  ② V4 执行 Context2D 指令(路径、填充、文字...)
  ③ CPU 光栅化到 FBO(GPixel 逐像素计算)
  ④ FBO 内容 → glTexImage2D → GPU 纹理(显存)
  
Render Thread:
  ⑤ 纹理作为 QSGTextureMaterial 挂到 QSGGeometryNode
  ⑥ Scene Graph 批处理渲染此节点
  ⑦ eglSwapBuffers

FBO 开销的量化对比:

操作 标准 QML 控件 Canvas
数据到渲染 QML 属性 → Scene Graph 顶点绑定 JS for 循环 → Context2D → CPU 光栅化
GPU 路径 直接:QSGGeometryNode 间接:FBO → 纹理 → QSGGeometryNode
CPU 参与度 几乎为零 100% 参与 FBO 光栅化
批处理 ✅ 全自动 ❌ 独立 FBO → 无法批处理

requestPaint() 的两种模式:

模式 触发方式 重绘范围 性能
requestPaint() 异步,下一帧 Sync 前合并 整个 Canvas 多次调用合并为一次
requestPaint(rect) 指定脏区域 仅 rect 区域 小范围更新更快
// 仅重绘变化部分——减小 FBO 光栅化面积
onDataChanged: canvas.requestPaint(Qt.rect(200, 0, 100, 300))

# 9.2.3 性能与缓存

Canvas 的性能临界点:

场景 帧率 建议
静态图表(柱状图、饼图) 按需刷新 ✅ Canvas 首选
低频动画(1-2fps) 2fps 稳定 ✅ Canvas 可用
中频更新(10fps) 10fps → CPU 60% ⚠️ 简化 onPaint 逻辑
高频更新(30fps+) 30fps → CPU 95% ❌ 换 QQuickItem + QSGNode

Canvas 缓存策略:

Canvas {
    renderStrategy: Canvas.Cooperative  // 默认:与 Scene Graph 协同
    // renderStrategy: Canvas.Immediate  // 立即渲染——阻塞性,仅调试用
    // renderStrategy: Canvas.Threaded   // Qt 5.10+ 独立线程渲染——实验性
}

# 9.2.4 频谱波形

import QtQuick

ApplicationWindow {
    width: 600; height: 200; visible: true

    Canvas {
        id: spectrum
        anchors.fill: parent
        property var frequencies: []

        onPaint: {
            var ctx = getContext("2d")
            var w = width; var h = height
            ctx.clearRect(0, 0, w, h)

            if (!frequencies.length) return
            var barW = w / frequencies.length

            for (var i = 0; i < frequencies.length; i++) {
                var barH = frequencies[i] * h
                var hue = (i / frequencies.length) * 360
                ctx.fillStyle = "hsl(" + hue + ", 80%, 60%)"
                ctx.fillRect(i * barW, h - barH, barW - 2, barH)
            }
        }

        // 定时更新数据
        Timer {
            interval: 50; repeat: true; running: true
            onTriggered: {
                var data = []
                for (var i = 0; i < 64; i++)
                    data.push(Math.random())
                spectrum.frequencies = data
                spectrum.requestPaint()   // 每次 Timer tick 触发重绘
            }
        }
    }
}

案例知识融合:本案例演示了 Canvas 的标准使用模式——①Timer 定时更新数据(非高频,50ms 间隔);②frequencies 变化触发 requestPaint();③onPaint 里纯 Context2D 绘制 64 个矩形条。这个场景 Canvas 完全够用——更新频率 ≤ 20fps + 绘制元素 ≤ 100。

思考题:

  1. 如果把 Timer.interval: 50 改成 interval: 5(200fps),在 ARM 板上 Canvas 能撑住吗?瓶颈在 FBO 光栅化还是 V4 JS 执行?
  2. ctx.fillStyle = "hsl(" + hue + "..." 每次循环都做字符串拼接——改成预计算颜色数组会提升多少性能?

# 9.3 Shape 矢量路径

# 9.3.1 矢量渲染

Qt 6 引入了 Shape + ShapePath——利用 GPU 的曲面细分(Tessellation) 直接渲染 SVG 路径,不经过 FBO:

import QtQuick.Shapes

Shape {
    width: 200; height: 200

    ShapePath {
        strokeColor: "#FF5252"; strokeWidth: 2
        fillColor: "#4CAF50"
        fillRule: ShapePath.WindingFill         // 填充规则

        startX: 100; startY: 10
        PathLine  { x: 190; y: 180 }
        PathLine  { x: 10;  y: 180 }
        PathLine  { x: 100; y: 10  }

        // 或使用 SVG 路径字符串
        // PathSvg { path: "M 100 10 L 190 180 L 10 180 Z" }
    }
}

Shape 的渲染路径 vs Canvas:

Shape:
  PathLine / PathQuad / PathCubic
    → GPU 曲面细分 → 三角形网格 → QSGGeometryNode
    → Scene Graph 批处理 → 直接渲染 ← 无 FBO

Canvas:
  PathLine / PathQuad / PathCubic(Context2D)
    → CPU 光栅化到 FBO → 纹理上传 → QSGGeometryNode
    → Scene Graph 渲染 ← 有 FBO

# 9.3.2 Shape 对比

维度 Canvas Shape
路径渲染 CPU 光栅化 → FBO GPU 曲面细分
批处理兼容 ❌ 独立 FBO ✅ 同批处理
动态路径 requestPaint() 全刷新 属性绑定自动更新
填充规则 nonzero / evenodd WindingFill / OddEvenFill
渐变色 createLinearGradient() LinearGradient {} 对象
嵌入式 GPU 占用 CPU 密集 GPU 密集——在 ARM Mali 上更快

# 9.4 Painted 命令式

# 9.4.1 QPainter 路径

QQuickPaintedItem 适用于从 Qt Widget 的 QPainter 代码迁移:

class ChartItem : public QQuickPaintedItem {
    Q_OBJECT
    Q_PROPERTY(QVector<double> data READ data WRITE setData NOTIFY dataChanged)

public:
    void paint(QPainter* painter) override {
        painter->setRenderHint(QPainter::Antialiasing);
        painter->fillRect(boundingRect(), QColor("#1a1a2e"));

        QPen pen(QColor("#4CAF50"), 2);
        painter->setPen(pen);

        QPainterPath path;
        for (int i = 0; i < m_data.size(); ++i) {
            double x = i * width() / m_data.size();
            double y = height() / 2 - m_data[i] * 100;
            if (i == 0) path.moveTo(x, y);
            else        path.lineTo(x, y);
        }
        painter->drawPath(path);
    }

private:
    QVector<double> m_data;
};
ChartItem {
    id: chart
    anchors.fill: parent
    data: [0.1, 0.3, 0.5, ...]
}

渲染路径:paint(QPainter*) → QPainter 引擎(C++,非 JS)→ 输出到 QImage(FBO)→ glTexImage2D 上传 GPU。

QQuickPaintedItem vs Canvas:

维度 Canvas QQuickPaintedItem
语言 V4 JS C++
CPU 光栅化速度 慢(V4 路径) 快 3-5×(QPainter C++ 路径)
FBO → 纹理 同样走 同样走
异步渲染 renderStrategy: Threaded setPerformanceHint(FastFBOResizing)
适用场景 QML 全栈开发者 有 Qt Widget 存量代码

# 9.4.2 Widget 迁移

原 Widget 代码 QQuickPaintedItem 等价
QWidget::paintEvent(QPaintEvent*) QQuickPaintedItem::paint(QPainter*)
painter->drawRect(QRect(10,10,100,50)) 完全相同——QPainter API 不变
QWidget::update() QQuickPaintedItem::update()
QWidget::setFixedSize() QQuickPaintedItem::setSize(QSizeF())

# 9.5 自定义节点

# 9.5.1 零拷贝路径

这是 QML 自定义渲染的性能天花板——数据直接从 C++ 写入 GPU 顶点缓冲,不经过任何 JS、FBO、纹理上传:

class WaveItem : public QQuickItem {
    Q_OBJECT
    static constexpr int MAX_POINTS = 500;

    QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override {
        // ① 复用或创建 QSGGeometryNode
        auto* node = static_cast<QSGGeometryNode*>(oldNode);
        if (!node) {
            node = new QSGGeometryNode();

            // ② 分配顶点缓冲——GL_LINE_STRIP 模式
            auto* geometry = new QSGGeometry(
                QSGGeometry::defaultAttributes_Point2D(), MAX_POINTS);
            geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
            node->setGeometry(geometry);
            node->setFlag(QSGNode::OwnsGeometry);   // node 负责释放 geometry

            // ③ 设置材质
            auto* material = new QSGFlatColorMaterial();
            material->setColor(QColor("#00ff00"));
            node->setMaterial(material);
            node->setFlag(QSGNode::OwnsMaterial);
        }

        // ④ 直接写入顶点数据——零拷贝!
        auto* geometry = node->geometry();
        geometry->allocate(m_pointCount);            // 按实际点数分配
        auto* vertices = geometry->vertexDataAsPoint2D();
        for (int i = 0; i < m_pointCount; ++i) {
            vertices[i].set(
                i * width() / MAX_POINTS,
                height() / 2 - m_buffer[i] * height() * 0.4
            );
        }

        // ⑤ 标记为 dirty——下一帧 Sync 阶段被 Scene Graph 拾取
        node->markDirty(QSGNode::DirtyGeometry);
        return node;
    }

    void pushSample(float value) {
        m_buffer[m_pointCount % MAX_POINTS] = value;
        if (m_pointCount < MAX_POINTS) ++m_pointCount;
        update();   // ← 标记 QQuickItem 为 dirty → 触发 updatePaintNode
    }

private:
    float m_buffer[MAX_POINTS] = {};
    int m_pointCount = 0;
};

# 9.5.2 顶点缓冲

QSGGeometry 的绘制模式与顶点属性:

绘制模式 表示 例子
DrawPoints 独立点 散点图
DrawLines 每两个顶点一条线段 网格线
DrawLineStrip 连续折线 心电波形
DrawTriangles 每三个顶点一个三角形 填充区域
DrawTriangleStrip 连续三角形带 带状图

顶点属性集选择:

// 二维坐标(x, y)
QSGGeometry::defaultAttributes_Point2D()    // → 两个 float

// 二维坐标 + 颜色(x, y, r, g, b, a)
QSGGeometry::defaultAttributes_ColoredPoint2D()

// 二维坐标 + 纹理坐标
QSGGeometry::defaultAttributes_TexturedPoint2D()

# 9.5.3 脏标同步

updatePaintNode() 在 Render Thread 的 Sync 阶段被调用——这是两线程唯一的交汇点:

数据写入路径(任意线程 → GUI 线程 → Render Thread):

  传感器线程: pushSample(value)
    → 锁保护写 m_buffer ← 可以来自任何线程
    → QQuickItem::update() ← 必须在 GUI 线程调用!

  使用 signal/slot 桥接:
  connect(sensor, &Sensor::newSample, waveItem, [=](float v) {
      waveItem->pushSample(v);  // ← Qt::AutoConnection 保证在 GUI 线程
  });
// 线程安全的环形缓冲
class WaveItem : public QQuickItem {
    Q_OBJECT
public slots:
    void pushSample(float value) {
        QMutexLocker lock(&m_mutex);
        m_ringBuffer[m_writeIndex % MAX_POINTS] = value;
        ++m_writeIndex;
        m_hasNewData = true;
        lock.unlock();
        update();   // ← 必须在 GUI 线程
    }

private:
    QMutex m_mutex;
    std::atomic<bool> m_hasNewData{false};
    int m_writeIndex = 0;
};

# 9.6 实时心电

// EKGItem.h + EKGItem.cpp
class EKGItem : public QQuickItem {
    Q_OBJECT
    static constexpr int MAX_POINTS = 1024;

    QSGNode* updatePaintNode(QSGNode* old, UpdatePaintNodeData*) override {
        auto* node = static_cast<QSGGeometryNode*>(old);
        if (!node) {
            node = new QSGGeometryNode();
            auto* geom = new QSGGeometry(
                QSGGeometry::defaultAttributes_Point2D(), MAX_POINTS);
            geom->setDrawingMode(QSGGeometry::DrawLineStrip);
            node->setGeometry(geom);
            node->setFlag(QSGNode::OwnsGeometry);
            auto* mat = new QSGFlatColorMaterial();
            mat->setColor(QColor("#00ff00"));
            node->setMaterial(mat);
            node->setFlag(QSGNode::OwnsMaterial);

            // 背景网格——独立 QSGGeometryNode
            auto* gridNode = createGridNode();
            node->appendChildNode(gridNode);
        }

        // 更新波形顶点
        QMutexLocker lock(&m_mutex);
        auto* geom = node->geometry();
        geom->allocate(m_pointCount);
        auto* verts = geom->vertexDataAsPoint2D();
        float w = width(), h = height();
        for (int i = 0; i < m_pointCount; ++i)
            verts[i].set(i * w / MAX_POINTS, h/2 - m_buffer[i] * h * 0.4);

        node->markDirty(QSGNode::DirtyGeometry);
        return node;
    }

    QSGGeometryNode* createGridNode() {
        // 水平网格线——简化实现
        auto* node = new QSGGeometryNode();
        // ...
        return node;
    }

public slots:
    void pushSample(float value) {
        QMutexLocker lock(&m_mutex);
        m_buffer[m_pointCount % MAX_POINTS] = value;
        if (m_pointCount < MAX_POINTS) ++m_pointCount;
        lock.unlock();
        update();
    }

private:
    QMutex m_mutex;
    float m_buffer[MAX_POINTS] = {};
    int m_pointCount = 0;
};
import QtQuick

ApplicationWindow {
    width: 800; height: 400; visible: true; color: "#0a0a1a"

    EKGItem {
        id: ekg
        anchors.fill: parent
        anchors.margins: 20
    }

    Timer {
        interval: 5; repeat: true; running: true   // 200Hz 模拟
        onTriggered: {
            ekg.pushSample(Math.sin(Date.now() / 200) * 0.8
                + Math.sin(Date.now() / 87) * 0.3
                + (Math.random() - 0.5) * 0.1)
        }
    }
}

案例知识融合:本案例集成了 QSGNode 零拷贝渲染的完整链路——①C++ pushSample 写入环形缓冲(线程安全);②update() 标记 dirty;③updatePaintNode 直接写顶点缓冲(零拷贝);④Scene Graph 在批处理阶段渲染波形。ARM A53 上实测 60fps 稳定,CPU 占用 < 15%。


# 9.7 路径选型

路径 性能 CPU 路径 批处理 适合场景 开发复杂度
Canvas ★★ V4 JS → FBO ❌ 低频图表、仪表盘 ★ 低
QQuickPaintedItem ★★★ C++ QPainter → FBO ❌ Widget 迁移、复杂路径 ★★ 中
Shape ★★★★ GPU 曲面细分 ✅ 矢量路径、SVG 图标 ★★ 中
QQuickItem + QSGNode ★★★★★ C++ 顶点缓冲 ✅ 高频实时数据渲染 ★★★★ 高

选型决策:

更新频率 < 5fps?
  ├─ 是 → Canvas
  └─ 否 → 有 Qt Widget 存量代码?
            ├─ 是 → QQuickPaintedItem
            └─ 否 → 更新频率 > 30fps?
                      ├─ 是 → QQuickItem + QSGNode
                      └─ 否 → Shape(矢量)/ Canvas(像素操作)

# 9.8 新手陷阱

# 陷阱 说明 修复
1 Canvas 高频 requestPaint() 每帧 FBO→纹理→GPU 全路径 换 QSGNode;或合并为批量更新
2 updatePaintNode 里没设 OwnsGeometry 内存泄漏——每次新 geometry 不释放 node->setFlag(QSGNode::OwnsGeometry)
3 sensor 线程直接调 update() QSGRenderThread safety 违反 通过信号桥接 Qt::AutoConnection
4 Canvas JS 循环里字符串拼接 每帧重做字符串操作 预计算、或换 C++ 方案
5 Shape 用太复杂路径 GPU 曲面细分顶点爆炸 简化路径点、关闭不必要的 fillRule

# 9.9 训练题

将 Canvas 心电波形改为 QSGNode 方案

下面的 Canvas 代码在 ARM 板上只能跑 5fps。写出等效的 QQuickItem + QSGNode 实现:

Canvas {
    property var data: []
    onPaint: {
        var ctx = getContext("2d")
        ctx.clearRect(0,0,width,height)
        ctx.beginPath()
        for (var i = 0; i < data.length; i++) {
            var x = i * width / 500; var y = height/2 - data[i] * 100
            i === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y)
        }
        ctx.stroke()
    }
}
参考答案

参见 §9.6 的 EKGItem 实现——updatePaintNode 里 QSGGeometry::DrawLineStrip 直接写顶点数据,绕过 Canvas 的 FBO 路径。


# 9.10 速查表

概念 一句话
Canvas V4 JS 执行 Context2D → FBO 离屏缓冲 → 纹理上传
requestPaint() 标记 Canvas dirty——下一帧 Sync 合并执行一次 onPaint
FBO 离屏帧缓冲——Canvas/QQuickPaintedItem 的中间渲染目标
Shape GPU 曲面细分矢量路径——无 FBO、可批处理
QQuickPaintedItem C++ QPainter → FBO——适合 Widget 迁移
QSGGeometryNode Scene Graph 核心绘制节点——顶点 + 材质
QSGGeometry 顶点缓冲——DrawPoints/DrawLines/DrawLineStrip/DrawTriangles
updatePaintNode() 每帧 Sync 阶段调用——在此写入顶点数据
markDirty 标记节点需要更新——DirtyGeometry/DirtyMaterial/DirtyMatrix
OwnsGeometry QSGNode 标志——节点析构时自动释放 geometry 内存
zero-copy 数据直写顶点缓冲 → 无 JS、无 FBO、无纹理上传

核心哲学:

Canvas = "给我 JS,我画一张位图贴到屏幕"。
QSGNode = "给我顶点数据,我让 GPU 直接画"。

前者开发快(5 行代码出图),后者性能高(200Hz 不掉帧)。
选型不看"谁更高级"——看你需要的是开发速度还是运行速度。

下一篇:10. QML与C++集成原理 (opens new window)

上次更新: 2026/07/12, 19:39:51
动画与状态机原理
QML与C++集成原理

← 动画与状态机原理 QML与C++集成原理→

最近更新
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号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式