图形库

条形图、散点图和曲面图图库。

图形库演示了所有三种图形类型及其某些特殊功能。这些图形在应用程序中都有各自的选项卡。

运行示例

运行示例 Qt Creator,打开Welcome 模式,然后从Examples 中选择示例。更多信息,请参见Qt Creator: 教程:构建并运行

条形图

Bar Graph 选项卡中,使用Q3DBars 创建三维条形图,并结合使用部件调整各种条形图质量。该示例展示了如何

  • 使用Q3DBars 和一些部件创建应用程序
  • 使用QBar3DSeriesQBarDataProxy 为图表设置数据
  • 使用部件控件调整一些图形和序列属性
  • 通过单击坐标轴标签选择一行或一列
  • 创建自定义代理,与Q3DBars

有关与图表交互的信息,请参阅本页

创建应用程序

首先,在bargraph.cpp 中实例化Q3DBars

m_barsGraph = new Q3DBars();

然后,创建窗口小部件以及水平和垂直布局。

使用QWidget::createWindowContainer() 将图形嵌入窗口容器。这是必需的,因为所有数据可视化图形类 (Q3DBars,Q3DScatter,Q3DSurface) 都继承了QWindow 。这是使用继承了QWindow 的类作为 widget 的唯一方法。

将图形和垂直布局添加到水平布局中:

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);

最后,调用用户界面中摄像机角度更改按钮的相同方法来设置摄像机角度,以循环切换各种摄像机角度:

changePresetCamera();

摄像机是通过图形的场景对象控制的:

static int preset = Q3DCamera::CameraPresetFront;

m_graph->scene()->activeCamera()->setCameraPreset((Q3DCamera::CameraPreset)preset);

if (++preset > Q3DCamera::CameraPresetDirectlyBelow)
    preset = Q3DCamera::CameraPresetFrontLow;

有关使用场景和摄像机的更多信息,请参阅Q3DSceneQ3DCamera

向图表添加数据

在构造函数的末尾,调用一个方法来设置数据:

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 中的方法:

QObject::connect(rotationSliderX, &QSlider::valueChanged, modifier, &GraphModifier::rotateX);

GraphModifier 中为信号连接创建一个插槽。通过场景对象控制摄像机。这一次,请沿着围绕中心点的轨道指定摄像机的实际位置,而不是指定预设的摄像机角度:

void GraphModifier::rotateX(int rotation)
{
    m_xRotation = rotation;
    m_graph->scene()->activeCamera()->setCameraPosition(m_xRotation, m_yRotation);
}

现在您可以使用滑块旋转图形。

在垂直布局中添加更多部件进行控制:

  • 图形旋转
  • 标签样式
  • 相机预设
  • 背景可见性
  • 网格可见性
  • 条形图阴影平滑度
  • 第二个条形图系列的可见性
  • 值轴方向
  • 轴标题可见性和旋转
  • 要显示的数据范围
  • 条形图样式
  • 选择模式
  • 主题
  • 阴影质量
  • 字体
  • 字体大小
  • 轴标签旋转
  • 数据模式

Custom Proxy Data 数据模式下,某些部件控件会被有意禁用。

通过单击轴标签选择行或列

通过轴标签选择是条形图的默认功能。例如,您可以按以下方式通过单击轴标签选择行:

  1. 将选择模式更改为SelectionRow
  2. 单击年份标签
  3. 点击年份的行被选中

只要同时设置了SelectionRowSelectionColumn ,同样的方法也适用于SelectionSliceSelectionItem 标志。

缩放至选区

作为调整摄像机目标的示例,可通过按下按钮实现缩放至选中位置的动画。动画初始化在构造函数中完成:

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 的目标是Q3DCamera::target 属性,该属性的取值范围为 (-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 中的值是该月的降雨量。

自定义代理类似于Qt Data Visualization 提供的基于 itemmodel 的代理,它需要映射来解释数据。

变量数据集

将数据项定义为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 fromQBarDataProxy 并为数据集和映射提供简单的获取器和设置器 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 中存储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 选项卡中,使用Q3DScatter 创建三维散点图。示例显示了如何

有关创建基本应用程序的信息,请参阅条形图

设置散点图

首先,在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());
扩展鼠标事件处理

首先,从Q3DInputHandler 而不是QAbstract3DInputHandler 继承自定义输入处理程序,以保留默认输入处理程序的所有功能,并在其上添加自定义功能:

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; }

由于鼠标移动距离在屏幕坐标中是绝对值,因此需要根据轴的范围进行调整。数值越大,拖动速度越慢。请注意,在本示例中,确定拖动速度时并未考虑场景缩放级别,因此当您改变缩放级别时,您会注意到范围调整的变化。

您也可以根据轴线范围和摄像机缩放级别自动调整修改器。

曲面图

Surface Graph 选项卡中,使用Q3DSurface 创建三维曲面图。示例显示了如何创建:

  • 设置基本的QSurfaceDataProxy 并为其设置数据。
  • 使用QHeightMapSurfaceDataProxy 显示三维高度图。
  • 使用地形数据创建三维高度图。
  • 使用三种不同的选择模式研究图表。
  • 使用坐标轴范围显示图表的选定部分。
  • 设置自定义表面梯度。
  • 使用QCustom3DItemQCustom3DLabel 添加自定义项目和标签。
  • 使用自定义输入处理程序启用缩放和平移。
  • 突出显示曲面的某个区域。

有关创建基本应用程序的信息,请参阅条形图

带有生成数据的简单曲面

首先,实例化一个新的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);
多序列高度图数据

通过实例化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);

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);

现在,代理可以使用这些数据了。

选择数据集

为了演示不同的代理,Surface Graph 有三个单选按钮用于在系列之间切换。

通过Sqrt & Sin ,可以激活简单生成的系列。首先,设置装饰功能,如启用表面网格和选择平面阴影模式。然后,定义轴标签格式和数值范围。设置标签自动旋转,以提高标签在摄像机低角度下的可读性。最后,确保将正确的系列添加到图形中,而不是其他系列:

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);

通过Multiseries Height Map ,高度图系列被激活,其他系列被禁用。自动调整 Y 轴范围对高度地图表面效果很好,因此请确保已设置。

m_graph->axisY()->setAutoAdjustRange(true);

使用Textured Topography 时,地形图系列被激活,其他系列被禁用。为该系列激活自定义输入处理程序,以便突出显示其中的区域:

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::SelectionSliceQAbstract3DGraph::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);

在图表中增加对 widget 控制设置 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>
在图表中添加自定义项

使用Multiseries Height Map 数据集,可将自定义项插入图表,并可使用复选框打开或关闭。其他视觉效果也可以通过另一组复选框来控制,包括两个顶层的透视效果和底层的高亮效果。

首先创建一个小的QImage 。在其中填充一种颜色,作为自定义对象的颜色:

QImage color = QImage(2, 2, QImage::Format_RGB32);
color.fill(Qt::red);

然后,在变量中指定项目的位置。该位置可用于从图形中移除正确的项目:

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);
}
高亮系列的渐变效果

由于HighlightSeriesQSurface3DSeries ,因此可以使用序列所能使用的所有装饰方法。在本示例中,我们添加了一个渐变来突出高程。由于合适的渐变样式取决于 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);
}

示例内容

示例项目 @ code.qt.io

© 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.