Integrating Custom Scene Graph Geometry with QML for Dynamic Path Rendering

Overview

This example demonstrates how to bridge C++ and QML by exposing a custom scene graph item for rendering dynamic Bézier curves. By subclassing QQuickItem and utilizing the Qt meta-object system, developers can create high-performance graphics that respond to QML-driven animations while maintaining clean separation between logic and presentation layers.

C++ Module Registration

The application entry point initializes the GUI runtime and binds the custom geometry class to the QML engine. The qmlRegisterType template function maps the C++ type to a specific module namespace, making it instantly available for declarative instantiation.

#include <QGuiApplication>
#include <QQuickView>
#include <QQmlEngine>

#include "pathgeometryrenderer.h"

int main(int argc, char *argv[])
{
    QGuiApplication runtime(argc, argv);

    qmlRegisterType<PathGeometryRenderer>(
        "CustomShapes", 1, 0, "DynamicPath"
    );

    QQuickView renderer;
    QSurfaceFormat surfaceConfig = renderer.format();
    surfaceConfig.setSamples(16); // Enable MSAA for anti-aliased lines
    renderer.setFormat(surfaceConfig);
    renderer.setSource(QUrl(QStringLiteral("qrc:/views/main.qml")));
    renderer.show();

    return runtime.exec();
}

Defining the Custom Item

The core component inherits from QQuickItem and exposes curve parameters through the Qt property system. Each Q_PROPERTY declaration pairs a getter, a setter, and a change notification signal, enabling automatic data binding and change tracking from the QML layer.

#include <QQuickItem>
#include <QPointF>

class PathGeometryRenderer : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(QPointF startPoint READ startPoint WRITE setStartPoint NOTIFY startChanged)
    Q_PROPERTY(QPointF controlOne READ controlOne WRITE setControlOne NOTIFY ctrlOneChanged)
    Q_PROPERTY(QPointF controlTwo READ controlTwo WRITE setControlTwo NOTIFY ctrlTwoChanged)
    Q_PROPERTY(QPointF endPoint READ endPoint WRITE setEndPoint NOTIFY endChanged)
    Q_PROPERTY(int meshDensity READ meshDensity WRITE setMeshDensity NOTIFY densityChanged)

public:
    explicit PathGeometryRenderer(QQuickItem *parent = nullptr);
    ~PathGeometryRenderer() override;

    QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data) override;

    QPointF startPoint() const { return m_start; }
    QPointF controlOne() const { return m_ctrl1; }
    QPointF controlTwo() const { return m_ctrl2; }
    QPointF endPoint() const { return m_end; }
    int meshDensity() const { return m_density; }

    void setStartPoint(const QPointF &pt);
    void setControlOne(const QPointF &pt);
    void setControlTwo(const QPointF &pt);
    void setEndPoint(const QPointF &pt);
    void setMeshDensity(int count);

signals:
    void startChanged(const QPointF &pt);
    void ctrlOneChanged(const QPointF &pt);
    void ctrlTwoChanged(const QPointF &pt);
    void endChanged(const QPointF &pt);
    void densityChanged(int count);

private:
    QPointF m_start{0, 0};
    QPointF m_ctrl1{0, 0};
    QPointF m_ctrl2{0, 0};
    QPointF m_end{0, 0};
    int m_density = 32;
};

The corresponding setter methods validate incoming values, update internal state, emit the associated signal, and invoke update(). This triggers the scene graph to rebuild the geometry only when actual property mutations occur, preventing unnecessary frame drops.

QML Integration and Animation

Inside the QML document, the registered module is imported and instantiated. Properties bound to animation sequences automatically invoke the C++ setters, resulting in synchronized, hardware-accelerated rendering without manual repaint loops.

import QtQuick 2.15
import CustomShapes 1.0

Rectangle {
    width: 320
    height: 240
    color: "#1a1a1a"

    DynamicPath {
        id: pathView
        anchors.fill: parent
        anchors.margins: 25

        property real progress: 0.0

        // Loop a normalized value that drives control point interpolation
        NumberAnimation on progress {
            from: 0.0; to: 1.0
            duration: 2500
            easing.type: Easing.InOutCubic
            loops: Animation.Infinite
        }

        controlOne: Qt.point(progress * 0.8, 0.9 - progress)
        controlTwo: Qt.point(1.0 - progress, progress * 0.7)
        meshDensity: 48
    }

    Label {
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        anchors.margins: 15
        color: "#cccccc"
        font.pixelSize: 12
        text: "Hardware-accelerated path rendered via Qt Scene Graph"
    }
}

The updatePaintNode override, called automatically by the rendering backend, trnaslates these normalized coordinates into low-level scene graph primitives such as QSGGeometryNode configured with GL_LINE_STRIP. Managing node allocation and reuse within this function ensures memory stability while sustaining consistent frame rates during continuous property interpolation.

Tags: qt-quick qml-cpp-integration qt-scene-graph custom-geometry bezier-curves

Posted on Sun, 30 Aug 2026 16:21:42 +0000 by neodaemon