本页内容

场景图 - 自定义几何体

演示如何在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 类,并添加了五个属性。其中四个属性分别对应贝塞尔曲线上的四个控制点,还有一个参数用于控制曲线被划分为若干段的数量。 对于每个属性,我们都提供了相应的获取器和设置器函数。由于这些属性可以在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 文件。

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

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