表面图库

展示三种不同方式的Surface3D 图。

表面图库展示了使用Surface3D图的三种不同的自定义功能。在应用程序中有它们自己的选项卡。

以下部分仅关注这些功能,并跳过了基本功能的解释 - 对于更详细的 QML 示例文档,请参阅 简单散点图

运行示例

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

高度图

高度图 选项卡中,从高度数据生成表面图。使用的数据是新西兰鲁阿佩胡山和瑙鲁霍埃山的高度图。

向图中添加数据

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

注意:图表的比例不是设置为实际生活的比例,而是将高度夸张了。

Surface3DSeries {
    id: heightSeries
    flatShadingEnabled: false
    drawMode: Surface3DSeries.DrawSurface

    HeightMapSurfaceDataProxy {
        heightMapFile: "://qml/qmlsurfacegallery/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 元素来显示数据。

首先,定义用于表面的自定义渐变。使用 ColorGradient 设置从 0.0 到 1.0 的颜色,增加两个额外的停止点以使图表更加生动

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

将此元素放入用于 Surface3DbaseGradients 属性中

theme: Theme3D {
    type: Theme3D.ThemeStoneMoss
    font.family: "STCaiyun"
    font.pointSize: 35
    colorStyle: Theme3D.ColorStyleRangeGradient
    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"
    }
}

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

频谱图

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

频谱图是一种表面图,使用范围梯度来强调不同的值。通常,频谱图以二维表面显示,这通过图的上视正射视图模拟。为了强制2D效果,在正射模式中通过鼠标或触摸禁用图形旋转。

创建频谱图

要创建2D频谱图,定义一个具有Surface3D项的数据,这些数据在Surface3DSeries中给出,并带有ItemModelSurfaceDataProxy

Surface3D {
    id: surfaceGraph
    anchors.fill: parent

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

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

启动2D效果的键属性是orthoProjectionscene.activeCamera.cameraPreset。通过为图表启用正射投影来移除透视,并从上方直接观看图表以移除Y维度

// Remove the perspective and view the graph from top down to achieve 2D effect
orthoProjection: true
scene.activeCamera.cameraPreset: Camera3D.CameraPresetDirectlyAbove

由于这个视角导致水平轴网格大部分被表面遮挡,翻转水平网格以在图表上方绘制

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属性,在正射模式下禁用图形旋转

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
auto *generator = QRandomGenerator::global();
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 rowColWaveAngleMul = M_PI * M_PI * rowMod;
        float rowColWaveMul = 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(rowColWaveAngleMul * colMod) + 1.0))))
                    * rowColWaveMul
                    + generator->bounded(0.15f) * yRangeMod;

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

第二种方法,update(),将一组缓存数据复制到另一个数组中,通过调用QSurfaceDataProxy::resetArray()将其设置为系列的数据代理。为了最小化开销,如果数组维度没有改变,请重用相同的数组

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

const 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 || series->dataProxy()->rowCount() != newRowCount
        || series->dataProxy()->columnCount() != newColumnCount) {
    m_resetArray = new QSurfaceDataArray();
    m_resetArray->reserve(newRowCount);
    for (int i = 0; i < newRowCount; ++i)
        m_resetArray->append(new 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];
    std::copy(sourceRow.cbegin(), sourceRow.cend(), row.begin());
}

// 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(qmlsurfacegallery
    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 <QtDataVisualization/qutils.h>
...
// Enable antialiasing in direct rendering mode
viewer.setFormat(qDefaultSurfaceFormat(true));

示例内容

示例项目 @ code.qt.io

© 2024 The Qt Company Ltd. 增加于此的文档贡献的版权分别为各自所有者所拥有。此处提供的文档是根据由Free Software Foundation发布的GNU自由文档许可证版本1.3的条款提供的。Qt及其相关标志是芬兰和/或其他国家的The Qt Company Ltd.的商标。所有其他商标均为各自所有者的财产。