图形库
图形库展示了柱状图、散点图和曲面图,以及它们的一些特殊功能。在应用程序中,每种图表类型都有自己的标签页。
图形库 展示了所有三种图表类型及其一些特殊功能。图表示例均有其独立的标签页。
运行示例
要从 Qt Creator 运行此示例,请打开 欢迎使用 模式,然后从 示例 中选择示例。有关更多信息,请访问 构建和运行示例。
柱状图
在 柱状图 标签页中,使用 Q3DBars 创建一个 3D 柱状图,并使用小部件调整各种柱状图属性。该示例展示了如何
- 使用 Q3DBars 和一些小部件创建应用程序
- 使用 QBar3DSeries 和 QBarDataProxy 将数据设置到图形中
- 使用小部件控制器调整一些图形和系列属性
- 通过点击坐标轴标签选择行或列
- 为使用 Q3DBars 创建自定义代理
有关与图形交互的信息,请参见此页面。
创建应用程序
首先,在 bargraph.cpp
中实例化 Q3DBars
m_barsGraph = new Q3DBars();
然后,创建小部件和水平和垂直布局。
使用 QWidget::createWindowContainer() 将图形嵌套在窗口容器中。这是必要的,因为所有数据可视化图形类(Q3DBars、Q3DScatter、Q3DSurface)都继承自 QWindow。这是将继承自 QWindow 的类作为小部件使用的唯一方法。
将图形和垂直布局添加到水平布局中
m_barsWidget = new QWidget; auto *hLayout = new QHBoxLayout(m_barsWidget); m_container = QWidget::createWindowContainer(m_barsGraph, m_barsWidget); m_barsGraph->resize(minimumGraphSize); m_container->setMinimumSize(minimumGraphSize); m_container->setMaximumSize(maximumGraphSize); m_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_container->setFocusPolicy(Qt::StrongFocus); hLayout->addWidget(m_container, 1); auto *vLayout = new QVBoxLayout(); hLayout->addLayout(vLayout);
接下来,创建另一个类来处理与图形的数据添加和其他交互
auto *modifier = new GraphModifier(m_barsGraph, this);
设置柱状图
在 GraphModifier
类的构造函数中设置图形
GraphModifier::GraphModifier(Q3DBars *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_celsiusString(u"°C"_s)
然后,为图形设置一些视觉质量
m_graph->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftMedium); m_graph->activeTheme()->setBackgroundEnabled(false); m_graph->activeTheme()->setFont(QFont("Times New Roman", m_fontSize)); m_graph->activeTheme()->setLabelBackgroundEnabled(true); m_graph->setMultiSeriesUniform(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->setLabelAutoRotation(30.0f); m_temperatureAxis->setTitleVisible(true); m_yearAxis->setTitle("Year"); m_yearAxis->setLabelAutoRotation(30.0f); m_yearAxis->setTitleVisible(true); m_monthAxis->setTitle("Month"); m_monthAxis->setLabelAutoRotation(30.0f); m_monthAxis->setTitleVisible(true); m_graph->setValueAxis(m_temperatureAxis); m_graph->setRowAxis(m_yearAxis); m_graph->setColumnAxis(m_monthAxis);
通过使用 setLabelAutoRotation
(()) 设置极小的自动翻转动角,使轴标签轻微朝向相机。这可以提高摄像机角度极端时的轴标签可读性。
接下来,初始化系列的可视属性。注意,第二个系列最初不可见
m_primarySeries->setItemLabelFormat(u"Oulu - @colLabel @rowLabel: @valueLabel"_s); m_primarySeries->setMesh(QAbstract3DSeries::MeshBevelBar); m_primarySeries->setMeshSmooth(false); m_secondarySeries->setItemLabelFormat(u"Helsinki - @colLabel @rowLabel: @valueLabel"_s); m_secondarySeries->setMesh(QAbstract3DSeries::MeshBevelBar); m_secondarySeries->setMeshSmooth(false); m_secondarySeries->setVisible(false);
将系列添加到图形中
m_graph->addSeries(m_primarySeries); m_graph->addSeries(m_secondarySeries);
最后,通过调用UI中用于切换各种相机角度的相同方法来设置相机角度。
changePresetCamera();
相机通过图层的场景对象进行控制。
static int preset = Q3DCamera::CameraPresetFront; m_graph->scene()->activeCamera()->setCameraPreset((Q3DCamera::CameraPreset)preset); if (++preset > Q3DCamera::CameraPresetDirectlyBelow) preset = Q3DCamera::CameraPresetFrontLow;
有关使用场景和相机更多信息,请参阅Q3DScene和Q3DCamera。
向图中添加数据
在构造函数的末尾,调用设置数据的函数
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 ... auto *dataSet = new QBarDataArray; auto *dataSet2 = new QBarDataArray; dataSet->reserve(m_years.size()); for (qsizetype year = 0; year < m_years.size(); ++year) { // Create a data row auto *dataRow = new QBarDataRow(m_months.size()); auto *dataRow2 = new QBarDataRow(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
中添加一些小部件。添加一个滑块
auto *rotationSliderX = new QSlider(Qt::Horizontal, m_barsWidget); rotationSliderX->setTickInterval(30); rotationSliderX->setTickPosition(QSlider::TicksBelow); rotationSliderX->setMinimum(-180); rotationSliderX->setValue(0); rotationSliderX->setMaximum(180);
使用滑块旋转图表,而不是仅使用鼠标或触摸。将其添加到垂直布局
vLayout->addWidget(new QLabel(u"Rotate horizontally"_s)); vLayout->addWidget(rotationSliderX, 0, Qt::AlignTop);
然后,将其连接到GraphModifier
中的方法
在GraphModifier
中创建一个槽,用于信号连接。相机通过场景对象进行控制。这次,指定绕中心点的实际相机位置,而不是指定预设的相机角度
void GraphModifier::rotateX(int rotation) { m_xRotation = rotation; m_graph->scene()->activeCamera()->setCameraPosition(m_xRotation, m_yRotation); }
现在您可以使用滑块旋转图表。
添加更多小部件以进行控制
- 图形旋转
- 标签样式
- 相机预设
- 背景可见性
- 网格可见性
- 条形着色平滑度
- 第二个条形序列的可见性
- 值轴方向
- 轴标题可见性和旋转
- 要显示的数据范围
- 条形样式
- 选择模式
- 主题
- 阴影质量
- 字体
- 字体大小
- 轴标签旋转
- 数据模式
在Custom Proxy Data数据模式下,某些小部件控制将被有意禁用。
通过点击轴标签选择行或列
点击轴标签进行选择是条形图的默认功能。例如,您可以按下述方式通过点击轴标签选择行
- 将选择模式更改为
SelectionRow
- 点击一个年份标签
- 点击年份的行被选中
只要设置了SelectionRow
或SelectionColumn
之一,该方法在SelectionSlice
和SelectionItem
标志中也是相同的。
缩放到选择
作为调整相机目标的例子,实现通过按按钮缩放选择动画。动画初始化在构造函数中完成
Q3DCamera *camera = m_graph->scene()->activeCamera(); m_defaultAngleX = camera->xRotation(); m_defaultAngleY = camera->yRotation(); m_defaultZoom = camera->zoomLevel(); m_defaultTarget = camera->target(); m_animationCameraX.setTargetObject(camera); m_animationCameraY.setTargetObject(camera); m_animationCameraZoom.setTargetObject(camera); m_animationCameraTarget.setTargetObject(camera); m_animationCameraX.setPropertyName("xRotation"); m_animationCameraY.setPropertyName("yRotation"); m_animationCameraZoom.setPropertyName("zoomLevel"); m_animationCameraTarget.setPropertyName("target"); 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
的目标状态,该状态接受一个标准化到范围(-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
,其中每个数据项都是一个变体列表。每个项目可以有多个值,通过它们在列表中的索引来识别。在这种情况下,数据集存储每月的降雨数据,索引零中的值是年份,索引一中的值是月份,索引二中的值是当月的降雨量。
自定义代理类似于Qt数据可视化提供的基于itemmodel的代理,它需要映射来解释数据。
VariantDataSet
将数据项定义为 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();
VariantBarDataProxy
从 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(nullptr); 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 auto *newProxyArray = new QBarDataArray; for (const QString &rowKey : rowList) { auto *newProxyRow = new QBarDataRow(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
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
在 RainfallData
类中,使用自定义代理设置 QBar3DSeries。
m_proxy = new VariantBarDataProxy; m_series = new QBar3DSeries(m_proxy);
在 addDataSet()
方法中填充变体数据集。
void RainfallData::addDataSet() { // Create a new variant data set and data item list m_dataSet = new VariantDataSet; auto *itemList = new VariantDataItemList; // Read data from a data file into the data item list QFile dataFile(":/data/raindata.txt"); if (dataFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream stream(&dataFile); while (!stream.atEnd()) { QString line = stream.readLine(); if (line.startsWith('#')) // Ignore comments continue; const auto strList = QStringView{line}.split(',', Qt::SkipEmptyParts); // Each line has three data items: Year, month, and rainfall value if (strList.size() < 3) { qWarning() << "Invalid row read from data:" << line; continue; } // Store year and month as strings, and rainfall value as double // into a variant data item and add the item to the item list. auto *newItem = new VariantDataItem; for (int i = 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; }
散点图
在 散点图 选项卡中,使用 Q3DScatter 创建一个三维散点图。该示例展示了如何
- 设置 Q3DScatter 图形
- 使用 QScatterDataProxy 将数据设置到图中
- 通过扩展 Q3DInputHandler 创建自定义输入处理程序
有关基本应用程序创建的信息,请参阅 柱状图。
设置散点图
首先,在ScatterDataModifier
的构造函数中,为图表设置一些视觉特性。
m_graph->activeTheme()->setType(Q3DTheme::ThemeStoneMoss); m_graph->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftHigh); m_graph->scene()->activeCamera()->setCameraPreset(Q3DCamera::CameraPresetFront); m_graph->scene()->activeCamera()->setZoomLevel(80.f);
这些选项都不是必需的,但可以用来覆盖图表的默认值。您可以取消注释上面的代码块来尝试使用预设的默认值。
接下来,创建一个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
的构造函数中这样做。在这里执行可以保持构造函数更简单,并将轴配置靠近数据。
接下来,创建一个数据数组并将其填充
auto *dataArray = new QScatterDataArray; 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);
现在,图表有了数据,可以使用了。有关添加控件以控制图表的信息,请参阅使用控件控制图表。
替换默认输入处理
在构造函数中初始化m_inputHandler
,使其指向散点图实例的指针
m_inputHandler(new AxesInputHandler(scatter))
通过将Q3DScatter的当前活动输入处理程序设置为AxesInputHandler
(它实现了自定义行为)来替换默认的输入处理机制
// Give ownership of the handler to the graph and make it the active handler m_graph->setActiveInputHandler(m_inputHandler);
输入处理程序需要访问图表的轴,因此将它们传递给它
// Give our axes to the input handler m_inputHandler->setAxes(m_graph->axisX(), m_graph->axisZ(), m_graph->axisY());
扩展鼠标事件处理
首先,从QAbstract3DInputHandler继承自定义输入处理程序,而不是从Q3DInputHandler继承,以保留默认输入处理的所有功能,并在其上添加自定义功能
class AxesInputHandler : public Q3DInputHandler
通过重新实现一些鼠标事件来开始扩展默认功能。首先,扩展mousePressEvent
。为此添加一个表示左键点击的m_mousePressed
标志,并保留 默认功能的其他部分
void AxesInputHandler::mousePressEvent(QMouseEvent *event, const QPoint &mousePos) { Q3DInputHandler::mousePressEvent(event, mousePos); if (Qt::LeftButton == event->button()) m_mousePressed = true; }
接下来,修改mouseReleaseEvent
以清除标志,并重置内部状态
void AxesInputHandler::mouseReleaseEvent(QMouseEvent *event, const QPoint &mousePos) { Q3DInputHandler::mouseReleaseEvent(event, mousePos); m_mousePressed = false; m_state = StateNormal; }
然后,修改mouseMoveEvent
。检查m_mousePressed
标志是true
,并且内部状态不是StateNormal
。如果是这样,为鼠标移动距离计算设置输入位置,并调用轴拖动函数(有关详细信息,请参阅实现轴拖动)
void AxesInputHandler::mouseMoveEvent(QMouseEvent *event, const QPoint &mousePos) { // Check if we're trying to drag axis label if (m_mousePressed && m_state != StateNormal) { setPreviousInputPos(inputPosition()); setInputPosition(mousePos); handleAxisDragging(); } else { Q3DInputHandler::mouseMoveEvent(event, mousePos); } }
实现轴拖动
首先,开始监听来自图表的选择信号。在构造函数中这样做,并将其连接到handleElementSelected
方法
// Connect to the item selection signal from graph connect(graph, &QAbstract3DGraph::selectedElementChanged, this, &AxesInputHandler::handleElementSelected);
在handleElementSelected
中,检查选择类型,并根据该类型设置内部状态
switch (type) { case QAbstract3DGraph::ElementAxisXLabel: m_state = StateDraggingX; break; case QAbstract3DGraph::ElementAxisYLabel: m_state = StateDraggingY; break; case QAbstract3DGraph::ElementAxisZLabel: m_state = StateDraggingZ; break; default: m_state = StateNormal; break; }
实际的拖动逻辑在handleAxisDragging
方法中实现,该方法从满足条件时的mouseMoveEvent
调用
// Check if we're trying to drag axis label if (m_mousePressed && m_state != StateNormal) {
在handleAxisDragging
中,首先从活动相机获取场景方向
// Get scene orientation from active camera float xRotation = scene()->activeCamera()->xRotation(); float yRotation = scene()->activeCamera()->yRotation();
然后,根据方向计算基于鼠标移动方向的调节器
// 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 = inputPosition() - previousInputPos(); // Flip the effect of y movement if we're viewing from below float yMove = (yRotation < 0) ? -move.y() : move.y();
然后,将移动距离应用到正确的轴上
// Adjust axes switch (m_state) { case StateDraggingX: distance = (move.x() * xMulX - yMove * xMulY) / m_speedModifier; m_axisX->setRange(m_axisX->min() - distance, m_axisX->max() - distance); break; case StateDraggingZ: distance = (move.x() * zMulX + yMove * zMulY) / m_speedModifier; m_axisZ->setRange(m_axisZ->min() + distance, m_axisZ->max() + distance); break; case StateDraggingY: distance = move.y() / m_speedModifier; // No need to use adjusted y move here m_axisY->setRange(m_axisY->min() + distance, m_axisY->max() + distance); break; default: break; }
最后,添加一个用于设置拖动速度的函数
inline void setDragSpeedModifier(float modifier) { m_speedModifier = modifier; }
这是必要的,因为鼠标移动距离在屏幕坐标上是绝对的,您需要将其调整到轴范围。值越大,拖动速度越慢。注意,在这个示例中,场景缩放级别在确定拖动速度时没有考虑,所以当您改变缩放级别时,您会注意到范围调整的变化。
您还可以根据轴范围和摄像机缩放级别自动调整修饰符。
表面图形
在 表面图形 标签下,使用 Q3DSurface 创建一个 3D 表面图形。示例展示了如何
- 设置基本的 QSurfaceDataProxy 并为其设置数据。
- 使用 QHeightMapSurfaceDataProxy 来显示 3D 高度图。
- 使用地形数据来创建 3D 高度图。
- 使用三种不同的选择模式来研究图形。
- 使用轴范围来显示图形的选择部分。
- 设置自定义表面梯度。
- 使用 QCustom3DItem 和 QCustom3DLabel 添加自定义项和标签。
- 使用自定义输入处理程序来启用缩放和平移。
- 突出显示表面的一部分。
有关基本应用程序创建的信息,请参阅 柱状图。
具有生成数据的简单表面图
首先,实例化一个新的 QSurfaceDataProxy 并将其附加到一个新的 QSurface3DSeries
m_sqrtSinProxy = new QSurfaceDataProxy(); m_sqrtSinSeries = new QSurface3DSeries(m_sqrtSinProxy);
然后,使用简单的平方根和正弦波数据填充代理。创建一个新的 QSurfaceDataArray
实例,并向其中添加 QSurfaceDataRow
元素。通过调用 resetArray()
将创建的 QSurfaceDataArray
设置为 QSurfaceDataProxy 的数据数组。
auto *dataArray = new QSurfaceDataArray; dataArray->reserve(sampleCountZ); for (int i = 0 ; i < sampleCountZ ; ++i) { auto *newRow = new QSurfaceDataRow; 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);
多序列高度图数据
通过实例化一个包含高度数据的 QImage 的 QHeightMapSurfaceDataProxy 来创建高度图。使用 QHeightMapSurfaceDataProxy::setValueRanges() 定义地图的值范围。在这个示例中,地图是从34.0° N - 40.0° N和18.0° E - 24.0° E的想象位置开始的。这些值用于定位地图在坐标系上的位置。
// 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
的产品,适用于此示例。地形数据来自利韦山顶。数据的精度远远超过了需求,因此它被压缩并编码成一个 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); auto *dataArray = new QSurfaceDataArray; dataArray->reserve(imageHeight); for (int i = 0; i < imageHeight; ++i) { int p = i * widthBits; float z = height - float(i) * stepZ; auto *newRow = new QSurfaceDataRow; 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);
现在,数据可供代理使用。
选择数据集
为了演示不同的代理,表面图形 有三个单选按钮来切换系列。
使用平方根 & 正弦,将简单生成的系列激活。首先,设置装饰功能,例如启用表面网格,并选择平面着色模式。接下来,定义坐标轴标签格式和值范围。设置自动标签旋转以提高低摄像头角度下的标签可读性。最后,确保已将正确的系列添加到图表中,而其他未添加。
m_sqrtSinSeries->setDrawMode(QSurface3DSeries::DrawSurfaceAndWireframe); m_sqrtSinSeries->setFlatShadingEnabled(true); 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()->setLabelAutoRotation(30.f); m_graph->axisY()->setLabelAutoRotation(90.f); m_graph->axisZ()->setLabelAutoRotation(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);
使用多系列高度图,激活高度图系列并禁用其他系列。对于高度图表面,自动调整Y轴范围效果良好,因此请确保已设置。
m_graph->axisY()->setAutoAdjustRange(true);
使用纹理地形,激活地形系列并禁用其他系列。为此系列启用自定义输入处理器,以便能够突出显示其上的区域。
m_graph->setActiveInputHandler(m_customInputHandler);
有关此数据集自定义输入处理器的信息,请参阅使用自定义输入处理器启用缩放和平移。
选择模式
Q3DSurface 支持的三个选择模式可以使用单选按钮使用。要激活选择模式或清除它,请添加以下行方法
void toggleModeNone() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionNone); } void toggleModeItem() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItem); } void toggleModeSliceRow() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItemAndRow | QAbstract3DGraph::SelectionSlice | QAbstract3DGraph::SelectionMultiSeries); } void toggleModeSliceColumn() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItemAndColumn | QAbstract3DGraph::SelectionSlice | QAbstract3DGraph::SelectionMultiSeries); }
将 QAbstract3DGraph::SelectionSlice
和 QAbstract3DGraph::SelectionMultiSeries
标志添加到行和列选择模式,以支持同时对图表中所有可见系列进行切片选择。
研究图表的轴范围
示例有四个滑动条控件用于调整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);
将支持从小部件控件设置X范围到图表中。
void SurfaceGraphModifier::setAxisXRange(float min, float max) { m_graph->axisX()->setRange(min, max); }
以相同方式添加对Z范围的支持。
自定义表面渐变
对于Sqrt & Sin数据集,可以使用两个按钮使用自定义表面渐变。使用QLinearGradient定义渐变,其中设置了所需颜色。此外,将颜色样式更改为Q3DTheme::ColorStyleRangeGradient以使用渐变。
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(Q3DTheme::ColorStyleRangeGradient);
将自定义网格添加到应用程序
将网格文件添加到CMakeLists.txt
中进行cmake构建
set(graphgallery_resource_files ... "data/oilrig.obj" "data/pipe.obj" "data/refinery.obj" ... ) qt6_add_resources(graphgallery "graphgallery" PREFIX "/" FILES ${graphgallery_resource_files} )
此外,在qrc资源文件中添加它们以供qmake使用
<RCC> <qresource prefix="/"> ... <file>data/refinery.obj</file> <file>data/oilrig.obj</file> <file>data/pipe.obj</file> ... </qresource> </RCC>
将自定义项添加到图表中
对于多系列高度图数据集,自定义项被插入到图表中,可以使用复选框切换开启或关闭。其他视觉质量也可以通过另一组复选框控制,包括两个顶层可见性和底部层突出显示。
首先创建一个小的QImage。用单色填充以用作自定义对象的颜色
然后,在一个变量中指定项的位置。然后可以使用此位置从图例中移除正确的项
QVector3D positionOne = QVector3D(39.f, 77.f, 19.2f);
然后,使用所有参数创建一个新的QCustom3DItem
auto *item = new QCustom3DItem(":/data/oilrig.obj", positionOne, QVector3D(0.025f, 0.025f, 0.025f), 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_axisX->setRange(m_axisXMinValue, m_axisXMaxValue); break;
对于缩放,捕获 wheelEvent
并根据QWheelEvent 上的 delta 值调整 X 和 Y 轴范围。调整 Y 轴,使 Y 轴与 XZ 平面的纵横比保持相同。这防止了高度被夸张的图表
void CustomInputHandler::wheelEvent(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_axisX->setRange(m_axisXMinValue, m_axisXMaxValue); m_axisY->setRange(100.f, y); m_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->dataProxy()->array()->at(0)->size(); m_srcHeight = m_topographicSeries->dataProxy()->array()->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.y() - halfWidth; if (startX < 0 ) startX = 0; int endX = position.y() + halfWidth; if (endX > (m_srcWidth - 1)) endX = m_srcWidth - 1; int startZ = position.x() - halfHeight; if (startZ < 0 ) startZ = 0; int endZ = position.x() + halfHeight; if (endZ > (m_srcHeight - 1)) endZ = m_srcHeight - 1; QSurfaceDataProxy *srcProxy = m_topographicSeries->dataProxy(); const QSurfaceDataArray &srcArray = *srcProxy->array(); auto *dataArray = new QSurfaceDataArray; dataArray->reserve(endZ - startZ); for (int i = startZ; i < endZ; ++i) { auto *newRow = new QSurfaceDataRow; 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() + 0.1f); 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(Q3DTheme::ColorStyleRangeGradient); }
示例内容
© 2024 Qt公司有限公司。本文档中包含的文档贡献属于各自所有者。提供的文档受自由软件基金会发布、根据GNU自由文档许可证版本1.3条款许可。Qt及其相关标志是芬兰及其它国家或地区的Qt公司有限公司的商标。商标。所有其他商标均为各自所有者的财产。