Qt Canvas Painter - Compact Health Example
Demonstrates the use of QCPainter 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 QCPainter. The rendering and QCPainter's integration with QRhi is managed via QCRhiPaintDriver.
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 QCPainter-specific step is to retrieve a QCPainterFactory instance once a QRhi is successfully initialized:
if (!m_factory) {
m_factory = new QCPainterFactory;
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, QCRhiPaintDriver is used to record a render pass with the QCPainter-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();
QCRhiPaintDriver *pd = m_factory->paintDriver();
QCPainter *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 QCPainter 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(QCPainter *p)
{
if (!m_initialized) {
auto flags = QCPainter::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 QCPainter 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.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.