Back to Blog home

2D渲染——Qt Canvas Painter简介

Published on 星期三 二月 25, 2026 by Kaj Grönholm in Graphics UI Development Canvas Painter | Comments

让我们来谈谈 Qt Canvas Painter - 一个全新的命令式 2D 渲染 API,旨在将性能、生产力和现代功能结合起来。本博文将为您介绍一些 2D 渲染的历史、现状和未来的可能性。

下面是一段 Qt Canvas Painter 2D 渲染的简短介绍视频

Qt 命令式 2D 渲染的历史

我们可以从 Qt 最受欢迎的绘图 API QPainter 开始了解命令式 2D 渲染的历史。QPainter 是一个出色的通用绘图 API,二十多年来一直为 Qt 中的 Widgets 和命令式绘图提供支持。要了解有关 QPainter 历史的更多信息,推荐您观看 Eirik 在 Qt World Summit 2025 的主题演讲:30 Years of Graphics Rendering in Qt。您将了解到 QPainter 最适合在 CPU 上实现绘制引擎。

在现代图形架构中QPainter API 及其 2D 渲染体系存在一个显著弱点:其设计理念与 GPU 及 3D API 的适配性欠佳。QPaintEngine 的抽象性使得 QPainter API 可以很容易地在不同的后端得到支持,但它并不能让 GPU 发挥出最佳性能,因为 GPU 喜欢并行处理并将数据保存在 GPU 显存中。功能集通常也是有限的,就像基于 OpenGL 的传统绘图引擎一样,只能提供纯软件(所谓的光栅)绘图引擎的一部分功能。

因此,考虑到我们非常希望将 QPainter 保留为通用的 2D 渲染 API,并避免给当前的 QPainter 和 widgets 用户带来麻烦,那么硬件加速的命令式绘图的最佳方法是什么呢?大约一年前,我们与图形团队的一些成员一起在 Qt 奥斯陆办公室举行了一次研讨会。作为这次研讨会的成果,我们决定启动一个实验项目,在QRhi 的基础上构建一个更精简的替代绘画 API。该 API 的主要目标是将性能和生产力结合起来。

Qt Canvas Painter 的历史可追溯到 Mikko Mononen 最初开发的优秀、精炼的绘图库 NanoVG。大约 10 年前,我个人启动了一个名为QNanoPainter 的项目,该项目在 NanoVG 的基础上提供了 Qt C++ API 和 Qt Quick & widget 辅助工具。多年来,它一直是一个相当成功的小项目,得到了许多用户的积极反馈。

因此,我们决定开始在 QNanoPainter 的基础上构建 Canvas Painter。我们首先用 Qt C++ 重写了 NanoVG 的 C 语言后端。把原有的 OpenGL 渲染后端重写为基于 QRhi 的实现,因此我们不仅支持 OpenGL,还支持 Vulkan、Metal 和 Direct3D。文本渲染从头开始重写,利用 Qt Quick 强大的文本渲染功能 SDF fonts 为基础来实现。此外,我们还添加了许多功能,其中一些功能补足了缺失的 HTML Canvas 2D 上下文功能,其他功能则实现了全新的功能。其中一些新功能仍处于试验阶段,但我们已经看到它们对于高性能和现代需求具有巨大的潜力。

说到这里,我不得不提一下警告:

Qt Canvas Painter 是 Qt 6.11 版本中的技术预览版。这意味着我们还不能保证 API 或 ABI 的稳定性。

实现出色命令式 2D 渲染的步骤

painter 的主要设计目标可归纳为以下三点:

性能:首先且可能是最重要的目标是实现高性能,尤其是在移动/嵌入式硬件上。如果性能不能优于现有的替代方案,那么使用新的应用程序接口就没有吸引力。为了达到最高性能,我们减少了功能和抽象层,直接在 QRhi 上实现了绘制。许多 2D 渲染库最初都是针对(CPU)软件渲染实现的,后来才有了(GPU)加速渲染后端,而 Qt Canvas Painter 从一开始就仅支持 GPU 渲染。目前还没有实施软件后端的计划。

生产力:性能当然不能是唯一的目标,否则我们可以指示开发人员直接使用 QRhi 或 Vulkan。这些底层 API 功能强大、速度极快,但使用门槛较高,因此无法提供高效的生产率。通过选择 HTML Canvas 2D 上下文作为 Qt Canvas Painter API 的基础,我们希望提供熟悉的 API、清晰的文档和代码示例,从而提高开发效率。 熟悉的 API 和互联网上丰富的源代码也能在未来提高 AI 助手的能力。

功能基于性能和生产率这两个目标,该 API 已经能够为许多用户提供相当大的帮助。但如果我们无法提供一套引人注目的功能集,它可能仍将局限于小众领域。如果一个库仅能快速地绘制成千上万个白色矩形,那么它将无法满足许多用户的需求。集成 HTML Canvas 2D 上下文的大部分功能为该库提供了良好的基础。但要真正吸引用户,我们需要并希望提供更多的功能,尤其是为现代 UI 和硬件加速量身定制的功能。

我们将在后续的博客中更详细地讨论上述要点,逐步介绍这些功能和基准测试结果。

Qt Quick 中使用 Canvas Painter

Canvas Painter 在 2D 渲染中的主要预期用途是实施自定义 Qt Quick 项目。 它可被视为 QQuickPaintedItem 的一个性能更强、集成度更高的替代品。

要创建自定义 QML 项目,您需要实现一个继承于 QQuickCPainterItem 的类,并重写 createItemRenderer() 方法。类似下面这样


class HelloItem : public QQuickCPainterItem { Q_OBJECT QML_ELEMENT public: HelloItem(QQuickItem *parent = nullptr); QQuickCPainterRenderer *createItemRenderer() const override; private: friend class HelloItemRenderer; QString m_label; };

然后,该类就可以像 QQuickItems 一样,拥有与 QML 集成的属性、槽等。在这个简单的例子中,我们只是直接设置标签文本,而不是从 QML 获取。因此,该类的实现是这样的


HelloItem::HelloItem(QQuickItem *parent) :  QQuickCPainterItem(parent) { m_label = QString("Canvas Painter"); } QQuickCPainterRenderer *HelloItem::createItemRenderer() const { // Create the renderer for this item return new HelloItemRenderer(); } // 创建此项目的呈现器。

渲染器类继承于QQuickCPainterRenderer,通常会重写 synchronize() 和 paint() 方法。由于大多数平台上的渲染都是在单独的线程中进行的,因此 synchronize() 是渲染器和 Item 唯一可以安全读写彼此变量的地方。在我们的 Hello Canvas Painter 示例中,渲染的代码如下:


void HelloItemRenderer::synchronize(QQuickCPainterItem *item) { // 在项目和呈现器之间同步此处的数据。
    HelloItem *helloItem = static_cast<helloitem*>(item); m_label = helloItem->m_label; } void HelloItemRenderer::paint(QCPainter *p) { float size = std::min(width(), height()); QPointF center(width() * 0.5, height() * 0.5); // 绘制背景圆 QCRadialGradient gradient1(center, size * 0.6); gradient1.setStartColor(0x909090); gradient1.setEndColor(0x202020); p->beginPath(); p->circle(center, size * 0.46); p->setFillStyle(gradient1); p->fill(); p->setStrokeStyle(0x202020); p->setLineWidth(size * 0.02); p->stroke(); // 绘制文本 p->setTextAlign(QCPainter::TextAlign::Center); p->setTextBaseline(QCPainter::TextBaseline::Middle); QFont font1("Titillium Web"); font1.setWeight(QFont::Weight::Bold); font1.setItalic(true); font1.setPixelSize(size * 0.08); p->setFont(font1); p->setFillStyle(0x2CDE85); p->fillText("HELLO", center.x(), center.y() - size * 0.28); QFont font2("Titillium Web"); font2.setWeight(QFont::Weight::Thin); font2.setPixelSize(size * 0.12); p->setFont(font2); p->fillText(m_label, center.x(), center.y() - size * 0.16); // Paint the heart QCImage logo = p->addImage(m_logoImage, QCPainter::ImageFlag::Repeat | QCPainter::ImageFlag::GenerateMipmaps); float pSize = size * 0.05; QCImagePattern pattern(logo, center.x(), center.p->setFillStyle(pattern); p->setStrokeStyle(0x2CDE85); p->beginPath(); float hs = size * 2; QPointF hc(width() * 0.5, height() * 0.25); p->moveTo(hc.x(), hc.y() + hs * 0.3); p->bezierCurveTo(hc.x() - hs * 0.25, hc.y() + hs * 0.1, hc.x(), hc.y() + hs * 0.05, hc.x(), hc.y() + hs * 0.18); p->bezierCurveTo(hc.x(), hc.y() + hs * 0.05,hc.x() + hs * 0.25,hc.y( ) + hs * 0.1,hc.x(),hc.y( ) + hs * 0.3);p->fill();p->stroke(); }

当我们运行上述示例时,自定义 Canvas Painter 项目看起来就像这样:
qcpainter_helloitem_example-1
如需了解更复杂的示例,请参阅 Qt 版本(从 Qt 6.11 开始)中提供的示例,如 Gallery 示例,该 示例展示了许多可用的 2D 渲染功能。此外还有Compact Health 示例,该示例展示了如何在不依赖 Qt Quick 或 Qt Widgets 的情况下直接将 QCPainter 与 QRhi 和 QWindow 一起使用。此外,请查看 Canvas Painter API 文档,尤其是 QCPainter 类的说明。

关于 Widgets

我们为所有 Qt Widget 用户带来了好消息:Canvas Painter 也完全支持 Widget!有一个 QCPainterWidget 辅助类,它建立在 QRhiWidget 之上,使用方法与 QWidget 类似,在您自己的 widget 类中继承它并重写绘制方法。但您使用的不是 QPainter,而是 QCPainter。这样,您就可以在 QWidget 应用程序中使用自定义 widgets,并在 QRhi 上进行硬件加速渲染。下面是一个简单的示例应用程序,其中包含一些标准 widgets 和一个使用 Canvas Painter 绘制的模拟时钟。
qcpainter_clockwidget_example-1
这意味着现有的应用程序和部件将继续使用带有光栅后端的 QPainter;只有专门为使用 Canvas Painter 而实现的部件才会发生变化。这种方法的优点是所有部件的外观和行为都与以前完全一样,而且可以针对从中受益的自定义部件进行硬件加速。但这也可以说是一个缺点,即现有的 QPainter 代码不会自动加速。尽管如此,由于绘制代码保持不变,因此如果需要的话,这种方式将 widget 应用程序移植到 Qt Quick 会变得更容易。

有关如何使用 Canvas Painter 实现自定义部件的示例,请查看Hello Widget 示例

当前状态

我们现在拥有的 API 一般遵循 HTML Canvas 2D 上下文规范(因此称为 Canvas Painter)。我们选择 2D 上下文作为基础规范有多方面原因。首先,作为 API,Canvas 2D 上下文比 QPainter 更简洁,因此在提供简洁的 API 的同时,我们与 2D 上下文的兼容性比 QPainter 更大。此外,由于新绘图器的一个目标是用作新的 Quick Canvas 后端,还有什么比 2D 上下文的 C++ 实现更适合 Quick Canvas QML 元素的后端呢?

尽管如此,我们的目标并不是与 2D 上下文 API 100% 兼容。对于浏览器,用户希望画布元素的呈现与 Chrome 浏览器相匹配(而非完全遵循规范)。对于 Canvas Painter C++ API(以及 Quick Canvas),开发人员始终需要移植代码并控制内容,因此完全兼容并不那么重要。更重要的是规范的一致性和高性能。

与 2D 上下文规范相比,我们目前缺少的主要功能有

  • 过滤器滤镜效果是我们很可能永远不会支持的 API。我们最终确实希望支持离屏画布的后期处理特效,但 C++ API 不会采用字符串加参数,然后需要对其进行解析。 这对于命令式 API 而言, 无论是体验还是性能都是非常低效的。
  • 虚线描边:目前,Canvas Painter 仅支持实线描边,不支持虚线描边。原因主要是缺乏时间和对性能的影响,因为三角化虚线(尤其是圆形连接)的速度较慢,而且会产生更多三角形。我们可能会在未来支持这一功能,但就目前而言,我们可以考虑一些替代方案:例如网格图案图像图案,甚至是自定义着色器笔刷来模拟虚线效果
  • 阴影:2D 上下文支持阴影效果。要创建阴影,需要渲染到离屏画布并进行高斯模糊处理。这对性能有影响,因此 Mozilla 甚至指示"尽可能避免使用 shadowBlur 属性"。我们已初步支持离屏画布元素,但仍不确定 2D 上下文阴影 API 是否是最佳选择。也许让用户更能控制何时/如何创建和更新离屏画布的方法会更理想?或者考虑到对性能的影响,快速的圆形矩形阴影自由调整的抗锯齿就足够了?
  • 形状裁剪:2D 上下文支持 clip() 方法,该方法允许剪切到任何形状。Canvas Painter 目前不支持这种方法;它只支持对有或无变换的矩形区域进行快速剪切。

虽然我们缺少一些 2D 画布功能,但我们是否引入了新特性呢?实际上,确实有很多新增功能!新功能的列表相当长,而我想要深入探讨这些细节,因此我们有一篇单独的后续博客文章,专门重点介绍 Qt Canvas Painter 的新特性。

未来计划

Qt Canvas Painter 将在 Qt 6.11 中作为技术预览版提供。我们计划改进 2D 渲染 API,并找到能将其最佳部分发挥出来的用例。最明显的一个例子就是新的 Quick Canvas 后端,它可以为 Canvas Painter C++ 和 QML JavaScript API 带来功能兼容性。这意味着使用 QML 脚本构建的快速动态 UI,在需要更多底层控制和数据处理时,可以轻松地将代码移植到 C++

在我看来,未来就是现在。请安装 Qt 6.11 预发行版或从源代码中构建 Qt 并试用 Canvas Painter。请在这里或相关的 Qt 论坛主题中提出问题,或为我们创建 bug 和建议单。获得真实的用户反馈是我们改进并使其从技术预览版中脱颖而出,成为 Qt 解决方案完全支持的一部分的最佳途径。

期待您在下一期博客中回访,届时我们将进一步介绍 Canvas Painter 的新功能:

Subscribe to Our Blog

Stay up to date with the latest marketing, sales and service tips and news.