场景图 - 自定义几何体

展示了如何在实际的 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 引擎能够捕获到这些变化并相应地使用。

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

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")
    }
}

最后,我们在示例上方叠加一段短文,概述示例所展示的内容。

示例项目 @ code.qt.io

© 2024 Qt公司有限公司。本体内的文档贡献归其所有者所有版权。本体内的文档根据自由软件基金会发布的GNU自由文档许可证第1.3版本许可条款提供。Qt及其相关标志是芬兰和/或其他世界各地的Qt公司商标。商标。所有其他商标归其所有者所有。