本页内容

图表图库

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

“图表图库”展示了这三种图表类型及其部分特殊功能。这些图表在应用程序中各有独立的标签页。

带有“条形图”、“散点图”和“曲面图”选项卡的带纹理地形3D曲面图

运行示例

您可以通过以下方式运行示例:

条形图

在“Bar Graph ”选项卡中,使用 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 中的一个方法:

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. 包含所点击年份的那一行即被选中

只要同时设置了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 指向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 ,其中每个数据项都是一个变体列表。 每个项目可以有多个值,并通过其在列表中的索引进行标识。在此示例中,该数据集存储的是月降雨量数据,其中索引为零的值代表年份,索引为一的值代表月份,索引为二的值代表该月的降雨量。

该自定义代理与Qt Data Visualization 提供的基于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

将VariantBarDataProxy 从QBarDataProxy派生为子类,并为数据集和映射提供包含getter和setter方法的简单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 的行、列及值之间的映射信息存储在VariantBarDataMapping 中。它包含将要包含在解析后的数据中的行和列列表:

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 映射功能的简化版本,经过调整可与变体列表(variant lists)配合使用,而非项目模型(item models)。

RainfallData

在RainfallData 类中,使用自定义代理处理 QBar3DSeries 的初始化:

m_proxy = new VariantBarDataProxy;
m_series = new QBar3DSeries(m_proxy);

在addDataSet() 方法中填充变量数据集:

voidRainfallData::addDataSet()
{
    // 创建新的变体数据集和数据项列表
    m_dataSet= newVariantDataSet;
    auto *itemList = newVariantDataItemList;

    // 从数据文件中读取数据并将其加载到数据项列表中
    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('#'))// 忽略注释
                continue;
            const autostrList=QStringView{line}.split(',',Qt::SkipEmptyParts);
            // 每行包含三个数据项:年、月和降雨量
            if(strList.size()< 3) {
                qWarning() << "Invalid row read from data:" << line;
               continue;
            }
            // 将年和月以字符串形式、降雨量值以双精度浮点数形式 
            // 存储到一个变体数据项中,并将该项添加到项目列表中。
            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 创建一个 3D 散点图。本示例演示了如何:

  • 设置 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());
扩展鼠标事件处理

首先,让自定义输入处理程序继承自 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 创建一个 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);
多系列高程图数据

通过实例化一个 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::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>
向图中添加自定义项目

通过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);
}
《渐变到高光》系列

由于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);
}

示例内容

示例项目 @ code.qt.io

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