曲面图库

提供三种不同的Surface3D 图形使用方法的图库。

Surface Graph Gallery演示了使用Surface3D 图形的三种不同自定义功能。这些功能在应用程序中有各自的选项卡。

以下部分只集中介绍这些功能,跳过对基本功能的解释--如需更详细的 QML 示例文档,请参阅 "简单散点图"

运行示例

运行示例 Qt Creator,打开Welcome 模式,并从Examples 选择示例。更多信息,请参阅Qt Creator: Tutorial:构建并运行

高度图

Height Map 选项卡中,根据高度数据生成曲面图。使用的数据是新西兰鲁阿佩胡山(Mount Ruapehu)和恩格鲁霍山(Mount Ngauruhoe)的高度图。

为图表添加数据

数据是通过HeightMapSurfaceDataProxy 设置的,它从高度地图图像中读取高度信息。代理本身包含在Surface3DSeries 中。在HeightMapSurfaceDataProxy 中,heightMapFile 属性指定了包含高度数据的图像文件。代理中的值属性定义了表面区域宽度、深度和高度的最小值和最大值。zx 的值以经纬度为单位,近似于现实世界的位置,而y 则以米为单位。

注意: 图表的纵横比并没有按照实际比例设置,而是夸大了高度。

Surface3DSeries {
    id: heightSeries
    shading: Surface3DSeries.Shading.Smooth
    drawMode: Surface3DSeries.DrawSurface

    HeightMapSurfaceDataProxy {
        heightMapFile: "://qml/surfacegallery/heightmap.png"
        // We don't want the default data values set by heightmap proxy, but use
        // actual coordinate and height values instead
        autoScaleY: true
        minYValue: 740
        maxYValue: 2787
        minZValue: -374 // ~ -39.374411"N
        maxZValue: -116 // ~ -39.115971"N
        minXValue: 472  // ~ 175.471767"E
        maxXValue: 781  // ~ 175.780758"E
    }

    onDrawModeChanged: heightMapView.checkState()
}
显示数据

main.qml 中,设置Surface3D 元素以显示数据。

首先,定义曲面使用的自定义渐变。使用 "渐变 "设置从位置 0.0 到 1.0 的颜色,多设置两档,使图表更加生动:

Gradient {
    id: surfaceGradient
    GradientStop { position: 0.0; color: "darkgreen"}
    GradientStop { position: 0.15; color: "darkslategray" }
    GradientStop { position: 0.7; color: "peru" }
    GradientStop { position: 1.0; color: "white" }
}

Surface3D 中使用的theme 中将此元素设置为baseGradients 属性:

theme: GraphsTheme {
    colorScheme: GraphsTheme.ColorScheme.Dark
    labelFont.family: "STCaiyun"
    labelFont.pointSize: 35
    colorStyle: GraphsTheme.ColorStyle.ObjectGradient
    baseGradients: [surfaceGradient] // Use the custom gradient
}

使用按钮控制其他Surface3D 功能。

第一个按钮用于打开或关闭曲面网格。绘制模式无法完全清除,因此除非曲面本身可见,否则曲面网格无法隐藏:

onClicked: {
    if (heightSeries.drawMode & Surface3DSeries.DrawWireframe)
        heightSeries.drawMode &= ~Surface3DSeries.DrawWireframe;
    else
        heightSeries.drawMode |= Surface3DSeries.DrawWireframe;
}

第二个按钮设置曲面网格的颜色:

onClicked: {
    if (Qt.colorEqual(heightSeries.wireframeColor, "#000000")) {
        heightSeries.wireframeColor = "red";
        text = "Black surface\ngrid color";
    } else {
        heightSeries.wireframeColor = "black";
        text = "Red surface\ngrid color";
    }
}

第三项是在曲面绘制模式下打开或关闭曲面。绘制模式无法完全清除,因此除非曲面网格可见,否则曲面本身无法隐藏:

onClicked: {
    if (heightSeries.drawMode & Surface3DSeries.DrawSurface)
        heightSeries.drawMode &= ~Surface3DSeries.DrawSurface;
    else
        heightSeries.drawMode |= Surface3DSeries.DrawSurface;
}

第四项设置着色模式。如果在 OpenGL ES 系统上运行示例,则无法使用平面着色:

onClicked: {
    if (heightSeries.shading === Surface3DSeries.Shading.Flat) {
        heightSeries.shading = Surface3DSeries.Shading.Smooth;
        text = "Show\nFlat"
    } else {
        heightSeries.shading = Surface3DSeries.Shading.Flat;
        text = "Show\nSmooth"
    }
}

其余按钮用于控制图形背景特征。

光谱图

Spectrogram 选项卡中,可以显示极坐标和直角坐标频谱图,并使用正投影以 2D 方式显示。

频谱图是一种表面图,其范围梯度用于强调不同的值。通常情况下,频谱图是以二维表面显示的,而二维表面是通过图形自上而下的正投影视图来模拟的。要实现二维效果,在正交模式下通过鼠标或触摸禁用图形旋转。

创建频谱图

要创建二维频谱图,请使用Surface3DSeries 中给出的数据定义一个Surface3D 项目,并使用ItemModelSurfaceDataProxy

Surface3D {
    id: surfaceGraph
    anchors.fill: parent

    // Don't show specular spotlight as we don't want it to distort the colors
    lightStrength: 0.0
    ambientLightStrength: 1.0

    Surface3DSeries {
        id: surfaceSeries
        shading: Surface3DSeries.Shading.Smooth
        drawMode: Surface3DSeries.DrawSurface
        baseGradient: surfaceGradient
        colorStyle: GraphsTheme.ColorStyle.RangeGradient
        itemLabelFormat: "(@xLabel, @zLabel): @yLabel"

        ItemModelSurfaceDataProxy {
            itemModel: surfaceData.model
            rowRole: "radius"
            columnRole: "angle"
            yPosRole: "value"
        }
    }

启用 2D 效果的关键属性是orthoProjectioncameraPreset 。通过为图表启用正投影来移除透视,并通过直接从上方查看图表来移除 Y 维:

// Remove the perspective and view the graph from top down to achieve 2D effect
orthoProjection: true
cameraPreset: Graphs3D.CameraPreset.DirectlyAbove

由于这种视角会导致水平轴网格大部分被表面遮挡,因此请将水平网格翻转绘制到图形顶部:

flipHorizontalGrid: true
极坐标频谱图

根据数据的不同,有时使用极坐标图比使用直角坐标图更自然。polar 属性支持这种做法。

添加一个在极坐标和直角坐标模式之间切换的按钮:

Button {
    id: polarToggle
    anchors.margins: 5
    anchors.left: parent.left
    anchors.top: parent.top
    width: spectrogramView.buttonWidth // Calculated elsewhere based on screen orientation
    text: "Switch to\n" + (surfaceGraph.polar ? "cartesian" : "polar")
    onClicked: surfaceGraph.polar = !surfaceGraph.polar;
}

在极坐标模式下,X 轴转换为角度极坐标轴,Z 轴转换为径向极坐标轴。曲面点将根据新轴重新计算。

径向轴标签默认绘制在图形外。若要在图形内紧靠 0 度角轴绘制,只需为它们定义一个小偏移量:

radialLabelOffset: 0.01

要实现 2D 效果,可使用自定义输入处理程序覆盖默认输入处理程序,从而在正交模式下禁用图形旋转。自定义输入处理程序可根据投影模式自动切换rotationEnabled 属性:

rotationEnabled: !surfaceGraph.orthoProjection

示波器

Oscilloscope 标签中,将 C++ 和 QML 结合到一个应用程序中,并显示动态变化的数据。

C++ 中的数据源

基于项模型的代理适用于简单或静态图形,但在显示实时变化的数据时,使用基本代理可获得最佳性能。QML 不支持这些代理,因为它们存储的数据项不继承QObject ,因此不能直接从 QML 代码中操作。为克服这一限制,用 C++ 实现一个简单的DataSource 类,以填充系列的数据代理。

创建DataSource 类,提供两个可从 QML 调用的方法:

class DataSource : public QObject
{
    Q_OBJECT
    ...
Q_INVOKABLE void generateData(int cacheCount,
                              int rowCount,
                              int columnCount,
                              float xMin,
                              float xMax,
                              float yMin,
                              float yMax,
                              float zMin,
                              float zMax);

Q_INVOKABLE void update(QSurface3DSeries *series);

第一个方法是generateData() ,创建一个要显示的模拟示波器数据缓存。数据以QSurfaceDataProxy 接受的格式缓存:

// Populate caches
for (int i = 0; i < cacheCount; i++) {
    QSurfaceDataArray &cache = m_data[i];
    float cacheXAdjustment = cacheStep * i;
    float cacheIndexAdjustment = cacheIndexStep * i;
    for (int j = 0; j < rowCount; j++) {
        QSurfaceDataRow &row = cache[j];
        float rowMod = (float(j)) / float(rowCount);
        float yRangeMod = yRange * rowMod;
        float zRangeMod = zRange * rowMod;
        float z = zRangeMod + zMin;
        qreal waveAngleMul = M_PI * M_PI * rowMod;
        float waveMul = yRangeMod * 0.2f;
        for (int k = 0; k < columnCount; k++) {
            float colMod = (float(k)) / float(columnCount);
            float xRangeMod = xRange * colMod;
            float x = xRangeMod + xMin + cacheXAdjustment;
            float colWave = float(qSin((2.0 * M_PI * colMod) - (1.0 / 2.0 * M_PI)) + 1.0);
            float y = (colWave * ((float(qSin(waveAngleMul * colMod) + 1.0)))) * waveMul
                      + QRandomGenerator::global()->bounded(0.15f) * yRangeMod;

            int index = k + cacheIndexAdjustment;
            if (index >= columnCount) {
                // Wrap over
                index -= columnCount;
                x -= xRange;
            }
            row[index] = QSurfaceDataItem(x, y, z);
        }
    }
}

第二个方法是update() ,它将一组缓存数据复制到另一个数组中,通过调用QSurfaceDataProxy::resetArray() 将其设置为系列数据。为尽量减少开销,如果数组的维数没有变化,可重复使用相同的数组:

// Each iteration uses data from a different cached array
m_index++;
if (m_index > m_data.count() - 1)
    m_index = 0;

QSurfaceDataArray array = m_data.at(m_index);
int newRowCount = array.size();
int newColumnCount = array.at(0).size();

// If the first time or the dimensions of the cache array have changed,
// reconstruct the reset array
if (m_resetArray.isEmpty() || series->dataProxy()->rowCount() != newRowCount
    || series->dataProxy()->columnCount() != newColumnCount) {
    m_resetArray.clear();
    m_resetArray.reserve(newRowCount);
    for (int i = 0; i < newRowCount; i++)
        m_resetArray.append(QSurfaceDataRow(newColumnCount));
}

// Copy items from our cache to the reset array
for (int i = 0; i < newRowCount; i++) {
    const QSurfaceDataRow &sourceRow = array.at(i);
    QSurfaceDataRow &row = m_resetArray[i];
    for (int j = 0; j < newColumnCount; j++)
        row[j].setPosition(sourceRow.at(j).position());
}

// Notify the proxy that data has changed
series->dataProxy()->resetArray(m_resetArray);

即使我们对先前设置为系列的数组指针进行操作,在更改其中的数据后,仍需要调用QSurfaceDataProxy::resetArray() 来提示图形渲染数据。

为了能从 QML 访问DataSource 方法,通过将 DataSource 设为QML_ELEMENT 来暴露数据源:

class DataSource : public QObject
{
    Q_OBJECT
    QML_ELEMENT

此外,在 CMakeLists.txt 中将其声明为 QML 模块:

qt6_add_qml_module(surfacegallery
    URI SurfaceGallery
    VERSION 1.0
    NO_RESOURCE_TARGET_PATH
    SOURCES
        datasource.cpp datasource.h
    ...
)

要在所有环境和构建中使用QSurface3DSeries 指针作为DataSource 类方法的参数,确保元类型已注册:

qRegisterMetaType<QSurface3DSeries *>();
QML 应用程序

要使用DataSource ,请导入 QML 模块并创建DataSource 实例:

import SurfaceGalleryExample
...
DataSource {
    id: dataSource
}

定义一个Surface3D 图,并给它一个Surface3DSeries

Surface3D {
    id: surfaceGraph
    anchors.fill: parent

    Surface3DSeries {
        id: surfaceSeries
        drawMode: Surface3DSeries.DrawSurfaceAndWireframe
        itemLabelFormat: "@xLabel, @zLabel: @yLabel"

不要为附加到图表的Surface3DSeries 指定代理。这将使系列使用默认的QSurfaceDataProxy

使用itemLabelVisible 隐藏项目标签。对于动态、快速变化的数据,浮动的选择标签会分散注意力且难以阅读。

itemLabelVisible: false

您可以在Text 元素中显示所选项信息,而不是在选择指针上方显示默认浮动标签:

onItemLabelChanged: {
    if (surfaceSeries.selectedPoint == surfaceSeries.invalidSelectionPosition)
        selectionText.text = "No selection";
    else
        selectionText.text = surfaceSeries.itemLabel;
}

当图表完成时,通过调用辅助函数generateData() 来初始化DataSource 缓存,该函数调用DataSource 中的同名方法:

Component.onCompleted: oscilloscopeView.generateData();
...
function generateData() {
    dataSource.generateData(oscilloscopeView.sampleCache, oscilloscopeView.sampleRows,
                            oscilloscopeView.sampleColumns,
                            surfaceGraph.axisX.min, surfaceGraph.axisX.max,
                            surfaceGraph.axisY.min, surfaceGraph.axisY.max,
                            surfaceGraph.axisZ.min, surfaceGraph.axisZ.max);
}

要触发数据更新,可定义一个Timer ,该函数会按要求的时间间隔调用DataSource 中的update() 方法:

Timer {
    id: refreshTimer
    interval: 1000 / frequencySlider.value
    running: true
    repeat: true
    onTriggered: dataSource.update(surfaceSeries);
}
启用直接渲染

由于该应用程序可能会处理大量快速变化的数据,因此它使用直接渲染模式来提高性能。要在此模式下启用抗锯齿功能,请更改应用程序窗口的表面格式。QQuickView 默认使用的格式不支持抗锯齿。请使用main.cpp 中提供的实用程序更改曲面格式:

#include <QtGraphs/qutils.h>
...
// Enable antialiasing in direct rendering mode
viewer.setFormat(QQuick3D::idealSurfaceFormat(8));

示例内容

示例项目 @ code.qt.io

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