条形图、散点图及表面图图库。
图形图库 demonstrates all three graph types and some of their special features. The graphs have their own tabs in the application.
要运行范例从 Qt Creator ,打开 欢迎 模式,然后选择范例从 范例 。更多信息,见 Qt Creator: Tutorial: Build and run .
在 条形图 tab, create a 3D bar graph using Q3DBarWidgetItem and combine the use of widgets to adjust various bar graph qualities. The example shows how to:
For information about interacting with the graph, see this page .
bargraph.cpp
,实例化
QQuickWidget
and
Q3DBarsWidgetItem
, and set the
QQuickWidget
instance as the widget for
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);
This is done to make them orient slightly toward the camera, which improves axis label readability at extreme camera angles.
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);
At the end of the constructor, call a method that sets up the data:
resetTemperatureData();
This method adds data to the relevant series using proxies of the two series:
// 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);
Continue by adding some widgets in
bargraph.cpp
.
auto *rotationSliderX = new QSlider(Qt::Horizontal, m_container); 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
:
QObject::connect(rotationSliderX, &QSlider::valueChanged, m_modifier, &GraphModifier::rotateX);
GraphModifier
for the signal connection. Specify the actual camera position along the orbit around the center point, instead of specifying a preset camera angle:
void GraphModifier::rotateX(int angle) { m_xRotation = angle; m_graph - > setCameraPosition(m_xRotation , m_yRotation); }
You can now use the slider to rotate the graph.
Add more widgets to the vertical layout to control:
Some widget controls are intentionally disabled when in the Custom Proxy Data data mode.
Selection by axis label is default functionality for bar graphs. For example, you can select rows by clicking an axis label in the following way:
Row
The same method works with
Slice
and
Item
flags, as long as either
Row
or
Column
is set as well.
As an example of adjusting the camera target, implement an animation of zooming to selection via a button press. Animation initializations are done in the constructor:
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()
contains the zooming functionality.
QPropertyAnimation
m_animationCameraTarget
目标
cameraTargetPosition
property, which takes a value normalized to the range (-1, 1).
Figure out where the selected bar is relative to axes, and use that as the end value for
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));
Then, rotate the camera so that it always points approximately to the center of the graph at the end of the animation:
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;
When you toggle Custom Proxy Data data mode on, the graph in the example uses a custom dataset and the corresponding proxy.
Define a simple flexible data set,
VariantDataSet
, where each data item is a variant list. Each item can have multiple values, identified by their index in the list. In this case, the data set is designed to store monthly rainfall data. The value in index zero represents the year, the value in index one represents the month, and the value in index two represents the amount of rainfall in that month.
The custom proxy is similar to itemmodel-based proxies, QItemModelBarDataProxy , provided by QtGraphs , and it requires mapping to interpret the data.
Define the data items as QVariantList objects. Add functionality for clearing the data set and querying a reference to the data contained in the set. Also, add signals to be emitted when data is added or the set is cleared:
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();
Derive the
VariantBarDataProxy
class from
QBarDataProxy
and implement a simple API of getters and setters for the data set and the mapping:
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();
The proxy listens for changes in the data set and the mapping, resolving the data set if any changes are detected. While this implementation may not be particularly efficient, as any change triggers the re-resolving of the entire data set, it is not a concern for this example.
在
resolveDataSet()
method, sort the variant data values into rows and columns based on the mapping. This is very similar to how the
QItemModelBarDataProxy
handles mapping, except you use list indexes instead of item model roles here. Once the values are sorted, generate a
QBarDataArray
out of them, and call the
resetArray()
method in the parent class:
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); }
Store the mapping information between
VariantDataSet
data item indexes and rows, columns, and values of
QBarDataArray
in
VariantBarDataMapping
. It contains the lists of rows and columns to be included in the resolved data:
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();
The primary way to use a
VariantBarDataMapping
object is to give the mappings in the constructor, though you can also use the
remap()
method to set them later, either individually or all together. Emit a signal if mapping changes. The outcome is a simplified version of the mapping functionality of
QItemModelBarDataProxy
, adapted to work with variant lists instead of item models.
RainfallData
类:
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; }
在 Scatter Graph tab, create a 3D scatter graph using Q3DScatterWidgetItem . The example shows how to:
For basic application creation, see 条形图 .
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);
None of these settings are mandatory, but they serve to override graph defaults. To observe the appearance with the preset defaults, the block above can be commented out.
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
constructor, add data to the graph:
addData();
addData()
method. First, configure the axes:
m_graph->axisX()->setTitle("X"); m_graph - > axisY() - > setTitle( "Y" ); m_graph - > axisZ() - > setTitle( "Z" );
You could do this also in the constructor of
ScatterDataModifier
. Doing it here keeps the constructor simpler and the axes' configuration near the data.
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);
Now, the graph has the data and is ready for use. For information about adding widgets to control the graph, see 使用 Widget 去控制图形 .
To replace the default input handling mechanism, set the new input handlers of Q3DScatterWidgetItem , which implements the custom behavior:
connect(m_graph, &Q3DGraphsWidgetItem::selectedElementChanged, this, &ScatterDataModifier::handleElementSelected); connect(m_graph, &Q3DGraphsWidgetItem::dragged, this, &ScatterDataModifier::handleAxisDragging); m_graph->setDragButton(Qt::LeftButton);
Implement a new
drag
event handler. It provides a mouse movement distance for the axis dragging calculation (see
Implementing Axis Dragging
了解细节):
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
, check the type of the selection, and set the internal state based on it:
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
method, which is called from
drag
event:
void ScatterDataModifier::handleAxisDragging(QVector2D delta)
handleAxisDragging
, first get the scene orientation from the active camera:
// 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));
// 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; }
在 表面图 tab, create a 3D surface graph using Q3DSurfaceWidgetItem . The example shows how to:
For basic application creation, see 条形图 .
m_sqrtSinProxy = new QSurfaceDataProxy(); m_sqrtSinSeries = new QSurface3DSeries(m_sqrtSinProxy);
QSurfaceDataArray
instance and add
QSurfaceDataRow
elements to it. Set the created
QSurfaceDataArray
as the data array for the
QSurfaceDataProxy
通过调用
resetArray()
.
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);
Create the height map by instantiating a QHeightMapSurfaceDataProxy 采用 QImage containing the height data. Use QHeightMapSurfaceDataProxy::setValueRanges () to define the value range of the map. In the example, the map is from an imaginary position of 34.0° N - 40.0° N and 18.0° E - 24.0° E. These values are used to position the map on the axes.
// 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);
Add the other surface layers the same way, by creating a proxy and a series for them using height map images. -
The topographic data is obtained from the National Land Survey of Finland. It provides a product called
Elevation Model 2 m
, which is suitable for this example.
The topography data is from Levi fell. The accuracy of the data is well beyond the need, and therefore it is compressed and encoded into a PNG file. The height value of the original ASCII data is encoded into RGB format using a multiplier, as demonstrated in the code sample below. The multiplier is calculated by dividing the largest 24-bit value with the highest point in Finland.
QHeightMapSurfaceDataProxy converts only one-byte values. To utilize the higher accuracy of the data from the National Land Survey of Finland, read the data from the PNG file and decode it into a 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);
Now, a Surface Graph can consume the data via the proxy.
To demonstrate different proxies, the 表面图 has three radio buttons to switch between the series.
采用 Sqrt & Sin , the simple generated series is activated.
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 , the height map series are activated and others are disabled. Auto-adjusting the Y-axis range works well for the height map surface, so ensure it is set.
m_graph->axisY()->setAutoAdjustRange(true);
采用 Textured Topography , the topographic series is activated and others are disabled. Activate a custom input handler for this series, to be able to highlight areas on it:
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);
见 Use Custom Input Handler to Enable Zooming and Panning for information about the custom input handler for this data set.
The three selection modes supported by Q3DSurfaceWidgetItem can be used with radio buttons. To activate the selected mode or to clear it, add the following inline methods:
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); }
To support doing a slice selection to all visible series in the graph simultaneously, add
QtGraphs3D::SelectionFlag::Slice
and
QtGraphs3D::SelectionFlag::MultiSeries
flags for the row and column selection modes.
The example has four slider controls for adjusting the min and max values for the X and Z axes. When selecting the proxy, these sliders are adjusted to match the axis ranges of the current data set:
// 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);
To add support for setting the X range from the widget controls to the graph, add:
void SurfaceGraphModifier::setAxisXRange(float min, float max) { m_graph->axisX()->setRange(min, max); }
Add the support for Z range the same way.
With the Sqrt & Sin data set, custom surface gradients can be taken into use with two push buttons. Define the gradient with QLinearGradient , where the desired colors are set. Also, change the color style to Q3DTheme::ColorStyle::RangeGradient to use the gradient.
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);
To add custom meshes tp the application:
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}
)
<RCC>
<qresource prefix="/">
...
<file>data/refinery.mesh</file>
<file>data/oilrig.mesh</file>
<file>data/pipe.mesh</file>
...
</qresource>
</RCC>
With the Multiseries Height Map data set, custom items are inserted into the graph and can be toggled on or off using checkboxes. Other visual changes can also be controlled with another set of checkboxes, including see-through for the two top layers, and a highlight for the bottom layer.
QImage color = QImage(2, 2, QImage::Format_RGB32); color.fill(Qt::red);
QVector3D positionOne = QVector3D(39.f, 77.f, 19.2f);
auto *item = new QCustom3DItem(":/data/oilrig.mesh", positionOne, QVector3D(0.025f, 0.025f, 0.025f), QQuaternion::fromAxisAndAngle(0.f, 1.f, 0.f, 45.f), color);
m_graph->addCustomItem(item);
Adding a custom label is very similar to adding a custom item. For the label, a custom mesh is not needed, but just a QCustom3DLabel instance:
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);
To remove a specific item from the graph, call
removeCustomItemAt()
with the position of the item:
m_graph->removeCustomItemAt(positionOne);
注意:
Removing a custom item from the graph also deletes the object. To preserve the item, use the
releaseCustomItem()
method instead.
With the Textured Topography data set, create a map texture to be used with the topographic height map.
Set an image to be used as the texture on a surface with QSurface3DSeries::setTextureFile (). Add a check box to control if the texture is set or not, and a handler to react to the checkbox state:
void SurfaceGraphModifier::toggleSurfaceTexture(bool enable) { if (enable) m_topography->setTextureFile(":/data/maptexture.jpg"); else m_topography->setTextureFile(""); }
The image in this example is read from a JPG file. Setting an empty file with the method clears the texture, and the surface uses the gradients or colors from the theme.
With the Textured Topography data set, create a custom input handler to highlight the selection on the graph and allow panning the graph.
The panning implementation is similar to the one shown in Implementing Axis Dragging . The difference is that, in this example, you only follow the X and Z axes and don't allow dragging the surface outside the graph. To limit the dragging, follow the limits of the axes and do nothing if going outside the graph:
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;
For zooming, catch the
wheelEvent
and adjust the X and Y axis ranges according to the delta value on
QWheelEvent
. Adjust the Y axis so that the aspect ratio between the Y axis and the XZ plane stays the same. This prevents getting a graph in which the height is exaggerated:
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); }
Next, add some limits to the zoom level, so that it won't get too near to or far from the surface. For instance, if the value for the X axis gets below the allowed limit, that is, zooming gets too far, the value is set to the minimum allowed value. If the range is going to below the range minimum, both ends of the axis are adjusted so that the range stays at the limit:
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; }
To implement a highlight to be displayed on the surface, create a copy of the series and add some offset to the y value. In this example, the class
HighlightSeries
implements the creation of the copy in its
handlePositionChange
方法。
First, give
HighlightSeries
the pointer to the original series, and then start listening to the
QSurface3DSeries::selectedPointChanged
signal:
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); }
When the signal triggers, check that the position is valid. Then, calculate he ranges for the copied area, and check that they stay within the bounds. Finally, fill the data array of the highlight series with the range from the data array of the topography series:
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
, all the decoration methods a series can have are available. In this example, add a gradient to emphasize the elevation. Because the suitable gradient style depends on the range of the Y axis and we change the range when zooming, the gradient color positions need to be adjusted as the range changes. Do this by defining proportional values for the gradient color positions:
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;
The gradient modification is done in the
handleGradientChange
method, so connect it to react to changes on the Y axis:
QObject::connect(m_graph->axisY(), &QValue3DAxis::maxChanged, m_highlight, &HighlightSeries::handleGradientChange);
When a change in the Y axis max value happens, calculate the new gradient color positions:
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); }