大数跨境

博客 | 2D渲染——Qt Canvas Painter简介

博客 | 2D渲染——Qt Canvas Painter简介 Qt软件
2026-03-31
0
导读:让我们来谈谈Qt Canvas Painter - 一个全新的命令式2D渲染API,旨在将性能、生产力和现代功





让我们来谈谈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版本中的技术预览版。这意味着我们还不能保证APIABI的稳定性。


实现出色命令式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项目看起来就像这样:

如需了解更复杂的示例,请参阅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绘制的模拟时钟。

这意味着现有的应用程序和部件将继续使用带有光栅后端的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上下文API100%兼容。对于浏览器,用户希望画布元素的呈现与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的新功能:



END




往期推荐

博客|Figma to Qt:将Figma设计快速转化为产品的最快途径

博客|介绍 Qt 6.11 版本更新中的 Qt Task Tree 模块

博客 | QtGraphs:2D和3D图形可视化数据的最新Qt模块


【声明】内容源于网络
0
0
Qt软件
Qt Group是一家全球软件公司,致力于开发用户喜爱的应用程序和智能设备,深受行业领导者和全球超过 150 万开发人员的信赖。我们帮助客户在整个产品开发生命周期中提高生产力——从 UI 设计和软件开发到质量管理和部署。网址:qt.io
内容 564
粉丝 0
Qt软件 Qt Group是一家全球软件公司,致力于开发用户喜爱的应用程序和智能设备,深受行业领导者和全球超过 150 万开发人员的信赖。我们帮助客户在整个产品开发生命周期中提高生产力——从 UI 设计和软件开发到质量管理和部署。网址:qt.io
总阅读4.0k
粉丝0
内容564