场景图 - 自定义几何体
展示如何在Qt Quick 场景图中实现自定义几何体。
自定义几何示例展示了如何创建一个QQuickItem ,使用场景图 API 为场景图创建自定义几何。它通过创建BezierCurve
项目来实现,该项目是 CustomGeometry 模块的一部分,并在 QML 文件中加以使用。
贝塞尔曲线声明
#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; };
item 声明是QQuickItem 类的子类,并添加了五个属性。贝塞尔曲线的四个控制点各有一个属性,还有一个参数用于控制曲线细分的段数。对于每个属性,我们都有相应的 getter 和 setter 函数。由于这些属性可以在 QML 中绑定,因此最好为每个属性都设置通知信号,这样 QML 引擎就能捕捉到变化并进行相应处理。
QML 场景和渲染场景图之间的同步点是虚拟函数QQuickItem::updatePaintNode() ,所有带有自定义场景图逻辑的项目都必须实现该函数。
注意: 在许多硬件配置中,场景图会在单独的线程上渲染。因此,与场景图的交互必须以受控的方式进行,首先是通过QQuickItem::updatePaintNode() 函数。
贝塞尔曲线的实现
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); }
贝塞尔曲线构造函数为控制点和线段数设置默认值。贝塞尔曲线是以相对于项目边界矩形的归一化坐标指定的。
构造函数还会设置标志QQuickItem::ItemHasContents 。该标志告诉画布该项目提供可视化内容,并在 QML 场景与渲染场景图同步时调用QQuickItem::updatePaintNode() 。
BezierCurve::~BezierCurve() = default;
BezierCurve 类没有需要清理的数据成员,因此析构函数什么也不做。值得一提的是,渲染场景图是由场景图自己管理的,可能在不同的线程中,所以永远不要在QQuickItem 类中保留QSGNode 引用,也不要试图显式地清理它们。
void BezierCurve::setP1(const QPointF &p) { if (p == m_p1) return; m_p1 = p; emit p1Changed(p); update(); }
p1 属性的 setter 函数会检查值是否未变,如果未变则提前退出。然后更新内部值,并发出更改信号。然后继续调用QQuickItem::update() 函数,该函数将通知渲染场景图,该对象的状态已发生变化,需要与渲染场景图同步。调用 update() 会导致稍后调用QQuickItem::updatePaintNode() 。
本例中省略了其他等价的属性设置器。
QSGNode *BezierCurve::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { QSGGeometryNode *node = nullptr; QSGGeometry *geometry = nullptr; if (!oldNode) { node = new QSGGeometryNode;
updatePaintNode() 函数是 QML 场景状态与渲染场景图同步的主要整合点。函数传递给QSGNode ,它是上次调用该函数时返回的实例。第一次调用该函数时,它将为空,我们将创建QSGGeometryNode ,并在其中填充几何体和材质。
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), m_segmentCount); geometry->setLineWidth(2); geometry->setDrawingMode(QSGGeometry::DrawLineStrip); node->setGeometry(geometry); node->setFlag(QSGNode::OwnsGeometry);
然后,我们创建几何体并将其添加到节点中。QSGGeometry 构造函数的第一个参数是顶点类型定义,称为 "属性集"。由于 QML 中经常使用的图形都围绕着几个常用的标准属性集,因此默认提供了这些属性集。这里我们使用 Point2D 属性集,它有两个浮点数,一个是 x 坐标,一个是 y 坐标。第二个参数是顶点数。
也可以创建自定义属性集,但本示例不涉及。
由于我们对几何体的内存管理没有特殊需求,因此我们指定QSGGeometryNode 拥有几何体。
为了最大限度地减少分配、减少内存碎片并提高性能,也可以将几何体作为QSGGeometryNode 子类的成员,在这种情况下,我们将不设置 QSGGeometryNode::OwnsGeometry 标志。
auto *material = new QSGFlatColorMaterial; material->setColor(QColor(255, 0, 0)); node->setMaterial(material); node->setFlag(QSGNode::OwnsMaterial);
场景图 API 提供了一些常用的材质实现。在本例中,我们使用QSGFlatColorMaterial ,它将用纯色填充几何体定义的形状。我们再次将材质的所有权传递给节点,以便由场景图进行清理。
} else { node = static_cast<QSGGeometryNode *>(oldNode); geometry = node->geometry(); geometry->allocate(m_segmentCount); }
如果 QML 项目已更改,而我们只想修改现有节点的几何图形,我们将oldNode
转换为QSGGeometryNode 实例并提取其几何图形。如果线段数发生变化,我们会调用QSGGeometry::allocate() 来确保它有正确的顶点数。
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);
为了填充几何体,我们首先从中提取顶点数组。由于我们使用的是默认属性集之一,因此可以使用便捷函数QSGGeometry::vertexDataAsPoint2D()。然后,我们逐段计算其位置,并将该值写入顶点。
return node; }
函数结束时,我们将返回节点,以便场景图可以渲染它。
应用程序入口点
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(); }
该应用程序是一个简单的 QML 应用程序,它有一个QGuiApplication 和一个QQuickView ,我们通过 传递一个 .qml 文件。
QML_ELEMENT
要使用 BezierCurve 项目,我们需要使用QML_ELEMENT 宏在 QML 引擎中注册它。这样,它就被命名为 BezierCurve,并成为项目构建文件中定义的CustomGeometry 1.0
模块的一部分:
# 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
由于贝塞尔曲线是以线条状绘制的,因此我们指定视图应采用多重采样以获得抗锯齿效果。这不是必需的,但在支持多采样的硬件上会使项目看起来更漂亮。默认情况下不启用多重采样,因为它通常会导致更高的内存使用率。
使用项目
import QtQuick import CustomGeometry
我们的 .qml 文件导入了QtQuick 2.0
模块以获得标准类型,还导入了我们自己的CustomGeometry 1.0
模块,其中包含我们新创建的 BezierCurve 对象。
Item { width: 300 height: 200 BezierCurve { id: line anchors.fill: parent anchors.margins: 20
然后,我们创建根项目和一个 BezierCurve 实例,并将其作为填充根项目的锚点。
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) }
为了使示例更加有趣,我们添加了一个动画来改变曲线中的两个控制点。端点保持不变。
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") } }
最后,我们叠加一段简短的文字,概述示例所展示的内容。
© 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.