SVG Icons in C++ with WinUI & Qt

By the Axialis Engineering team ·

SVG Icons in C++ with WinUI & Qt

Native C++ toolkits have no built-in SVG icon pipeline, so teams end up shipping dozens of PNG sizes or hand-tuned bitmaps that blur on high-DPI screens. This guide shows how to export a clean SVG from Axialis IconVectors and render it sharply in WinUI (Windows) and Qt (cross-platform), with code you can paste directly into a control.

You get three concrete paths: load an SVG at runtime with SvgImageSource or QSvgRenderer, export XAML geometry into a PathIcon for brush-based theming, or rasterize off-screen with LunaSVG when you control the drawing yourself.

The SVG file is parsed once by an SVG library and rasterized at the pixel size each display scale needs, then handed to the toolkit as a toolkit bitmap icon.svgone sourcean SVG libraryLunaSVG, NanoSVG, othersparses the vector oncepaths, fills, strokesrasterize at the target size16 pxat 100%24 pxat 150%32 pxat 200%a toolkit bitmapHBITMAP, wxBitmap, QPixmap...crisp at 1x, 2x, 3xwhole-pixel sizes onlyno scaling of finished bitmapsRender on demand for the DPI the window actually has; never scale a bitmap that was drawn for another one.
Vector in, pixels out, at the right moment. an SVG library parses the file once; you ask for a bitmap at the size the current display scale needs, 16 px at 100 percent, 24 at 150, 32 at 200, and hand the result to the toolkit as a toolkit bitmap. The toolkit never sees the vector, only a bitmap drawn for the screen in front of the user.

Why SVG for native C++ apps?

Step-by-step: export in IconVectors, then render in C++

  1. Export a clean SVG from IconVectors
    • Open or create: File → Open… (Ctrl+O) or New Icon (Ctrl+N).
    • Theme-ready paint: set fills/strokes to currentColor so you can recolor in apps.
    • Compact output: File → Export → Export Minified (Shift+Ctrl+M) for a lean SVG with the correct viewBox.
    • Alternate for WinUI: to bind color via XAML brushes, export geometry: File → Export → Export to XAML (Shift+Ctrl+X).
    An icon open in the IconVectors editor, ready to export as SVG or XAML
  2. WinUI: load an SVG at runtime (uses SvgImageSource)

    XAML — bind an SVG file packaged in Assets/Icons/:

    <Image Width="24" Height="24">
      <Image.Source>
        <SvgImageSource UriSource="ms-appx:///Assets/Icons/check.svg"/>
      </Image.Source>
    </Image>

    C++/WinRT — create and set the source programmatically:

    #include <winrt/Microsoft.UI.Xaml.Media.Imaging.h>
    using namespace winrt::Microsoft::UI::Xaml::Media::Imaging;
    using namespace winrt::Windows::Foundation;
    
    SvgImageSource svg{ Uri{ L"ms-appx:///Assets/Icons/check.svg" } };
    // myImage is an Image control reference
    myImage().Source(svg);
    Tip: this approach is best when you keep the SVG's own colors. For app-driven theming, use XAML geometry in the next step.
  3. WinUI: export XAML paths for brush-based theming

    Export geometry from IconVectors via File → Export → Export to XAML (Shift+Ctrl+X) and paste into a PathIcon. Now you can recolor with the control's Foreground brush.

    <PathIcon Width="20" Height="20"
              Foreground="{ThemeResource SystemControlForegroundBaseHighBrush}"
              Data="M4 12 8 16 20 4"/>

    Use theme resources or your own SolidColorBrush to match light/dark modes dynamically.

  4. Qt: render an SVG with QSvgRenderer

    Package the icon in a Qt resource (.qrc) so it ships inside the app binary:

    <RCC>
      <qresource prefix="/icons">
        <file>icons/check.svg</file>
      </qresource>
    </RCC>

    Render to a QPixmap and create a QIcon:

    #include <QSvgRenderer>
    #include <QPainter>
    #include <QIcon>
    #include <QPixmap>
    
    QSvgRenderer renderer(QStringLiteral(":/icons/check.svg"));
    QPixmap pm(24, 24); pm.fill(Qt::transparent);
    QPainter p(&pm);
    renderer.render(&p);
    p.end();
    
    QIcon icon(pm);
    myButton->setIcon(icon);
  5. Qt: recolor at runtime (uses currentColor)

    Export SVGs from IconVectors with fill="currentColor"/stroke="currentColor". To theme, replace currentColor with a hex color in memory, then render:

    #include <QFile>
    #include <QSvgRenderer>
    
    QIcon makeIconColored(const QString& svgPath, const QColor& color, const QSize& size) {
      QFile f(svgPath);
      if (!f.open(QIODevice::ReadOnly)) return QIcon{};
      QByteArray xml = f.readAll();
      xml.replace("currentColor", color.name(QColor::HexRgb).toUtf8());
    
      QSvgRenderer r(xml);
      QPixmap pm(size); pm.fill(Qt::transparent);
      QPainter p(&pm); r.render(&p); p.end();
      return QIcon(pm);
    }
    
    // Usage:
    auto themed = makeIconColored(":/icons/check.svg", QColor("#2563EB"), QSize(20,20));
    myAction->setIcon(themed);
  6. Package & verify
    • WinUI: put SVGs under Assets/Icons/, build action Content, and ensure they copy to output.
    • Qt: register in .qrc; reference via :/icons/name.svg.
    • Final check: open the SVG in IconVectors' read-only viewer (View → Source Code (F3)) to confirm viewBox and currentColor are present for theming.

Render a SVG icon using LunaSVG

LunaSVG is a lightweight, MIT-licensed C++ library that parses and renders SVG to an RGBA bitmap buffer. It targets static SVG rendering (no scripting or animation) and covers most of SVG 1.1/Tiny. It installs via Homebrew, vcpkg, and Conan, and builds cleanly with CMake.

Install options

Minimal C++ example for Windows (render to RGBA and convert to HBITMAP)

The snippet below loads an SVG, rasterizes it to a LunaSVG Bitmap, then converts it to a Windows HBITMAP. The LunaSVG API centers on lunasvg::Document, loaded with loadFromData() (or loadFromFile()) and rendered with renderToBitmap().

Embedding the SVG as a string literal (static const char kSVGContent[]) keeps the icon inside the binary, so the executable needs no external file at runtime. Paste the minified SVG from IconVectors straight into the raw-string block.


#include <lunasvg.h>

#include <windows.h>   // HBITMAP, CreateDIBSection, etc.
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <iostream>

using namespace lunasvg;

// Helper: copy a lunasvg::Bitmap into a 32-bpp top-down DIB HBITMAP.
static HBITMAP CreateHBITMAPFromLunaBitmap(const lunasvg::Bitmap& bmp)
{
    if (!bmp.valid()) return nullptr;

    const int width     = bmp.width();
    const int height    = bmp.height();
    const int srcStride = bmp.stride();       // bytes per row from LunaSVG (ARGB premultiplied)
    const int dstStride = width * 4;          // BGRA premultiplied for 32bpp DIB

    BITMAPINFO bi{};
    bi.bmiHeader.biSize        = sizeof(BITMAPINFOHEADER);
    bi.bmiHeader.biWidth       = width;
    bi.bmiHeader.biHeight      = -height;     // top-down DIB
    bi.bmiHeader.biPlanes      = 1;
    bi.bmiHeader.biBitCount    = 32;
    bi.bmiHeader.biCompression = BI_RGB;

    void* dibData = nullptr;
    HDC hdc = GetDC(nullptr);
    HBITMAP hbmp = CreateDIBSection(hdc, &bi, DIB_RGB_COLORS, &dibData, nullptr, 0);
    ReleaseDC(nullptr, hdc);
    if (!hbmp || !dibData) return nullptr;

    const uint8_t* src = bmp.data();              // ARGB premultiplied (A,R,G,B)
    uint8_t* dst       = static_cast<uint8_t*>(dibData); // BGRA premultiplied (B,G,R,A)

    for (int y = 0; y < height; ++y)
    {
        const uint8_t* srow = src + y * srcStride;
        uint8_t* drow       = dst + y * dstStride;

        // Convert per pixel: ARGB -> BGRA
        for (int x = 0; x < width; ++x)
        {
            const uint8_t A = srow[x*4 + 0];
            const uint8_t R = srow[x*4 + 1];
            const uint8_t G = srow[x*4 + 2];
            const uint8_t B = srow[x*4 + 3];

            drow[x*4 + 0] = B; // B
            drow[x*4 + 1] = G; // G
            drow[x*4 + 2] = R; // R
            drow[x*4 + 3] = A; // A (premultiplied already)
        }
    }
    return hbmp;
}

// Export the minified code from IconVectors and copy it here
static const char kSVGContent[] = R"SVG(
<svg width="400" height="200" xmlns="http://www.w3.org/2000/svg">
  <rect id="red-rect" x="20" y="20" width="100" height="100" fill="red"/>
  <circle id="blue-circle" cx="200" cy="70" r="50" fill="blue"/>
  <rect id="green-rect" x="300" y="30" width="70" height="130" fill="green"/>
</svg>
)SVG";

int main()
{
    // 1) Load the SVG from in-memory text
    auto document = Document::loadFromData(kSVGContent);
    if (!document)
    {
        std::cerr << "Failed to load SVG.\n";
        return 1;
    }

    // 2) Render to a bitmap. You can specify explicit pixel size if you like:
    //    auto bmp = document->renderToBitmap(400, 200);
    auto bmp = document->renderToBitmap();
    if (!bmp.valid())
    {
        std::cerr << "Failed to render SVG.\n";
        return 2;
    }

    // 3) Convert to HBITMAP (32-bpp top-down DIB)
    HBITMAP hbmp = CreateHBITMAPFromLunaBitmap(bmp);
    if (!hbmp)
    {
        std::cerr << "Failed to create HBITMAP.\n";
        return 3;
    }

    // TODO: use hbmp with GDI/GDI+ or assign to a control.
    // Example: select into a memory DC, BitBlt to a window DC, etc.

    // 4) Cleanup when done
    DeleteObject(hbmp);
    return 0;
}

Notes

Optional: blit the HBITMAP with per-pixel alpha

// After you get HBITMAP hbmp:
HDC hdcScreen = GetDC(hwnd);
HDC hdcMem    = CreateCompatibleDC(hdcScreen);
HBITMAP old   = (HBITMAP)SelectObject(hdcMem, hbmp);

BLENDFUNCTION bf{};
bf.BlendOp             = AC_SRC_OVER;
bf.BlendFlags          = 0;
bf.SourceConstantAlpha = 255;       // use per-pixel alpha
bf.AlphaFormat         = AC_SRC_ALPHA;

AlphaBlend(hdcScreen, dstX, dstY, width, height,
           hdcMem,     0,    0,  width, height, bf);

SelectObject(hdcMem, old);
DeleteDC(hdcMem);
ReleaseDC(hwnd, hdcScreen);

Pair with IconVectors exports
For best results, export icons from IconVectors as minified SVG with a clean viewBox and currentColor paints, then rasterize with LunaSVG—or keep SVGs vector and recolor upstream.

Troubleshooting

Related guides

Start Making SVG Icons Today with IconVectors

Download the fully-functional 30‑Day Free Trial and unlock your icon design workflow.

Version 2.10 for Windows - October 6, 2026. macOS and Linux: version 2.00 available now, 2.10 coming soon.