表面图形库

展示三种使用 Surface3D 图表的不同方法的图形库。

表面图形库 通过三个与 Surface3D 图表相关的自定义特性进行展示。这些特性在应用程序中各自拥有标签页。

以下节将专门介绍这些特性,并跳过基本功能的解释 - 了解更详细的 QML 示例文档,请参阅 简单散点图

运行示例

要从 Qt Creator 运行示例,请打开 欢迎使用 模式,并在 示例 中选择示例。有关更多信息,请参阅 构建和运行示例

高度图

高度图 标签页中,从高度数据生成表面图形。使用的数据是新西兰鲁阿佩胡火山和瑙鲁霍伊火山的高度图。

向图形中添加数据

数据通过 HeightMapSurfaceDataProxy 设置,该代理从高度图图像中读取高度信息。代理本身包含在一个 Surface3DSeries 中。在 HeightMapSurfaceDataProxy 中,heightMapFile 属性指定包含高度数据的图像文件。代理中的值属性定义了表面面积宽度、深度和高度的最小和最大值。zx 值位于纬度和经度,约为真实世界的位置,而 y 的单位为米。

注意:图形的纵横比并未设置为真实世界的比例,而是强调了高度。

Surface3DSeries {
    id: heightSeries
    flatShadingEnabled: false
    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 中使用的 themebaseGradients 属性中

theme: Theme3D {
    type: Theme3D.Theme.StoneMoss
    font.family: "STCaiyun"
    font.pointSize: 35
    colorStyle: Theme3D.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.flatShadingEnabled) {
        heightSeries.flatShadingEnabled = false;
        text = "Show\nFlat"
    } else {
        heightSeries.flatShadingEnabled = true;
        text = "Show\nSmooth"
    }
}

剩余的按钮控制图表背景功能。

频谱图

频谱图选项卡中,显示极坐标和笛卡尔频谱图,并使用正射投影在二维中显示。

频谱图是一个具有范围渐变的表面图,用于强调不同的值。通常,频谱图以二维表面显示,这是通过从上往下的正射视图模拟的。要强制二维效果,请在正射模式下通过鼠标或触摸禁用图形旋转。

创建频谱图

要创建一个二维频谱图,定义一个带有给定在Surface3DSeries中的数据Surface3D项,并具有一个ItemModelSurfaceDataProxy

Surface3D {
    id: surfaceGraph
    anchors.fill: parent

    Surface3DSeries {
        id: surfaceSeries
        flatShadingEnabled: false
        drawMode: Surface3DSeries.DrawSurface
        baseGradient: surfaceGradient
        colorStyle: Theme3D.ColorStyle.RangeGradient
        itemLabelFormat: "(@xLabel, @zLabel): @yLabel"

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

启用二维效果的关键属性是orthoProjectioncameraPreset。通过为图形启用正射投影来消除透视,并通过直接从上面查看图形来消除Y维度的透视。

// Remove the perspective and view the graph from top down to achieve 2D effect
orthoProjection: true
cameraPreset: AbstractGraph3D.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

要强制二维效果,通过用自定义的、根据投影模式自动切换rotationEnabled属性的默认输入处理器来在正射模式下禁用图形旋转。

inputHandler: TouchInputHandler3D {
    rotationEnabled: !surfaceGraph.orthoProjection
}

示波器

示波器选项卡中,在一个应用程序中将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 SurfaceGallery
...
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(qDefaultSurfaceFormat(true));

示例内容

示例项目 @ code.qt.io

© 2024 Qt公司有限公司。此处包含的文档贡献的版权属于各自的所有者。本文件中的文档按照自由软件基金会发布并受GNU自由文档许可证版本1.3的条款提供。Qt和相应的标志是芬兰的Qt公司及其它国家/地区的商标。所有其他商标均为各自所有者的财产。