场景图 - 自定义几何体
展示了如何在实际的 Qt Quick 场景图中实现自定义几何体。
自定义几何体示例展示了如何创建一个QQuickItem,该项使用场景图 API 来为场景图构建自定义几何体。它是通过创建一个 BezierCurve
项来实现的,这个项是 CustomGeometry 模块的一部分,并在 QML 文件中使用它。
BezierCurve 声明
#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; };
该项声明从 QQuickItem 类中继承,并增加了五个属性。分别对应 bezer 曲线中的四个控制点,以及一个控制曲线分割成有几个段数的参数。对于我们每个属性,都有对应的获取器和设置器函数。由于这些属性可以在 QML 中绑定,因此最好为它们提供相应的通知信号,以便 QML 引擎能够捕获到这些变化并相应地使用。
QML 场景和渲染场景图之间的同步点是虚函数 QQuickItem::updatePaintNode(),所有具有自定义场景图逻辑的项都必须实现此函数。
注意:在许多硬件配置中,场景图将在单独的线程上进行渲染。因此,与场景图的交互必须以受控的方式进行,首先是通过 QQuickItem::updatePaintNode() 函数。
BezierCurve 实现
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); }
BezierCurve 构造函数设置了控制点和段数的默认值。贝塞尔曲线在相对于该项边界矩形的归一化坐标中指定。
构造函数还设置了标志 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 属性的设置器函数会检查值是否未发生变化,如果是,则提前退出。然后它会更新内部值并发送一个已更改的信号。然后它继续调用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,这是上一次函数调用返回的实例。第一次调用该函数时,它将是null,我们将创建自己的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文件传递给QQuickView。
QML_ELEMENT
要使用BezierCurve项目,我们必须在QML引擎中将其注册,使用QML_ELEMENT宏。这将给它"BezierCurve"的名称,并使其成为自定义几何体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") } }
最后,我们在示例上方叠加一段短文,概述示例所展示的内容。
© 2024 Qt公司有限公司。本体内的文档贡献归其所有者所有版权。本体内的文档根据自由软件基金会发布的GNU自由文档许可证第1.3版本许可条款提供。Qt及其相关标志是芬兰和/或其他世界各地的Qt公司商标。商标。所有其他商标归其所有者所有。