此前关于 Qt Canvas Painter 的博客文章,介绍了它是什么、带来的新渲染功能、与 QPainter 相比的性能表现,以及路径缓存如何让百万级线段也能流畅渲染。这些文章使用的都是 QCanvasPainter C++ API。在路径缓存那篇文章的结尾,我曾预告 QML 元素即将推出,现在它来了:Canvas2D,已在 Qt 6.12 中提供。

注意:Qt Canvas Painter C++ API 在 Qt 6.12 中已不再是技术预览版,但 Canvas2D 仍是处于技术预览阶段。它可以正常工作,我们也鼓励您使用,但该 API 仍可能变更,目前也不提供源代码或二进制兼容性保证。
什么是 Canvas2D?
Canvas2D 是一个 QML 项,可让您使用 JavaScript 进行绘制,采用 HTML Canvas 和 Qt Quick Canvas 所普及的命令式绘图模型。其底层使用 Qt Canvas Painter,这意味着绘制通过 QRhi 在 GPU 上完成,而不是通过 CPU 绘制到 QImage 中。
上手非常简单,以下是一个简单的 QML 示例:
import QtQuick
import QtCanvas2D
Canvas2D {
width: 100
height: 200
onPaint: {
var ctx = getContext("2d");
// Paint a red rectangle
ctx.fillStyle = Qt.rgba(1, 0, 0, 1);
ctx.fillRect(0, 0, width, height);
}
}
如果您曾在 QML 中编写过 Canvas { onPaint: ... },或在浏览器中使用过 <canvas> 元素,那么这里没有什么特别之处。您只需导入 QtCanvas2D 模块,即可创建 Canvas2D 元素。
与 HTML Canvas 的兼容性
Canvas2DContext API 实现了 Web 开发者已经熟悉的同一套 W3C Canvas 2D Context API:beginPath()、moveTo()、lineTo()、bezierCurveTo()、fill()、stroke() 等,以及熟悉的状态属性 fillStyle、strokeStyle、lineWidth、lineCap、lineJoin 等。
这意味着,来自网络、教程、Stack Overflow 或大语言模型(LLM)的画布代码通常可以直接运行。对于命令式 2D API 而言,这是巨大的效率优势:相关知识和代码片段已经存在。
不过,Canvas2D 的目标并非 100% 兼容 HTML Canvas。为使 API 更简洁并保持 GPU 上的高性能,部分功能未被纳入。相较 HTML Canvas,目前缺少以下功能:
- 路径裁剪 – 裁剪区域只能是(可经变换的)矩形,通过 setClipRect() 和 resetClipping() 实现。
- 虚线 – 描边始终为实线。
- 路径检测 – 不支持 isPointInPath() 和 isPointInStroke()。
- 文字描边 – 不支持文字轮廓描边。
- 滤镜 – 不支持画布 SVG 滤镜效果。
- 合成模式 – 仅限于无需渲染到额外缓冲区即可支持的三种模式:source-over、source-atop 和 destination-out。
- 内置阴影属性 – shadowBlur 及相关属性由可调抗锯齿和盒状阴影取代,详见下文。
另一方面,Canvas2D 还新增了 HTML Canvas 不具备的功能:
- 带路径组的 Path2D – createPath2D() 可生成 path2d 容器,并可通过可选的路径组进行填充和描边。同一组中的路径共享一个在 GPU 端缓存的顶点缓冲区,这正是路径缓存那篇文章中大幅性能提升的来源。
- 可调抗锯齿 – ctx.antialias 和 ctx.textAntialias 以像素为单位设置抗锯齿宽度,只需一个数值即可实现从锐利到柔和光晕的效果。
- 盒状渐变 – createBoxGradient() 可沿圆角矩形的形状创建渐变。
- 盒状阴影 – createBoxShadow() / drawBoxShadow() 采用 SDF(有向距离场) 方法渲染 CSS 风格的盒状阴影,与 Qt Quick 的 RectangularShadow 类似。相比高斯模糊阴影,开销极低。
- 网格图案 – createGridPattern() 可用于动态网格和条形图图案,且开销恒定。
- 色彩效果 – 在标准 globalAlpha 之外,还提供 globalBrightness、globalContrast 和 globalSaturation 色彩效果。
- 孔洞子路径 – beginHoleSubPath() / beginSolidSubPath() / setPathWinding() / windingEnforce 让环形和镂空图形的绘制变得简单,无需手动管理路径的环绕方向。
- 文本换行 – fillText(text, x, y, width, height) 可将文本换行到矩形区域内,由 textWrapMode 和 textLineHeight 控制。
- 额外图形 – circle()、ellipse() 和 ellipseRect() 作为一等路径图元提供。
- Transform2d – createTransform2D() 提供真正的 3x3 矩阵对象,可逐步构建并传递给 setTransform() 或 transform(),无需手动处理六个浮点数,也无需使用完整的 DOMMatrix。
与 Qt Quick Canvas 的兼容性
如果您已在使用 Qt Quick Canvas,Canvas2D API 与其几乎完全兼容。您只需导入 QtCanvas2D 并将 Canvas 改名为 Canvas2D,基本即可正常使用。
以下代码示例展示了这两行改动:
// Before
import QtQuick
Canvas {
onPaint: { var ctx = getContext("2d"); paintEverything(ctx); }
}
// After
import QtQuick
import QtCanvas2D
Canvas2D {
onPaint: { var ctx = getContext("2d"); paintEverything(ctx); }
}
下文介绍的 Canvas 2D Tester 示例正是依赖于此:同一套 JavaScript 绘制函数同时被用于 Canvas 和 Canvas2D 元素。
下表总结了 Qt Quick Canvas 与 Canvas2D 的差异。
| |
Qt Quick Canvas |
Canvas2D |
| 绘制后端 |
QPainter(CPU)绘制到 QImage 后上传为纹理 |
通过 QRhi 在 GPU 上运行的 Qt Canvas Painter |
| 渲染目标 |
Canvas.Image(自 Qt 6.0 起忽略 FramebufferObject) |
通过 QRhi 直接渲染到场景图中。 |
| 动画与大尺寸画布 |
官方文档不建议此类用法,因为每次更新都需要上传纹理 |
正是其主要适用场景 |
| 帧间内容 |
保留,需自行调用 clearRect() |
每帧清空,fillColor 定义背景。 |
| 路径缓存 |
不支持 |
支持,通过 path2d 与路径组实现 |
| 裁剪 |
clip() 可裁剪任意路径(开销可能较大) |
setClipRect(),仅支持矩形 |
| 虚线描边 |
支持(setLineDash) |
暂不可用 |
| 阴影 |
shadowBlur / shadowColor |
针对圆角矩形提供快速盒状阴影(SDF),其他图形可调抗锯齿。 |
| 像素操作 |
getImageData() / putImageData()(开销可能较大) |
不可用 |
| 抗锯齿 |
固定 |
可按描边、填充及文字分别调整 |
| 额外画刷 |
无 |
盒状渐变、网格图案、着色图像 |
| 色彩效果 |
globalAlpha |
globalAlpha、globalBrightness、globalContrast、globalSaturation |
简而言之,取舍如下:您需放弃少数较少使用的功能,换来 GPU 级别的渲染速度以及大量新画刷和效果。对于典型场景(如动画自定义可视化、仪表盘、图表、波形图、手绘控件等),这是值得的取舍。如果您的代码依赖 Canvas2D 不具备的 Quick Canvas 功能,且运行良好,可继续使用,该功能仍然可用。我们也很乐意了解您的使用场景,以便今后优先安排这些功能。
有一个行为差异值得再次强调:Canvas2D 默认每帧从头重绘,因此无需像 HTML Canvas 和 Quick Canvas 那样在 onPaint 开头调用 clearRect()。
与 QCanvasPainter C++ API 的兼容性
QCanvasPainter 与 Canvas2DContext 是同一个绘制器的两种形式。C++ API 使用 setter 方法和 Qt 值类型,QML API 使用属性和对 JavaScript 友好的参数,但方法名、参数顺序和语义保持一致。两个方向之间的移植几乎是机械性的,可以轻松借助脚本或大语言模型完成。
以下是 QCanvasPainter 文档中的圆角按钮示例,并与 Canvas2DContext 文档中的同一示例并列对照。
C++ 使用 QCanvasPainter:
QRectF rect(40, 70, 120, 60);
QRectF shadowRect = rect.translated(2, 4);
// Paint shadow
QCanvasBoxShadow shadow(shadowRect);
shadow.setRadius(30);
shadow.setBlur(15);
shadow.setColor("#60373F26");
p->drawBoxShadow(shadow);
// Paint rounded rect
p->beginPath();
p->roundRect(rect, 30);
p->setFillStyle("#DBEB00");
p->fill();
// Paint text
p->setTextAlign(QCanvasPainter::TextAlign::Center);
p->setTextBaseline(QCanvasPainter::TextBaseline::Middle);
QFont font("Titillium Web", 18);
p->setFont(font);
p->setFillStyle("#373F26");
p->fillText("CLICK!", rect);
JavaScript 使用 Canvas2DContext:
let offsetX = 2;
let offsetY = 4;
// Paint shadow
let shadow = ctx.createBoxShadow(40 + offsetX, 70 + offsetY,
120, 60);
shadow.setRadius(30);
shadow.setBlur(15);
shadow.setColor("#60373F26");
ctx.drawBoxShadow(shadow);
// Paint rounded rect
ctx.beginPath();
ctx.roundRect(40, 70, 120, 60, 30);
ctx.fillStyle = "#DBEB00";
ctx.fill();
// Paint text
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "24px 'Titillium Web'";
ctx.fillStyle = "#373F26";
ctx.fillText("CLICK!", 100, 100);
移植规则相当简单:
| C++ QCanvasPainter 写法 |
QML Canvas2DContext 写法 |
| p->setFillStyle("#ff0000") |
ctx.fillStyle = "#ff0000" |
| p->setAntialias(10) |
ctx.antialias = 10 |
| p->setTextAlign(QCanvasPainter::TextAlign::Center) |
ctx.textAlign = "center" |
| QCanvasLinearGradient lg(...) |
let lg = ctx.createLinearGradient(...) |
| QCanvasPath |
path2d |
| QRectF rect 参数 |
x、y、width、height 参数 |
API 的统一意义重大,其价值可能超出想象。您可以先在 QML 中原型化可视化效果,编辑与运行的循环只需几秒。之后若性能分析表明 JavaScript 成为瓶颈,或需要使用位于 C++ 端的实际数据,只需通过大部分查找替换,即可将相同的绘制代码迁移到 C++ 中的 QCanvasPainterItem。反过来也可以:将现有的 C++ Canvas Painter 项进行封装,提供可供 QML 脚本调用的版本。这一过程中并不会更换渲染引擎,只是切换编程语言,因此渲染输出保持完全一致。
Canvas 2D Tester 示例
要切身体会这一切,最好的方式是运行随模块提供的 Canvas 2D Tester 示例。它正是为回答大家最关心的两个问题而构建的:我的画布代码还能正常运行吗?它的速度真的更快吗?

该示例的巧妙之处在于:Canvas 和 Canvas2D 元素使用完全相同的 QML JavaScript 绘制代码。只有一套绘制函数,工具栏中的开关决定由哪个元素执行它们。界面角落显示 FPS 计数器,"Animate" 开关用于开启或关闭持续重绘,"Complexity" 滑块(范围 1 至 10)用于调整每个测试绘制的内容量。因此,您可以选择一个测试、观察帧率、切换开关,即可在自己的硬件和 GPU 驱动上直观看到差异。
测试分为三列:
基准测试 — 矩形、线条、圆形、裁剪、线型、文本、状态处理、图像和变换。这些都是最常用的基础操作,每次绘制数量可达数百乃至数千次。矩形测试每帧最多交替执行 5000 次 fillRect() 和 strokeRect();线条测试最多绘制 50 条折线,每条 1000 段;圆形测试最多用 2000 段圆弧构建单一路径。将复杂度调高,即可观察每个元素在何处开始力不从心。
示例 — 文本对齐、合成模式和渐变。这些是测试用于验证渲染正确性,而非比较速度:两个元素运行相同代码,便于您验证关于基线位置、合成及渐变色标的假设是否成立。
新功能 — 盒状渐变、盒状阴影、色彩效果、孔洞子路径、调整抗锯齿、网格图案和 Path2D。这些仅可在 Canvas2D 上运行。该列是对 Canvas2D 新功能的快速导览。
该示例也是获取代码灵感的好地方。其 CanvasView.qml 包含数百行纯 JavaScript 绘制函数,其中的 Path2D 测试会构建一个基于 SVG 路径的图标,并渲染该图标的多个经变换、带缓存的副本。
您可以在 code.qt.io 上找到该示例项目,也可以在 Qt Creator 示例中搜索 "canvas" 找到。
使用 QCPainterBench 进行基准测试
如果上文的 Canvas 2D Tester 示例尚未让您信服 Canvas2D 的性能,QCPainterBench 或许能让您改变看法。我们为该基准测试应用新增了两个后端:(旧)Quick Canvas 和(新)Canvas2D。请观看以下视频,了解两者之间的性能差异:
在所有测试各渲染 16 次的条件下,作者笔记本电脑上的性能数据为 4 FPS 对比 165 FPS,因此Canvas2D 的速度比 Canvas 快 40 倍以上。
关键在于两者的 JavaScript 代码完全相同,因此性能差异完全源于 Canvas2D 对动画内容进行了更好的优化。这种从 QML JavaScript 实现的高性能绘制,将带来众多新的可能性。
当前状态与后续计划
Canvas2D 是 Qt 6.12 中的新功能,目前为技术预览版。API 可能会根据我们的经验而调整,性能也很可能进一步提升——迄今为止每个版本都是如此。
因此,请试用 Canvas 2D Tester,在您的目标硬件上运行,并尝试将一个现有的 Canvas 项移植到 Canvas2D。然后告诉我们体验如何,例如通过 Qt 缺陷跟踪系统 或论坛反馈。在 API 仍处于技术预览阶段时提供的反馈,将决定最终 API 的形态。
在下一篇博客中,我将介绍一个基于 Canvas2D 的具体应用(并结合特效与Qt Quick 3D),敬请期待。