- Qt 6.11
- Qt Canvas Painter
- Qt Canvas Painter Examples and Tutorials
- Qt Canvas Painter - Compact Health Example
- Qt Canvas Painter | Commercial or GPLv3

# Qt Canvas Painter - Compact Health Example

Demonstrates the use of QCanvasPainter in a QWindow.

This example demonstrates using Qt Canvas Painter in a pure QWindow application. Qt Quick or QWidget are not used at all, and there is no dependency on Qt modules other than Core, Gui, and Canvas Painter.

The application sets up accelerated 3D rendering using QRhi, and renders all content in the window with QCanvasPainter. The rendering and QCanvasPainter's integration with QRhi is managed via QCanvasRhiPaintDriver.

#### Walkthrough

While choosing a 3D API to use based on the platform, the application also offers the possibility of requesting one using command-line arguments.

     int main(int argc, char *argv[])
     {
         QGuiApplication app(argc, argv);
         app.setAttribute(Qt::AA_SynthesizeTouchForUnhandledMouseEvents);

         QRhi::Implementation graphicsApi;
     #if defined(Q_OS_WIN)
         graphicsApi = QRhi::D3D11;
     #elif QT_CONFIG(metal)
         graphicsApi = QRhi::Metal;
     #elif QT_CONFIG(vulkan)
         graphicsApi = QRhi::Vulkan;
     #else
         graphicsApi = QRhi::OpenGLES2;
     #endif

         QCommandLineParser cmdLineParser;
         cmdLineParser.addHelpOption();
         QCommandLineOption nullOption({ "n", "null" }, QLatin1String("Null"));
         cmdLineParser.addOption(nullOption);
         QCommandLineOption glOption({ "g", "opengl" }, QLatin1String("OpenGL"));
         cmdLineParser.addOption(glOption);
         QCommandLineOption vkOption({ "v", "vulkan" }, QLatin1String("Vulkan"));
         cmdLineParser.addOption(vkOption);
         QCommandLineOption d3d11Option({ "d", "d3d11" }, QLatin1String("Direct3D 11"));
         cmdLineParser.addOption(d3d11Option);
         QCommandLineOption d3d12Option({ "D", "d3d12" }, QLatin1String("Direct3D 12"));
         cmdLineParser.addOption(d3d12Option);
         QCommandLineOption mtlOption({ "m", "metal" }, QLatin1String("Metal"));
         cmdLineParser.addOption(mtlOption);

         cmdLineParser.process(app);

         if (cmdLineParser.isSet(nullOption))
             graphicsApi = QRhi::Null;
         if (cmdLineParser.isSet(glOption))
             graphicsApi = QRhi::OpenGLES2;
         if (cmdLineParser.isSet(vkOption))
             graphicsApi = QRhi::Vulkan;
         if (cmdLineParser.isSet(d3d11Option))
             graphicsApi = QRhi::D3D11;
         if (cmdLineParser.isSet(d3d12Option))
             graphicsApi = QRhi::D3D12;
         if (cmdLineParser.isSet(mtlOption))
             graphicsApi = QRhi::Metal;

     #if QT_CONFIG(opengl)
         QSurfaceFormat fmt;
         fmt.setDepthBufferSize(24);
         fmt.setStencilBufferSize(8);
     #ifdef Q_OS_MACOS
         fmt.setVersion(4, 1);
         fmt.setProfile(QSurfaceFormat::CoreProfile);
     #endif
         QSurfaceFormat::setDefaultFormat(fmt);
     #endif

         MainWindow window(graphicsApi);
         window.resize(1920 / 2, 1080 / 2);
         window.show();

         return app.exec();
     }

MainWindow is a subclass of PainterWindow, which is a QWindow. PainterWindow implements a window that shows content rendered via QRhi, Qt's 3D graphics abstraction. Its task is to initialize a QRhi instance upon receiving an expose event, i.e., when the window is shown, manage a swapchain and a depth-stencil buffer, while also taking care of acting when the window is resized, and to invoke a virtual paint() function that is implemented in MainWindow.

For a better understanding of the PainterWindow implementation, it is recommended to check out the RHI Window Example, since in essence it is very similar to the RhiWindow class in that example.

One QCanvasPainter-specific step is to retrieve a QCanvasPainterFactory instance once a QRhi is successfully initialized:

         if (!m_factory) {
             m_factory = new QCanvasPainterFactory;
             m_factory->create(m_rhi.get());
         }

Rendering a new frame happens in the render() function, which is invoked when the window is shown, resized, and in response to update requests scheduled via requestUpdate().

Unlike in the RHI Window Example, we will not create vertex and uniform buffers or graphics pipelines via the QRhi API, and issue draw calls directly. Instead, QCanvasRhiPaintDriver is used to record a render pass with the QCanvasPainter-generated rendering.

     void PainterWindow::render()
     {
         if (!m_factory || !m_factory->isValid() || !m_rhi || !m_sc)
             return;

         if (!m_hasSwapChain || m_notExposed)
             return;

         if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) {
             resizeSwapChain();
             if (!m_hasSwapChain)
                 return;
             m_newlyExposed = false;
         }

         QRhi::FrameOpResult r = m_rhi->beginFrame(m_sc.get());
         if (r == QRhi::FrameOpSwapChainOutOfDate) {
             resizeSwapChain();
             if (!m_hasSwapChain)
                 return;
             r = m_rhi->beginFrame(m_sc.get());
         }
         if (r != QRhi::FrameOpSuccess) {
             qDebug("beginFrame failed with %d, retry", r);
             requestUpdate();
             return;
         }

         QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
         QRhiRenderTarget *rt = m_sc->currentFrameRenderTarget();

         QCanvasRhiPaintDriver *pd = m_factory->paintDriver();
         QCanvasPainter *painter = m_factory->painter();

         pd->resetForNewFrame();
         pd->beginPaint(cb, rt, m_fillColor, size(), float(devicePixelRatio()));
         paint(painter);
         pd->endPaint();

         m_rhi->endFrame(m_sc.get());
     }

MainWindow's paint() implementation works with the QCanvasPainter API, and does not need to worry about low-level details such as the QRhi.

First, it loads and registers images if not yet done:

     void MainWindow::paint(QCanvasPainter *p)
     {
         if (!m_initialized) {
             auto flags = QCanvasPainter::ImageFlag::GenerateMipmaps;
             m_b1ImageLight = p->addImage(QImage(":/images/icon_random_light.png"), flags);
             m_b1ImageDark = p->addImage(QImage(":/images/icon_random_dark.png"), flags);
             m_b2ImageLight = p->addImage(QImage(":/images/icon_theme_light.png"), flags);
             m_b2ImageDark = p->addImage(QImage(":/images/icon_theme_dark.png"), flags);
             m_b3ImageLight = p->addImage(QImage(":/images/icon_settings_light.png"), flags);
             m_b3ImageDark = p->addImage(QImage(":/images/icon_settings_dark.png"), flags);
             m_sImageLight = p->addImage(QImage(":/images/icon_run_light.png"), flags);
             m_sImageDark = p->addImage(QImage(":/images/icon_run_dark.png"), flags);
             m_initialized = true;
         }

This is followed by the actual drawing, involving the filling and stroking of paths, rendering text, and drawing images. For example:

         // Highlight pressed button
         if (m_selectedButton) {
             p->beginPath();
             p->roundRect(m_views[m_selectedButton].rect, viewRadius);
             p->setLineWidth(2.0f * m_px);
             p->setStrokeStyle(m_theme.highlight());
             p->stroke();
         }

The QCanvasPainter fill and stroke calls prepare vertex, index, uniform data and QRhi-based draw calls internally. The stream of commands is flushed when endPaint() is called in PainterWindow::render(). That is where a render pass is recorded on the associated QRhiCommandBuffer.

Before returning, MainWindow::paint() calls requestUpdate(), a QWindow function that requests a new frame, synchronized with the display's presentation rate. This is what ensures that the window content is continuously updated and so it can animate.

Check the full example source code linked below for details.

Example project @ code.qt.io