图形库
条形图、散点图和曲面图图库。
图形库演示了所有三种图形类型及其某些特殊功能。这些图形在应用程序中都有各自的选项卡。
运行示例
运行示例 Qt Creator,打开Welcome 模式,然后从Examples 中选择示例。更多信息,请参见Qt Creator: 教程:构建并运行。
条形图
在Bar Graph 选项卡中,使用 Q3DBarWidgetItem 创建三维条形图,并结合使用部件调整各种条形图质量。该示例展示了如何
- 使用 Q3DBarWidgetItem 和一些控件部件创建应用程序
- 使用QBar3DSeries 和QBarDataProxy 为图表设置数据
- 使用 widget 控件调整一些图形和序列属性
- 通过点击坐标轴标签选择一行或一列
- 创建自定义代理,与 Q3DBarWidgetItem 配合使用
有关与图表交互的信息,请参阅本页。
创建应用程序
- 在
bargraph.cpp
中,实例化QQuickWidget 和Q3DBarsWidgetItem ,并将QQuickWidget 实例设置为Q3DBarsWidgetItem 的部件:m_quickWidget = new QQuickWidget(); m_barGraph = new Q3DBarsWidgetItem(this); m_barGraph->setWidget(m_quickWidget);
- 创建容器部件以及水平和垂直布局。将图形和垂直布局添加到水平布局中:
m_container = new QWidget(); auto *hLayout = new QHBoxLayout(m_container); QSize screenSize = m_quickWidget->screen()->size(); m_quickWidget->setMinimumSize(QSize(screenSize.width() / 2, screenSize.height() / 1.75)); m_quickWidget->setMaximumSize(screenSize); m_quickWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_quickWidget->setFocusPolicy(Qt::StrongFocus); hLayout->addWidget(m_quickWidget, 1); auto *vLayout = new QVBoxLayout(); hLayout->addLayout(vLayout);
- 创建另一个类来处理数据添加和与图形的其他交互:
m_modifier = new GraphModifier(m_barGraph, this);
设置柱形图
- 在
GraphModifier
类的构造函数中设置图形:GraphModifier::GraphModifier(Q3DBarsWidgetItem *bargraph, QObject *parent) : QObject(parent) , m_graph(bargraph)
- 将坐标轴和序列创建为成员变量,以支持更改它们:
, m_temperatureAxis(new QValue3DAxis) , m_yearAxis(new QCategory3DAxis) , m_monthAxis(new QCategory3DAxis) , m_primarySeries(new QBar3DSeries) , m_secondarySeries(new QBar3DSeries)
- 为图形设置一些视觉效果:
m_graph->setShadowQuality(QtGraphs3D::ShadowQuality::SoftMedium); m_graph->setMultiSeriesUniform(true); // These are set through the active theme m_graph->activeTheme()->setPlotAreaBackgroundVisible(false); m_graph->activeTheme()->setLabelFont(QFont("Times New Roman", m_fontSize)); m_graph->activeTheme()->setLabelBackgroundVisible(true);
- 设置坐标轴并使其成为图形的活动坐标轴:
m_temperatureAxis->setTitle("Average temperature"); m_temperatureAxis->setSegmentCount(m_segments); m_temperatureAxis->setSubSegmentCount(m_subSegments); m_temperatureAxis->setRange(m_minval, m_maxval); m_temperatureAxis->setLabelFormat(u"%.1f "_s + m_celsiusString); m_temperatureAxis->setLabelAutoAngle(30.0f); m_temperatureAxis->setTitleVisible(true); m_yearAxis->setTitle("Year"); m_yearAxis->setLabelAutoAngle(30.0f); m_yearAxis->setTitleVisible(true); m_monthAxis->setTitle("Month"); m_monthAxis->setLabelAutoAngle(30.0f); m_monthAxis->setTitleVisible(true); m_graph->setValueAxis(m_temperatureAxis); m_graph->setRowAxis(m_yearAxis); m_graph->setColumnAxis(m_monthAxis);
- 给坐标轴标签一个小的自动旋转角度:
m_yearAxis->setLabelAutoAngle(30.0f);
这样做的目的是使它们稍微朝向摄像机,从而提高轴标签在极端摄像机角度下的可读性。
- 初始化序列的可视化属性。请注意,第二个系列最初是不可见的:
m_primarySeries->setItemLabelFormat(u"Oulu - @colLabel @rowLabel: @valueLabel"_s); m_primarySeries->setMesh(QAbstract3DSeries::Mesh::BevelBar); m_primarySeries->setMeshSmooth(false); m_secondarySeries->setItemLabelFormat(u"Helsinki - @colLabel @rowLabel: @valueLabel"_s); m_secondarySeries->setMesh(QAbstract3DSeries::Mesh::BevelBar); m_secondarySeries->setMeshSmooth(false); m_secondarySeries->setVisible(false);
- 将序列添加到图表中:
m_graph->addSeries(m_primarySeries); m_graph->addSeries(m_secondarySeries);
- 调用用户界面中摄像机角度更改按钮的相同方法来设置摄像机角度,以循环切换各种摄像机角度:
changePresetCamera();
- 将新的摄像机预设值设置到图表中:
static int preset = int(QtGraphs3D::CameraPreset::Front); m_graph->setCameraPreset((QtGraphs3D::CameraPreset) preset); if (++preset > int(QtGraphs3D::CameraPreset::DirectlyBelow)) preset = int(QtGraphs3D::CameraPreset::FrontLow);
向图表添加数据
在构造函数的末尾,调用一个方法来设置数据:
resetTemperatureData();
该方法使用两个序列的代理为相关序列添加数据:
// Set up data static const float tempOulu[8][12] = { {-7.4f, -2.4f, 0.0f, 3.0f, 8.2f, 11.6f, 14.7f, 15.4f, 11.4f, 4.2f, 2.1f, -2.3f}, // 2015 {-13.4f, -3.9f, -1.8f, 3.1f, 10.6f, 13.7f, 17.8f, 13.6f, 10.7f, 3.5f, -3.1f, -4.2f}, // 2016 ... QBarDataArray dataSet; QBarDataArray dataSet2; dataSet.reserve(m_years.size()); for (qsizetype year = 0; year < m_years.size(); ++year) { // Create a data row QBarDataRow dataRow(m_months.size()); QBarDataRow dataRow2(m_months.size()); for (qsizetype month = 0; month < m_months.size(); ++month) { // Add data to the row dataRow[month].setValue(tempOulu[year][month]); dataRow2[month].setValue(tempHelsinki[year][month]); } // Add the row to the set dataSet.append(dataRow); dataSet2.append(dataRow2); } // Add data to the data proxy (the data proxy assumes ownership of it) m_primarySeries->dataProxy()->resetArray(dataSet, m_years, m_months); m_secondarySeries->dataProxy()->resetArray(dataSet2, m_years, m_months);
使用部件控制图表
继续在bargraph.cpp
中添加一些部件。
- 添加一个滑块:
- 使用滑块旋转图表,而不只是使用鼠标或触摸。将其添加到垂直布局中:
- 将其连接到
GraphModifier
中的方法: - 在
GraphModifier
中为信号连接创建一个插槽。沿围绕中心点的轨道指定摄像机的实际位置,而不是指定预设的摄像机角度:void GraphModifier::rotateX(int angle) { m_xRotation = angle; m_graph->setCameraPosition(m_xRotation, m_yRotation); }
现在可以使用滑块旋转图表。
在垂直布局中添加更多部件进行控制:
- 图形旋转
- 标签样式
- 相机预设
- 背景可见性
- 网格可见性
- 条形图阴影平滑度
- 第二个条形图系列的可见性
- 值轴方向
- 轴标题可见性和旋转
- 要显示的数据范围
- 条形图样式
- 选择模式
- 主题
- 阴影质量
- 字体
- 字体大小
- 轴标签旋转
- 数据模式
在Custom Proxy Data 数据模式下,某些部件控件会被有意禁用。
通过单击轴标签选择行或列
通过轴标签选择是条形图的默认功能。例如,您可以通过单击轴标签选择行,方法如下:
- 将选择模式更改为
Row
- 单击年份标签
- 点击年份的行被选中
只要同时设置了Row
或Column
,同样的方法也适用于Slice
和Item
标志。
缩放至选区
作为调整摄像机目标的示例,可通过按下按钮实现缩放至选中位置的动画。动画初始化在构造函数中完成:
m_defaultAngleX = m_graph->cameraXRotation(); m_defaultAngleY = m_graph->cameraYRotation(); m_defaultZoom = m_graph->cameraZoomLevel(); m_defaultTarget = m_graph->cameraTargetPosition(); m_animationCameraX.setTargetObject(m_graph); m_animationCameraY.setTargetObject(m_graph); m_animationCameraZoom.setTargetObject(m_graph); m_animationCameraTarget.setTargetObject(m_graph); m_animationCameraX.setPropertyName("cameraXRotation"); m_animationCameraY.setPropertyName("cameraYRotation"); m_animationCameraZoom.setPropertyName("cameraZoomLevel"); m_animationCameraTarget.setPropertyName("cameraTargetPosition"); int duration = 1700; m_animationCameraX.setDuration(duration); m_animationCameraY.setDuration(duration); m_animationCameraZoom.setDuration(duration); m_animationCameraTarget.setDuration(duration); // The zoom always first zooms out above the graph and then zooms in qreal zoomOutFraction = 0.3; m_animationCameraX.setKeyValueAt(zoomOutFraction, QVariant::fromValue(0.0f)); m_animationCameraY.setKeyValueAt(zoomOutFraction, QVariant::fromValue(90.0f)); m_animationCameraZoom.setKeyValueAt(zoomOutFraction, QVariant::fromValue(50.0f)); m_animationCameraTarget.setKeyValueAt(zoomOutFraction, QVariant::fromValue(QVector3D(0.0f, 0.0f, 0.0f)));
函数GraphModifier::zoomToSelectedBar()
包含缩放功能。QPropertyAnimation m_animationCameraTarget
的目标是cameraTargetPosition 属性,该属性的取值范围为 (-1, 1)。
找出所选条形图相对于坐标轴的位置,并将其作为m_animationCameraTarget
的端值:
QVector3D endTarget; float xMin = m_graph->columnAxis()->min(); float xRange = m_graph->columnAxis()->max() - xMin; float zMin = m_graph->rowAxis()->min(); float zRange = m_graph->rowAxis()->max() - zMin; endTarget.setX((selectedBar.y() - xMin) / xRange * 2.0f - 1.0f); endTarget.setZ((selectedBar.x() - zMin) / zRange * 2.0f - 1.0f); ... m_animationCameraTarget.setEndValue(QVariant::fromValue(endTarget));
然后,旋转摄像机,使其在动画结束时始终大致指向图表中心:
qreal endAngleX = 90.0 - qRadiansToDegrees(qAtan(qreal(endTarget.z() / endTarget.x()))); if (endTarget.x() > 0.0f) endAngleX -= 180.0f; float barValue = m_graph->selectedSeries() ->dataProxy() ->itemAt(selectedBar.x(), selectedBar.y()) .value(); float endAngleY = barValue >= 0.0f ? 30.0f : -30.0f; if (m_graph->valueAxis()->reversed()) endAngleY *= -1.0f;
自定义数据代理
打开Custom Proxy Data 数据模式后,示例中的图形将使用自定义数据集和相应的代理。
定义一个简单灵活的数据集VariantDataSet
,其中每个数据项都是一个变量列表。每个项目都可以有多个值,由它们在列表中的索引标识。在本例中,数据集旨在存储月降雨量数据。索引 0 中的值代表年份,索引 1 中的值代表月份,索引 2 中的值代表该月份的降雨量。
自定义代理类似于QtGraphs 提供的基于项目模型的代理(QItemModelBarDataProxy ),它需要映射来解释数据。
实现数据集
将数据项定义为QVariantList 对象。添加清除数据集和查询数据集所含数据引用的功能。此外,还要添加信号,以便在添加数据或清除数据集时发出:
using VariantDataItem = QVariantList; using VariantDataItemList = QList<VariantDataItem *>; ... void clear(); int addItem(VariantDataItem *item); int addItems(VariantDataItemList *itemList); const VariantDataItemList &itemList() const; Q_SIGNALS: void itemsAdded(int index, int count); void dataCleared();
实现数据代理
从QBarDataProxy 派生VariantBarDataProxy
类,并为数据集和映射实现简单的获取器和设置器 API:
class VariantBarDataProxy : public QBarDataProxy ... // Doesn't gain ownership of the dataset, but does connect to it to listen for // data changes. void setDataSet(VariantDataSet *newSet); VariantDataSet *dataSet(); // Map key (row, column, value) to value index in data item (VariantItem). // Doesn't gain ownership of mapping, but does connect to it to listen for // mapping changes. Modifying mapping that is set to proxy will trigger // dataset re-resolving. void setMapping(VariantBarDataMapping *mapping); VariantBarDataMapping *mapping();
代理会侦听数据集和映射的变化,一旦发现任何变化,就会解析数据集。由于任何更改都会触发整个数据集的重新解析,因此这种实现方式可能不是特别高效,但这并不是本示例所关注的问题。
在resolveDataSet()
方法中,根据映射将变量数据值排序为行和列。这与QItemModelBarDataProxy 处理映射的方式非常相似,只不过这里使用的是列表索引而不是项模型角色。值排序完毕后,将其生成QBarDataArray
,并调用父类中的resetArray()
方法:
void VariantBarDataProxy::resolveDataSet() { // If we have no data or mapping, or the categories are not defined, simply // clear the array if (m_dataSet.isNull() || m_mapping.isNull() || !m_mapping->rowCategories().size() || !m_mapping->columnCategories().size()) { resetArray(); return; } const VariantDataItemList &itemList = m_dataSet->itemList(); int rowIndex = m_mapping->rowIndex(); int columnIndex = m_mapping->columnIndex(); int valueIndex = m_mapping->valueIndex(); const QStringList &rowList = m_mapping->rowCategories(); const QStringList &columnList = m_mapping->columnCategories(); // Sort values into rows and columns using ColumnValueMap = QHash<QString, float>; QHash<QString, ColumnValueMap> itemValueMap; for (const VariantDataItem *item : itemList) { itemValueMap[item->at(rowIndex).toString()][item->at(columnIndex).toString()] = item->at(valueIndex).toReal(); } // Create a new data array in format the parent class understands QBarDataArray newProxyArray; for (const QString &rowKey : rowList) { QBarDataRow newProxyRow(columnList.size()); for (qsizetype i = 0; i < columnList.size(); ++i) newProxyRow[i].setValue(itemValueMap[rowKey][columnList.at(i)]); newProxyArray.append(newProxyRow); } // Finally, reset the data array in the parent class resetArray(newProxyArray); }
实现数据映射器
在VariantBarDataMapping
中存储VariantDataSet
数据项索引与QBarDataArray
的行、列和值之间的映射信息。它包含要包含在解析数据中的行和列列表:
Q_PROPERTY(int rowIndex READ rowIndex WRITE setRowIndex NOTIFY rowIndexChanged) Q_PROPERTY(int columnIndex READ columnIndex WRITE setColumnIndex NOTIFY columnIndexChanged) Q_PROPERTY(int valueIndex READ valueIndex WRITE setValueIndex NOTIFY valueIndexChanged) Q_PROPERTY(QStringList rowCategories READ rowCategories WRITE setRowCategories NOTIFY rowCategoriesChanged) Q_PROPERTY(QStringList columnCategories READ columnCategories WRITE setColumnCategories NOTIFY columnCategoriesChanged) ... explicit VariantBarDataMapping(int rowIndex, int columnIndex, int valueIndex, const QStringList &rowCategories, const QStringList &columnCategories); ... void remap(int rowIndex, int columnIndex, int valueIndex, const QStringList &rowCategories, const QStringList &columnCategories); ... void mappingChanged();
使用VariantBarDataMapping
对象的主要方法是在构造函数中给出映射,不过也可以使用remap()
方法在稍后单独或全部设置映射。如果映射发生变化,则发出信号。结果是QItemModelBarDataProxy 的映射功能的简化版本,经调整后可与变体列表而不是项目模型一起使用。
雨量数据
- 使用
RainfallData
类中的自定义代理处理QBar3DSeries 的设置:m_proxy = new VariantBarDataProxy; m_series = new QBar3DSeries(m_proxy);
- 在
addDataSet()
方法中填充变量数据集:voidRainfallData::addDataSet() {// 创建一个新的变量数据集和数据项列表m_dataSet= newVariantDataSet;auto *itemList = newVariantDataItemList;// 从数据文件中读取数据到数据项列表中 QFiledataFile(":/data/raindata.txt");if(dataFile.open(QIODevice::ReadOnly|QIODevice::Text)) { QTextStreamstream(&dataFile);while(!stream.atEnd()) { QStringline=stream.readLine();if(line.startsWith('#'))// 忽略注释 continue;const autostrList=QStringView{line}.split(','、 Qt::SkipEmptyParts);// 每行有三个数据项:年、月和降雨值 if(strList.size()< 3) { qWarning() << "Invalid row read from data:" << line; continue; }// 将年份和月份存储为字符串,将降雨量值存储为 double // 并将该项目添加到项目列表中。 auto *newItem = newVariantDataItem;for(inti= 0; i< 2;++i) newItem->append(strList.at(i).trimmed().toString()); newItem->append(strList.at(2).trimmed().toDouble()); itemList->append(newItem); } }else{ qWarning() << "Unable to open data file:" << dataFile.fileName(); } ...
- 使用自定义代理函数将数据集添加到系列并设置映射:
// Add items to the data set and set it to the proxy m_dataSet->addItems(itemList); m_proxy->setDataSet(m_dataSet); // Create new mapping for the data and set it to the proxy m_mapping = new VariantBarDataMapping(0, 1, 2, m_years, m_numericMonths); m_proxy->setMapping(m_mapping);
- 最后,添加一个函数,用于获取创建的系列以进行显示:
QBar3DSeries *customSeries() { return m_series; }
散点图
在Scatter Graph 选项卡中,使用Q3DScatterWidgetItem 创建三维散点图。示例显示了如何操作:
- 设置Q3DScatterWidgetItem 图形
- 使用QScatterDataProxy 为图表设置数据
- 创建并添加自定义输入处理程序
有关基本应用程序的创建,请参阅条形图。
设置散点图
- 在
ScatterDataModifier
的构造函数中为图形设置一些视觉效果:m_graph->setShadowQuality(QtGraphs3D::ShadowQuality::SoftHigh); m_graph->setCameraPreset(QtGraphs3D::CameraPreset::Front); m_graph->setCameraZoomLevel(80.f); // These are set through active theme m_graph->activeTheme()->setTheme(QGraphsTheme::Theme::MixSeries); m_graph->activeTheme()->setColorScheme(QGraphsTheme::ColorScheme::Dark);
这些设置都不是强制性的,但可以覆盖图形默认值。要观察预设默认值的外观,可以注释掉上面的代码块。
- 创建QScatterDataProxy 和相关的QScatter3DSeries 。为序列设置自定义标签格式和网格平滑,并将其添加到图表中:
auto *proxy = new QScatterDataProxy; auto *series = new QScatter3DSeries(proxy); series->setItemLabelFormat(u"@xTitle: @xLabel @yTitle: @yLabel @zTitle: @zLabel"_s); series->setMeshSmooth(m_smooth); m_graph->addSeries(series);
添加散点图数据
- 在
ScatterDataModifier
构造函数中,向图表中添加数据:addData();
- 实际数据添加在
addData()
方法中完成。首先,配置坐标轴:m_graph->axisX()->setTitle("X"); m_graph->axisY()->setTitle("Y"); m_graph->axisZ()->setTitle("Z");
您也可以在
ScatterDataModifier
的构造函数中进行配置。在这里进行配置可以使构造函数更简单,并且坐标轴的配置靠近数据。 - 创建数据数组并填充:
QScatterDataArray dataArray; dataArray.reserve(m_itemCount); ... const float limit = qSqrt(m_itemCount) / 2.0f; for (int i = -limit; i < limit; ++i) { for (int j = -limit; j < limit; ++j) { const float x = float(i) + 0.5f; const float y = qCos(qDegreesToRadians(float(i * j) / m_curveDivider)); const float z = float(j) + 0.5f; dataArray.append(QScatterDataItem(x, y, z)); } }
- 最后,告诉代理开始使用我们给它的数据:
m_graph->seriesList().at(0)->dataProxy()->resetArray(dataArray);
现在,图表拥有了数据,可以使用了。有关添加部件控制图表的信息,请参阅使用部件控制图表。
替换默认输入处理
要替换默认的输入处理机制,请设置Q3DScatterWidgetItem 的新输入处理程序,以实现自定义行为:
connect(m_graph, &Q3DGraphsWidgetItem::selectedElementChanged, this, &ScatterDataModifier::handleElementSelected); connect(m_graph, &Q3DGraphsWidgetItem::dragged, this, &ScatterDataModifier::handleAxisDragging); m_graph->setDragButton(Qt::LeftButton);
扩展鼠标事件处理
实施新的drag
事件处理程序。它为轴拖动计算提供了鼠标移动距离(详见实施轴拖动):
connect(m_graph, &Q3DGraphsWidgetItem::selectedElementChanged, this, &ScatterDataModifier::handleElementSelected); connect(m_graph, &Q3DGraphsWidgetItem::dragged, this, &ScatterDataModifier::handleAxisDragging); m_graph->setDragButton(Qt::LeftButton);
实现轴拖动
- 开始监听图形的选择信号。在构造函数中进行,并将其连接到
handleElementSelected
方法:connect(m_graph, &Q3DGraphsWidgetItem::selectedElementChanged, this, &ScatterDataModifier::handleElementSelected); connect(m_graph, &Q3DGraphsWidgetItem::dragged, this, &ScatterDataModifier::handleAxisDragging); m_graph->setDragButton(Qt::LeftButton);
- 在
handleElementSelected
中,检查选择的类型,并据此设置内部状态:switch (type) { case QtGraphs3D::ElementType::AxisXLabel: m_state = StateDraggingX; break; case QtGraphs3D::ElementType::AxisYLabel: m_state = StateDraggingY; break; case QtGraphs3D::ElementType::AxisZLabel: m_state = StateDraggingZ; break; default: m_state = StateNormal; break; }
- 实际的拖动逻辑在
handleAxisDragging
方法中实现,该方法由drag
事件调用:void ScatterDataModifier::handleAxisDragging(QVector2D delta)
- 在
handleAxisDragging
中,首先从活动相机获取场景方向:// Get scene orientation from active camera float xRotation = m_graph->cameraXRotation(); float yRotation = m_graph->cameraYRotation();
- 根据方向计算鼠标移动方向的修改器:
// Calculate directional drag multipliers based on rotation float xMulX = qCos(qDegreesToRadians(xRotation)); float xMulY = qSin(qDegreesToRadians(xRotation)); float zMulX = qSin(qDegreesToRadians(xRotation)); float zMulY = qCos(qDegreesToRadians(xRotation));
- 计算鼠标移动,并根据摄像机的 Y 轴旋转来修改鼠标移动:
// Get the drag amount QPoint move = delta.toPoint(); // Flip the effect of y movement if we're viewing from below float yMove = (yRotation < 0) ? -move.y() : move.y();
- 将移动距离应用到正确的轴上:
// Adjust axes QValue3DAxis *axis = nullptr; switch (m_state) { case StateDraggingX: axis = m_graph->axisX(); distance = (move.x() * xMulX - yMove * xMulY) / m_dragSpeedModifier; axis->setRange(axis->min() - distance, axis->max() - distance); break; case StateDraggingZ: axis = m_graph->axisZ(); distance = (move.x() * zMulX + yMove * zMulY) / m_dragSpeedModifier; axis->setRange(axis->min() + distance, axis->max() + distance); break; case StateDraggingY: axis = m_graph->axisY(); distance = move.y() / m_dragSpeedModifier; // No need to use adjusted y move here axis->setRange(axis->min() + distance, axis->max() + distance); break; default: break; }
曲面图
在Surface Graph 选项卡中,使用Q3DSurfaceWidgetItem 创建三维曲面图。示例显示了如何创建:
- 设置基本的QSurfaceDataProxy 并为其设置数据。
- 使用QHeightMapSurfaceDataProxy 显示三维高度图。
- 使用地形数据创建三维高度图。
- 使用三种不同的选择模式研究图表。
- 使用坐标轴范围显示图表的选定部分。
- 设置自定义表面梯度。
- 使用QCustom3DItem 和QCustom3DLabel 添加自定义项目和标签。
- 使用自定义输入处理程序启用缩放和平移。
- 突出显示曲面的某个区域。
有关创建基本应用程序的信息,请参阅条形图。
带有生成数据的简单曲面
- 首先,实例化一个新的QSurfaceDataProxy 并将其附加到一个新的QSurface3DSeries 上:
m_sqrtSinProxy = new QSurfaceDataProxy(); m_sqrtSinSeries = new QSurface3DSeries(m_sqrtSinProxy);
- 用简单的平方根和正弦波数据填充代理。创建
QSurfaceDataArray
实例并添加QSurfaceDataRow
元素。通过调用resetArray()
,将创建的QSurfaceDataArray
设置为QSurfaceDataProxy 的数据数组。QSurfaceDataArray dataArray; dataArray.reserve(sampleCountZ); for (int i = 0; i < sampleCountZ; ++i) { QSurfaceDataRow newRow; newRow.reserve(sampleCountX); // Keep values within range bounds, since just adding step can cause minor // drift due to the rounding errors. float z = qMin(sampleMax, (i * stepZ + sampleMin)); for (int j = 0; j < sampleCountX; ++j) { float x = qMin(sampleMax, (j * stepX + sampleMin)); float R = qSqrt(z * z + x * x) + 0.01f; float y = (qSin(R) / R + 0.24f) * 1.61f; newRow.append(QSurfaceDataItem(x, y, z)); } dataArray.append(newRow); } m_sqrtSinProxy->resetArray(dataArray);
多序列高度图数据
通过实例化QHeightMapSurfaceDataProxy 与包含高度数据的QImage ,创建高度图。使用QHeightMapSurfaceDataProxy::setValueRanges() 定义地图的取值范围。在示例中,地图的假想位置为北纬 34.0° - 40.0°,东经 18.0° - 24.0°。
// Create the first surface layer QImage heightMapImageOne(":/data/layer_1.png"); m_heightMapProxyOne = new QHeightMapSurfaceDataProxy(heightMapImageOne); m_heightMapSeriesOne = new QSurface3DSeries(m_heightMapProxyOne); m_heightMapSeriesOne->setItemLabelFormat(u"(@xLabel, @zLabel): @yLabel"_s); m_heightMapProxyOne->setValueRanges(34.f, 40.f, 18.f, 24.f);
用同样的方法添加其他地图层,使用高度地图图像为它们创建代理和序列。-
地形图数据
地形图数据来自芬兰国家土地测量局。它提供了一个名为Elevation Model 2 m
的产品,适用于本示例。
地形数据来自 Levi fell。数据的精度远远超出了需要,因此将其压缩并编码为 PNG 文件。原始 ASCII 数据的高度值使用乘法器编码成 RGB 格式,如下代码示例所示。乘数的计算方法是将最大的 24 位数值除以芬兰的最高点。
QHeightMapSurfaceDataProxy 只转换单字节值。要利用芬兰全国土地调查局提供的精度更高的数据,可从 PNG 文件中读取数据并解码成 。QSurface3DSeries
- 定义编码乘数:
// Value used to encode height data as RGB value on PNG file const float packingFactor = 11983.f;
- 执行实际解码:
QImage heightMapImage(file); uchar *bits = heightMapImage.bits(); int imageHeight = heightMapImage.height(); int imageWidth = heightMapImage.width(); int widthBits = imageWidth * 4; float stepX = width / float(imageWidth); float stepZ = height / float(imageHeight); QSurfaceDataArray dataArray; dataArray.reserve(imageHeight); for (int i = 0; i < imageHeight; ++i) { int p = i * widthBits; float z = height - float(i) * stepZ; QSurfaceDataRow newRow; newRow.reserve(imageWidth); for (int j = 0; j < imageWidth; ++j) { uchar aa = bits[p + 0]; uchar rr = bits[p + 1]; uchar gg = bits[p + 2]; uint color = uint((gg << 16) + (rr << 8) + aa); float y = float(color) / packingFactor; newRow.append(QSurfaceDataItem(float(j) * stepX, y, z)); p += 4; } dataArray.append(newRow); } dataProxy()->resetArray(dataArray);
现在,Surface Graph 可以通过代理读取数据。
选择数据集
为了演示不同的代用指标,Surface Graph 有三个单选按钮用于切换系列。
通过Sqrt & Sin ,可激活简单生成的系列。
- 设置装饰功能,如启用表面网格和选择平面阴影模式。
- 定义轴标签格式和数值范围。设置标签自动旋转功能,以提高低摄像机角度下的标签可读性。
- 确保将正确的系列添加到图形中,而不是其他系列。
m_sqrtSinSeries->setDrawMode(QSurface3DSeries::DrawSurfaceAndWireframe); m_sqrtSinSeries->setShading(QSurface3DSeries::Shading::Flat); m_graph->axisX()->setLabelFormat("%.2f"); m_graph->axisZ()->setLabelFormat("%.2f"); m_graph->axisX()->setRange(sampleMin, sampleMax); m_graph->axisY()->setRange(0.f, 2.f); m_graph->axisZ()->setRange(sampleMin, sampleMax); m_graph->axisX()->setLabelAutoAngle(30.f); m_graph->axisY()->setLabelAutoAngle(90.f); m_graph->axisZ()->setLabelAutoAngle(30.f); m_graph->removeSeries(m_heightMapSeriesOne); m_graph->removeSeries(m_heightMapSeriesTwo); m_graph->removeSeries(m_heightMapSeriesThree); m_graph->removeSeries(m_topography); m_graph->removeSeries(m_highlight); m_graph->addSeries(m_sqrtSinSeries);
通过Multiseries Height Map ,高度图系列被激活,其他系列被禁用。自动调整 Y 轴范围对高度地图表面效果很好,因此请确保已设置。
m_graph->axisY()->setAutoAdjustRange(true);
通过Textured Topography ,地形图系列被激活,其他系列被禁用。为该系列激活自定义输入处理程序,以便突出显示其中的区域:
m_graph->setDragButton(Qt::LeftButton); QObject::connect(m_graph, &Q3DGraphsWidgetItem::dragged, this, &SurfaceGraphModifier::handleAxisDragging); QObject::connect(m_graph, &Q3DGraphsWidgetItem::wheel, this, &SurfaceGraphModifier::onWheel); m_graph->setZoomEnabled(false);
有关此数据集自定义输入处理程序的信息,请参阅使用自定义输入处理程序启用缩放和平移。
选择模式
Q3DSurfaceWidgetItem 支持的三种选择模式可与单选按钮一起使用。要激活选定模式或清除选定模式,请添加以下内联方法:
void toggleModeNone() { m_graph->setSelectionMode(QtGraphs3D::SelectionFlag::None); } void toggleModeItem() { m_graph->setSelectionMode(QtGraphs3D::SelectionFlag::Item); } void toggleModeSliceRow() { m_graph->setSelectionMode(QtGraphs3D::SelectionFlag::ItemAndRow | QtGraphs3D::SelectionFlag::Slice | QtGraphs3D::SelectionFlag::MultiSeries); } void toggleModeSliceColumn() { m_graph->setSelectionMode(QtGraphs3D::SelectionFlag::ItemAndColumn | QtGraphs3D::SelectionFlag::Slice | QtGraphs3D::SelectionFlag::MultiSeries); }
为支持同时对图表中所有可见序列进行切片选择,请为行和列选择模式添加QtGraphs3D::SelectionFlag::Slice
和QtGraphs3D::SelectionFlag::MultiSeries
标志。
研究图表的坐标轴范围
示例中有四个滑块控件,用于调整 X 轴和 Z 轴的最小值和最大值。选择代理时,这些滑块将被调整为与当前数据集的轴范围相匹配:
// Reset range sliders for Sqrt & Sin m_rangeMinX = sampleMin; m_rangeMinZ = sampleMin; m_stepX = (sampleMax - sampleMin) / float(sampleCountX - 1); m_stepZ = (sampleMax - sampleMin) / float(sampleCountZ - 1); m_axisMinSliderX->setMinimum(0); m_axisMinSliderX->setMaximum(sampleCountX - 2); m_axisMinSliderX->setValue(0); m_axisMaxSliderX->setMinimum(1); m_axisMaxSliderX->setMaximum(sampleCountX - 1); m_axisMaxSliderX->setValue(sampleCountX - 1); m_axisMinSliderZ->setMinimum(0); m_axisMinSliderZ->setMaximum(sampleCountZ - 2); m_axisMinSliderZ->setValue(0); m_axisMaxSliderZ->setMinimum(1); m_axisMaxSliderZ->setMaximum(sampleCountZ - 1); m_axisMaxSliderZ->setValue(sampleCountZ - 1);
要在图形中添加对从 widget 控件设置 X 范围的支持,请添加:
void SurfaceGraphModifier::setAxisXRange(float min, float max) { m_graph->axisX()->setRange(min, max); }
以同样方式添加对 Z 轴范围的支持。
自定义表面渐变
使用Sqrt & Sin 数据集,可以通过两个按钮使用自定义表面梯度。使用QLinearGradient 定义梯度,在这里可以设置所需的颜色。同时,将颜色样式更改为 Q3DTheme::ColorStyle::RangeGradient 以使用渐变。
QLinearGradient gr; gr.setColorAt(0.f, Qt::black); gr.setColorAt(0.33f, Qt::blue); gr.setColorAt(0.67f, Qt::red); gr.setColorAt(1.f, Qt::yellow); m_sqrtSinSeries->setBaseGradient(gr); m_sqrtSinSeries->setColorStyle(QGraphsTheme::ColorStyle::RangeGradient);
在应用程序中添加自定义网格
为应用程序添加自定义网格:
- 对于 cmake 构建。将网格文件添加到
CMakeLists.txt
:set(graphgallery_resource_files ... "data/oilrig.mesh" "data/pipe.mesh" "data/refinery.mesh" ... ) qt6_add_resources(widgetgraphgallery "widgetgraphgallery" PREFIX "/" FILES ${graphgallery_resource_files} )
- 对于 qmake 联编。在 qrc 资源文件中添加网格文件:
<RCC> <qresource prefix="/"> ... <file>data/refinery.mesh</file> <file>data/oilrig.mesh</file> <file>data/pipe.mesh</file> ... </qresource> </RCC>
在图表中添加自定义项
通过Multiseries Height Map 数据集,自定义项被插入到图形中,并可通过复选框打开或关闭。还可以使用另一组复选框控制其他视觉变化,包括两个顶层的透视和底层的高亮。
- 创建一个小的QImage 。用单一颜色填充,作为自定义对象的颜色:
- 在变量中指定项目的位置。该位置可用于从图形中移除正确的项目:
QVector3D positionOne = QVector3D(39.f, 77.f, 19.2f);
- 创建一个包含所有参数的新QCustom3DItem :
auto *item = new QCustom3DItem(":/data/oilrig.mesh", positionOne, QVector3D(0.0125f, 0.0125f, 0.0125f), QQuaternion::fromAxisAndAngle(0.f, 1.f, 0.f, 45.f), color);
- 将项目添加到图形中:
m_graph->addCustomItem(item);
在图表中添加自定义标签
添加自定义标签与添加自定义项目非常相似。对于标签,不需要自定义网格,只需要一个QCustom3DLabel 实例:
auto *label = new QCustom3DLabel(); label->setText("Oil Rig One"); label->setPosition(positionOneLabel); label->setScaling(QVector3D(1.f, 1.f, 1.f)); m_graph->addCustomItem(label);
从图表中移除自定义项
要从图形中移除特定项,请调用removeCustomItemAt()
并输入该项的位置:
m_graph->removeCustomItemAt(positionOne);
注: 从图形中删除自定义项的同时也会删除对象。要保留该项目,请使用releaseCustomItem()
方法。
为曲面系列创建纹理
使用Textured Topography 数据集,创建与地形高度图一起使用的地图纹理。
使用QSurface3DSeries::setTextureFile() 将图像设置为表面纹理。添加一个复选框来控制是否设置纹理,并添加一个处理程序来对复选框状态做出反应:
void SurfaceGraphModifier::toggleSurfaceTexture(bool enable) { if (enable) m_topography->setTextureFile(":/data/maptexture.jpg"); else m_topography->setTextureFile(""); }
本例中的图片是从 JPG 文件中读取的。使用该方法设置一个空文件后,纹理将被清除,曲面将使用主题中的渐变或颜色。
使用自定义输入处理程序启用缩放和平移
使用Textured Topography 数据集创建自定义输入处理程序,以突出显示图表上的选区并允许平移图表。
平移的实现方式与 "实现轴拖动 "中显示的方式类似。不同之处在于,在本示例中,您只能沿 X 轴和 Z 轴拖动,而不允许在图形外拖动表面。要限制拖动,请遵循轴的限制,如果超出图形范围,则不做任何操作:
case StateDraggingX: distance = (move.x() * xMulX - move.y() * xMulY) * m_speedModifier; m_axisXMinValue -= distance; m_axisXMaxValue -= distance; if (m_axisXMinValue < m_areaMinValue) { float dist = m_axisXMaxValue - m_axisXMinValue; m_axisXMinValue = m_areaMinValue; m_axisXMaxValue = m_axisXMinValue + dist; } if (m_axisXMaxValue > m_areaMaxValue) { float dist = m_axisXMaxValue - m_axisXMinValue; m_axisXMaxValue = m_areaMaxValue; m_axisXMinValue = m_axisXMaxValue - dist; } m_graph->axisX()->setRange(m_axisXMinValue, m_axisXMaxValue); break;
缩放时,捕捉wheelEvent
,并根据QWheelEvent 上的 delta 值调整 X 和 Y 轴范围。调整 Y 轴,使 Y 轴和 XZ 平面之间的纵横比保持不变。这样可以防止出现高度被夸大的图形:
void SurfaceGraphModifier::onWheel(QWheelEvent *event) { float delta = float(event->angleDelta().y()); m_axisXMinValue += delta; m_axisXMaxValue -= delta; m_axisZMinValue += delta; m_axisZMaxValue -= delta; checkConstraints(); float y = (m_axisXMaxValue - m_axisXMinValue) * m_aspectRatio; m_graph->axisX()->setRange(m_axisXMinValue, m_axisXMaxValue); m_graph->axisY()->setRange(100.f, y); m_graph->axisZ()->setRange(m_axisZMinValue, m_axisZMaxValue); }
接下来,为缩放级别添加一些限制,使其不会离表面太近或太远。例如,如果 X 轴的值低于允许的限制,即缩放太远,该值就会被设置为允许的最小值。如果范围将低于范围最小值,则会调整轴的两端,使范围保持在极限值:
if (m_axisXMinValue < m_areaMinValue) m_axisXMinValue = m_areaMinValue; if (m_axisXMaxValue > m_areaMaxValue) m_axisXMaxValue = m_areaMaxValue; // Don't allow too much zoom in if ((m_axisXMaxValue - m_axisXMinValue) < m_axisXMinRange) { float adjust = (m_axisXMinRange - (m_axisXMaxValue - m_axisXMinValue)) / 2.f; m_axisXMinValue -= adjust; m_axisXMaxValue += adjust; }
突出显示曲面的某个区域
要在曲面上显示高亮显示,可创建序列的副本并在 Y 值上添加一些偏移量。在本例中,类HighlightSeries
在其handlePositionChange
方法中实现了副本的创建。
首先,将指向原始序列的指针交给HighlightSeries
,然后开始监听QSurface3DSeries::selectedPointChanged 信号:
void HighlightSeries::setTopographicSeries(TopographicSeries *series) { m_topographicSeries = series; m_srcWidth = m_topographicSeries->dataArray().at(0).size(); m_srcHeight = m_topographicSeries->dataArray().size(); QObject::connect(m_topographicSeries, &QSurface3DSeries::selectedPointChanged, this, &HighlightSeries::handlePositionChange); }
当信号触发时,检查位置是否有效。然后,计算复制区域的范围,并检查它们是否在边界内。最后,用地形序列数据数组中的范围填充高亮序列数据数组:
void HighlightSeries::handlePositionChange(const QPoint &position) { m_position = position; if (position == invalidSelectionPosition()) { setVisible(false); return; } int halfWidth = m_width / 2; int halfHeight = m_height / 2; int startX = position.x() - halfWidth; if (startX < 0) startX = 0; int endX = position.x() + halfWidth; if (endX > (m_srcWidth - 1)) endX = m_srcWidth - 1; int startZ = position.y() - halfHeight; if (startZ < 0) startZ = 0; int endZ = position.y() + halfHeight; if (endZ > (m_srcHeight - 1)) endZ = m_srcHeight - 1; const QSurfaceDataArray &srcArray = m_topographicSeries->dataArray(); QSurfaceDataArray dataArray; dataArray.reserve(endZ - startZ); for (int i = startZ; i < endZ; ++i) { QSurfaceDataRow newRow; newRow.reserve(endX - startX); QSurfaceDataRow srcRow = srcArray.at(i); for (int j = startX; j < endX; ++j) { QVector3D pos = srcRow.at(j).position(); pos.setY(pos.y() + m_heightAdjustment); newRow.append(QSurfaceDataItem(pos)); } dataArray.append(newRow); } dataProxy()->resetArray(dataArray); setVisible(true); }
高亮系列的渐变效果
由于HighlightSeries
是QSurface3DSeries ,因此可以使用序列所能使用的所有装饰方法。在本示例中,我们添加了一个渐变来突出高程。由于合适的渐变样式取决于 Y 轴的范围,而我们在缩放时会改变范围,因此需要根据范围的变化调整渐变颜色的位置。为此,需要为渐变色位置定义比例值:
const float darkRedPos = 1.f; const float redPos = 0.8f; const float yellowPos = 0.6f; const float greenPos = 0.4f; const float darkGreenPos = 0.2f;
渐变色的修改是在handleGradientChange
方法中完成的,因此连接该方法可对 Y 轴的变化做出反应:
QObject::connect(m_graph->axisY(), &QValue3DAxis::maxChanged, m_highlight, &HighlightSeries::handleGradientChange);
当 Y 轴最大值发生变化时,计算新的渐变色位置:
void HighlightSeries::handleGradientChange(float value) { float ratio = m_minHeight / value; QLinearGradient gr; gr.setColorAt(0.f, Qt::black); gr.setColorAt(darkGreenPos * ratio, Qt::darkGreen); gr.setColorAt(greenPos * ratio, Qt::green); gr.setColorAt(yellowPos * ratio, Qt::yellow); gr.setColorAt(redPos * ratio, Qt::red); gr.setColorAt(darkRedPos * ratio, Qt::darkRed); setBaseGradient(gr); setColorStyle(QGraphsTheme::ColorStyle::RangeGradient); handleZoomChange(ratio); }
示例内容
© 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.