Scene Graph - Benutzerdefinierte Geometrie

Zeigt, wie man eine benutzerdefinierte Geometrie in den Qt Quick Scene Graph implementiert.

Das Beispiel für benutzerdefinierte Geometrie zeigt, wie eine QQuickItem erstellt wird, die die API des Szenegraphs verwendet, um eine benutzerdefinierte Geometrie für den Szenegraph zu erstellen. Dazu wird ein BezierCurve Element erstellt, das Teil des CustomGeometry-Moduls ist und dieses in einer QML-Datei verwendet.

BezierCurve Deklaration

#include <QtQuick/QQuickItem>

class BezierCurve : public QQuickItem
{
    Q_OBJECT

    Q_PROPERTY(QPointF p1 READ p1 WRITE setP1 NOTIFY p1Changed)
    Q_PROPERTY(QPointF p2 READ p2 WRITE setP2 NOTIFY p2Changed)
    Q_PROPERTY(QPointF p3 READ p3 WRITE setP3 NOTIFY p3Changed)
    Q_PROPERTY(QPointF p4 READ p4 WRITE setP4 NOTIFY p4Changed)

    Q_PROPERTY(int segmentCount READ segmentCount WRITE setSegmentCount NOTIFY segmentCountChanged)
    QML_ELEMENT

public:
    BezierCurve(QQuickItem *parent = nullptr);
    ~BezierCurve();

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

    QPointF p1() const { return m_p1; }
    QPointF p2() const { return m_p2; }
    QPointF p3() const { return m_p3; }
    QPointF p4() const { return m_p4; }

    int segmentCount() const { return m_segmentCount; }

    void setP1(const QPointF &p);
    void setP2(const QPointF &p);
    void setP3(const QPointF &p);
    void setP4(const QPointF &p);

    void setSegmentCount(int count);

signals:
    void p1Changed(const QPointF &p);
    void p2Changed(const QPointF &p);
    void p3Changed(const QPointF &p);
    void p4Changed(const QPointF &p);

    void segmentCountChanged(int count);

private:
    QPointF m_p1;
    QPointF m_p2;
    QPointF m_p3;
    QPointF m_p4;

    int m_segmentCount;
};

Die Item-Deklaration ist eine Unterklasse der Klasse QQuickItem und fügt fünf Eigenschaften hinzu. Eine für jeden der vier Kontrollpunkte in der Bezier-Kurve und einen Parameter zur Steuerung der Anzahl der Segmente, in die die Kurve unterteilt ist. Für jede dieser Eigenschaften haben wir entsprechende Getter- und Setter-Funktionen. Da diese Eigenschaften in QML gebunden werden können, ist es auch besser, für jede von ihnen Notifier-Signale zu haben, damit Änderungen von der QML-Engine aufgenommen und entsprechend verwendet werden.

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

Der Synchronisationspunkt zwischen der QML-Szene und dem Rendering-Szenengraph ist die virtuelle Funktion QQuickItem::updatePaintNode(), die alle Elemente mit benutzerdefinierter Szenengraph-Logik implementieren müssen.

Hinweis: Bei vielen Hardwarekonfigurationen wird der Szenegraph in einem separaten Thread gerendert. Daher ist es von entscheidender Bedeutung, dass die Interaktion mit dem Szenengraphen auf kontrollierte Weise erfolgt, in erster Linie durch die Funktion QQuickItem::updatePaintNode().

BezierCurve-Implementierung

BezierCurve::BezierCurve(QQuickItem *parent)
    : QQuickItem(parent)
    , m_p1(0, 0)
    , m_p2(1, 0)
    , m_p3(0, 1)
    , m_p4(1, 1)
    , m_segmentCount(32)
{
    setFlag(ItemHasContents, true);
}

Der BezierCurve-Konstruktor legt Standardwerte für die Kontrollpunkte und die Anzahl der Segmente fest. Die Bezier-Kurve wird in normalisierten Koordinaten relativ zum Begrenzungsrechteck des Elements angegeben.

Der Konstruktor setzt auch das Flag QQuickItem::ItemHasContents. Dieses Flag teilt dem Canvas mit, dass dieses Element visuelle Inhalte bereitstellt und QQuickItem::updatePaintNode() aufruft, wenn es an der Zeit ist, die QML-Szene mit dem Rendering-Szenengraph zu synchronisieren.

BezierCurve::~BezierCurve() = default;

Die BezierCurve-Klasse hat keine Datenelemente, die bereinigt werden müssen, so dass der Destruktor nichts tut. Es ist erwähnenswert, dass der Rendering-Szenengraph vom Szenengraph selbst verwaltet wird, möglicherweise in einem anderen Thread, so dass man niemals QSGNode Referenzen in der QQuickItem Klasse behalten oder versuchen sollte, sie explizit zu bereinigen.

void BezierCurve::setP1(const QPointF &p)
{
    if (p == m_p1)
        return;

    m_p1 = p;
    emit p1Changed(p);
    update();
}

Die Setter-Funktion für die Eigenschaft p1 prüft, ob der Wert unverändert ist, und verlässt die Funktion vorzeitig, wenn dies der Fall ist. Dann aktualisiert sie den internen Wert und gibt das geänderte Signal aus. Anschließend wird die Funktion QQuickItem::update() aufgerufen, die dem Rendering-Szenengraph mitteilt, dass sich der Zustand dieses Objekts geändert hat und mit dem Rendering-Szenengraph synchronisiert werden muss. Ein Aufruf von update() führt zu einem späteren Zeitpunkt zu einem Aufruf von QQuickItem::updatePaintNode().

Die anderen Eigenschaftssetzer sind gleichwertig und werden in diesem Beispiel weggelassen.

QSGNode *BezierCurve::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
    QSGGeometryNode *node = nullptr;
    QSGGeometry *geometry = nullptr;

    if (!oldNode) {
        node = new QSGGeometryNode;

Die Funktion updatePaintNode() ist der primäre Integrationspunkt für die Synchronisierung des Zustands der QML-Szene mit dem Rendering-Szenengraph. Der Funktion wird eine QSGNode übergeben, die Instanz, die beim letzten Aufruf der Funktion zurückgegeben wurde. Beim ersten Aufruf der Funktion ist sie null und wir erstellen unsere QSGGeometryNode, die wir mit Geometrie und einem Material füllen werden.

        geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), m_segmentCount);
        geometry->setLineWidth(2);
        geometry->setDrawingMode(QSGGeometry::DrawLineStrip);
        node->setGeometry(geometry);
        node->setFlag(QSGNode::OwnsGeometry);

Anschließend erstellen wir die Geometrie und fügen sie dem Knoten hinzu. Das erste Argument des QSGGeometry -Konstruktors ist eine Definition des Vertex-Typs, ein so genannter "Attributsatz". Da sich die in QML häufig verwendeten Grafiken auf einige wenige Standard-Attributsätze konzentrieren, werden diese standardmäßig bereitgestellt. Hier verwenden wir das Point2D-Attribut-Set, das zwei Fließkommazahlen hat, eine für x-Koordinaten und eine für y-Koordinaten. Das zweite Argument ist die Anzahl der Scheitelpunkte.

Es können auch benutzerdefinierte Attributsätze erstellt werden, aber das wird in diesem Beispiel nicht behandelt.

Da wir keine besonderen Anforderungen an die Speicherverwaltung der Geometrie haben, geben wir an, dass die QSGGeometryNode die Geometrie besitzen soll.

Um Zuweisungen zu minimieren, die Speicherfragmentierung zu reduzieren und die Leistung zu verbessern, wäre es auch möglich, die Geometrie zu einem Mitglied einer QSGGeometryNode Unterklasse zu machen, in diesem Fall hätten wir das QSGGeometryNode::OwnsGeometry Flag nicht gesetzt.

        auto *material = new QSGFlatColorMaterial;
        material->setColor(QColor(255, 0, 0));
        node->setMaterial(material);
        node->setFlag(QSGNode::OwnsMaterial);

Die Szenengraphen-API bietet ein paar häufig verwendete Material-Implementierungen. In diesem Beispiel verwenden wir das Material QSGFlatColorMaterial, das die durch die Geometrie definierte Form mit einer Volltonfarbe füllt. Auch hier übergeben wir den Besitz des Materials an den Knoten, damit es vom Szenengraphen aufgeräumt werden kann.

    } else {
        node = static_cast<QSGGeometryNode *>(oldNode);
        geometry = node->geometry();
        geometry->allocate(m_segmentCount);
    }

Wenn sich das QML-Element geändert hat und wir nur die Geometrie des bestehenden Knotens ändern wollen, wandeln wir oldNode in eine QSGGeometryNode -Instanz um und extrahieren seine Geometrie. Falls sich die Anzahl der Segmente geändert hat, rufen wir QSGGeometry::allocate() auf, um sicherzustellen, dass es die richtige Anzahl von Scheitelpunkten hat.

    QSizeF itemSize = size();
    QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();
    for (int i = 0; i < m_segmentCount; ++i) {
        qreal t = i / qreal(m_segmentCount - 1);
        qreal invt = 1 - t;

        QPointF pos = invt * invt * invt * m_p1
                    + 3 * invt * invt * t * m_p2
                    + 3 * invt * t * t * m_p3
                    + t * t * t * m_p4;

        float x = pos.x() * itemSize.width();
        float y = pos.y() * itemSize.height();

        vertices[i].set(x, y);
    }
    node->markDirty(QSGNode::DirtyGeometry);

Um die Geometrie zu füllen, extrahieren wir zuerst das Scheitelpunkt-Array aus ihr. Da wir einen der Standard-Attribut-Sets verwenden, können wir die Komfortfunktion QSGGeometry::vertexDataAsPoint2D() benutzen. Dann gehen wir durch jedes Segment, berechnen seine Position und schreiben diesen Wert in den Scheitelpunkt.

    return node;
}

Am Ende der Funktion geben wir den Knoten zurück, damit der Szenegraph ihn rendern kann.

Anwendung Entry-Point

int main(int argc, char **argv)
{
    QGuiApplication app(argc, argv);

    QQuickView view;
    QSurfaceFormat format = view.format();
    format.setSamples(16);
    view.setFormat(format);
    view.setSource(QUrl("qrc:///scenegraph/customgeometry/main.qml"));
    view.show();

    return app.exec();
}

Die Anwendung ist eine einfache QML-Anwendung, mit einer QGuiApplication und einer QQuickView, der wir eine .qml-Datei übergeben.

    QML_ELEMENT

Um das Element BezierCurve zu verwenden, müssen wir es in der QML-Engine registrieren, indem wir das Makro QML_ELEMENT verwenden. Dadurch erhält es den Namen BezierCurve und wird Teil des Moduls CustomGeometry 1.0, wie es in den Build-Dateien des Projekts definiert ist:

# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

cmake_minimum_required(VERSION 3.16)
project(customgeometry_declarative LANGUAGES CXX)

find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick)

qt_standard_project_setup()

qt_add_executable(customgeometry_declarative WIN32 MACOSX_BUNDLE
    beziercurve.cpp beziercurve.h
    main.cpp
)

target_link_libraries(customgeometry_declarative PRIVATE
    Qt6::Core
    Qt6::Gui
    Qt6::Quick
)

qt_add_qml_module(customgeometry_declarative
    URI CustomGeometry
    QML_FILES main.qml
    RESOURCE_PREFIX /scenegraph/customgeometry
    NO_RESOURCE_TARGET_PATH
)

install(TARGETS customgeometry_declarative
    BUNDLE  DESTINATION .
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

qt_generate_deploy_qml_app_script(
    TARGET customgeometry_declarative
    OUTPUT_SCRIPT deploy_script
    MACOS_BUNDLE_POST_BUILD
    NO_UNSUPPORTED_PLATFORM_ERROR
    DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM
)
install(SCRIPT ${deploy_script})
TARGET = customgeometry
QT += quick

CONFIG += qmltypes
QML_IMPORT_NAME = CustomGeometry
QML_IMPORT_MAJOR_VERSION = 1

SOURCES += \
    main.cpp \
    beziercurve.cpp

HEADERS += \
    beziercurve.h

RESOURCES += customgeometry.qrc

target.path = $$[QT_INSTALL_EXAMPLES]/quick/scenegraph/customgeometry
INSTALLS += target

Da die Bézier-Kurve als Linienstreifen gezeichnet wird, geben wir an, dass die Ansicht mehrfach abgetastet werden soll, um Antialiasing zu erhalten. Dies ist nicht erforderlich, aber es lässt das Element auf Hardware, die dies unterstützt, etwas schöner aussehen. Multisampling ist nicht standardmäßig aktiviert, da es oft zu einer höheren Speichernutzung führt.

Verwenden des Objekts

import QtQuick
import CustomGeometry

Unsere .qml-Datei importiert das Modul QtQuick 2.0, um die Standardtypen zu erhalten, und auch unser eigenes Modul CustomGeometry 1.0, das unsere neu erstellten BezierCurve-Objekte enthält.

Item {
    width: 300
    height: 200

    BezierCurve {
        id: line
        anchors.fill: parent
        anchors.margins: 20

Dann erstellen wir unser Wurzelelement und eine Instanz der BezierCurve, die wir verankern, um die Wurzel zu füllen.

        property real t
        SequentialAnimation on t {
            NumberAnimation { to: 1; duration: 2000; easing.type: Easing.InOutQuad }
            NumberAnimation { to: 0; duration: 2000; easing.type: Easing.InOutQuad }
            loops: Animation.Infinite
        }

        p2: Qt.point(t, 1 - t)
        p3: Qt.point(1 - t, t)
    }

Um das Beispiel etwas interessanter zu gestalten, fügen wir eine Animation hinzu, um die beiden Kontrollpunkte der Kurve zu verändern. Die Endpunkte bleiben unverändert.

    Text {
        anchors.bottom: line.bottom

        x: 20
        width: parent.width - 40
        wrapMode: Text.WordWrap

        text: qsTr("This curve is a custom scene graph item, implemented using line strips")
    }
}

Schließlich wird ein kurzer Text eingeblendet, der das Beispiel erläutert.

Beispielprojekt @ code.qt.io

© 2025 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.