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