表面图形库
展示三种使用 Surface3D 图表的不同方法的图形库。
表面图形库 通过三个与 Surface3D 图表相关的自定义特性进行展示。这些特性在应用程序中各自拥有标签页。
以下节将专门介绍这些特性,并跳过基本功能的解释 - 了解更详细的 QML 示例文档,请参阅 简单散点图。
运行示例
要从 Qt Creator 运行示例,请打开 欢迎使用 模式,并在 示例 中选择示例。有关更多信息,请参阅 构建和运行示例。
高度图
在 高度图 标签页中,从高度数据生成表面图形。使用的数据是新西兰鲁阿佩胡火山和瑙鲁霍伊火山的高度图。
向图形中添加数据
数据通过 HeightMapSurfaceDataProxy 设置,该代理从高度图图像中读取高度信息。代理本身包含在一个 Surface3DSeries 中。在 HeightMapSurfaceDataProxy 中,heightMapFile
属性指定包含高度数据的图像文件。代理中的值属性定义了表面面积宽度、深度和高度的最小和最大值。z
和 x
值位于纬度和经度,约为真实世界的位置,而 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
中使用的 theme
的 baseGradients
属性中
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" } }
启用二维效果的关键属性是orthoProjection和cameraPreset。通过为图形启用正射投影来消除透视,并通过直接从上面查看图形来消除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));
示例内容
© 2024 Qt公司有限公司。此处包含的文档贡献的版权属于各自的所有者。本文件中的文档按照自由软件基金会发布并受GNU自由文档许可证版本1.3的条款提供。Qt和相应的标志是芬兰的Qt公司及其它国家/地区的商标。所有其他商标均为各自所有者的财产。