图形库

图形库展示了柱状图、散点图和曲面图,以及它们的一些特殊功能。在应用程序中,每种图表类型都有自己的标签页。

图形库 展示了所有三种图表类型及其一些特殊功能。图表示例均有其独立的标签页。

运行示例

要从 Qt Creator 运行此示例,请打开 欢迎使用 模式,然后从 示例 中选择示例。有关更多信息,请访问 构建和运行示例

柱状图

柱状图 标签页中,使用 Q3DBars 创建一个 3D 柱状图,并使用小部件调整各种柱状图属性。该示例展示了如何

  • 使用 Q3DBars 和一些小部件创建应用程序
  • 使用 QBar3DSeriesQBarDataProxy 将数据设置到图形中
  • 使用小部件控制器调整一些图形和系列属性
  • 通过点击坐标轴标签选择行或列
  • 为使用 Q3DBars 创建自定义代理

有关与图形交互的信息,请参见此页面。

创建应用程序

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

m_barsGraph = new Q3DBars();

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

使用 QWidget::createWindowContainer() 将图形嵌套在窗口容器中。这是必要的,因为所有数据可视化图形类(Q3DBarsQ3DScatterQ3DSurface)都继承自 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;

有关使用场景和相机更多信息,请参阅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()包含缩放功能。处于QPropertyAnimationm_animationCameraTarget的目标状态,该状态接受一个标准化到范围(-1,1)的值。

确定所选条形相对于轴的位置,并使用它作为m_animationCameraTarget的最终值。

QVector3D endTarget;
float xMin = m_graph->columnAxis()->min();
float xRange = m_graph->columnAxis()->max() - xMin;
float zMin = m_graph->rowAxis()->min();
float zRange = m_graph->rowAxis()->max() - zMin;
endTarget.setX((selectedBar.y() - xMin) / xRange * 2.0f - 1.0f);
endTarget.setZ((selectedBar.x() - zMin) / zRange * 2.0f - 1.0f);
...
m_animationCameraTarget.setEndValue(QVariant::fromValue(endTarget));

然后,旋转相机,以便在动画结束时它大约指向图表的中心

qreal endAngleX = 90.0 - qRadiansToDegrees(qAtan(qreal(endTarget.z() / endTarget.x())));
if (endTarget.x() > 0.0f)
    endAngleX -= 180.0f;
float barValue = m_graph->selectedSeries()->dataProxy()->itemAt(selectedBar.x(),
                                                                selectedBar.y())->value();
float endAngleY = barValue >= 0.0f ? 30.0f : -30.0f;
if (m_graph->valueAxis()->reversed())
    endAngleY *= -1.0f;
自定义数据代理

通过在Custom Proxy Data数据模式下切换,使用自定义数据集及其相应的代理。

定义一个简单的灵活的数据集 VariantDataSet,其中每个数据项都是一个变体列表。每个项目可以有多个值,通过它们在列表中的索引来识别。在这种情况下,数据集存储每月的降雨数据,索引零中的值是年份,索引一中的值是月份,索引二中的值是当月的降雨量。

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

VariantDataSet

将数据项定义为 QVariantList 对象。添加清除数据集和查询集合中所含数据的引用的功能。另外,添加在数据添加或集合被清除时发出的信号。

using VariantDataItem = QVariantList;
using VariantDataItemList = QList<VariantDataItem *>;
...

void clear();

int addItem(VariantDataItem *item);
int addItems(VariantDataItemList *itemList);

const VariantDataItemList &itemList() const;

Q_SIGNALS:
void itemsAdded(int index, int count);
void dataCleared();
VariantBarDataProxy

QBarDataProxy 继承 VariantBarDataProxy 并为数据集和映射提供简单的获取器和设置器API。

class VariantBarDataProxy : public QBarDataProxy
...

// Doesn't gain ownership of the dataset, but does connect to it to listen for data changes.
void setDataSet(VariantDataSet *newSet);
VariantDataSet *dataSet();

// Map key (row, column, value) to value index in data item (VariantItem).
// Doesn't gain ownership of mapping, but does connect to it to listen for mapping changes.
// Modifying mapping that is set to proxy will trigger dataset re-resolving.
void setMapping(VariantBarDataMapping *mapping);
VariantBarDataMapping *mapping();

代理监听数据集和映射的变化,并在检测到任何更改时解析数据集。这是不是很高效的实现,因为任何更改都将导致整个数据集重新解析,但在这个例子中这不是问题。

resolveDataSet() 方法中,根据映射对变体数据值进行排序,基于行和列。这与 QItemModelBarDataProxy 中的映射处理非常相似,但你在这里使用列表索引而不是项目模型角色。一旦值排列好,从它们中生成 QBarDataArray,并调用父类的 resetArray() 方法。

void VariantBarDataProxy::resolveDataSet()
{
    // If we have no data or mapping, or the categories are not defined, simply clear the array
    if (m_dataSet.isNull() || m_mapping.isNull() || !m_mapping->rowCategories().size()
            || !m_mapping->columnCategories().size()) {
        resetArray(nullptr);
        return;
    }
    const VariantDataItemList &itemList = m_dataSet->itemList();

    int rowIndex = m_mapping->rowIndex();
    int columnIndex = m_mapping->columnIndex();
    int valueIndex = m_mapping->valueIndex();
    const QStringList &rowList = m_mapping->rowCategories();
    const QStringList &columnList = m_mapping->columnCategories();

    // Sort values into rows and columns
    using ColumnValueMap = QHash<QString, float>;
    QHash <QString, ColumnValueMap> itemValueMap;
    for (const VariantDataItem *item : itemList) {
        itemValueMap[item->at(rowIndex).toString()][item->at(columnIndex).toString()]
                = item->at(valueIndex).toReal();
    }

    // Create a new data array in format the parent class understands
    auto *newProxyArray = new QBarDataArray;
    for (const QString &rowKey : rowList) {
        auto *newProxyRow = new QBarDataRow(columnList.size());
        for (qsizetype i = 0; i < columnList.size(); ++i)
            (*newProxyRow)[i].setValue(itemValueMap[rowKey][columnList.at(i)]);
        newProxyArray->append(newProxyRow);
    }

    // Finally, reset the data array in the parent class
    resetArray(newProxyArray);
}
VariantBarDataMapping

VariantBarDataMapping 存储在 VariantDataSet 数据项索引与行、列和 QBarDataArray 的值之间的映射信息。它包含要包含在解析数据中的行和列的列表。

Q_PROPERTY(int rowIndex READ rowIndex WRITE setRowIndex NOTIFY rowIndexChanged)
Q_PROPERTY(int columnIndex READ columnIndex WRITE setColumnIndex NOTIFY columnIndexChanged)
Q_PROPERTY(int valueIndex READ valueIndex WRITE setValueIndex NOTIFY valueIndexChanged)
Q_PROPERTY(QStringList rowCategories READ rowCategories WRITE setRowCategories NOTIFY rowCategoriesChanged)
Q_PROPERTY(QStringList columnCategories READ columnCategories WRITE setColumnCategories NOTIFY columnCategoriesChanged)
...

explicit VariantBarDataMapping(int rowIndex, int columnIndex, int valueIndex,
                               const QStringList &rowCategories,
                               const QStringList &columnCategories);
...

void remap(int rowIndex, int columnIndex, int valueIndex,
           const QStringList &rowCategories,
           const QStringList &columnCategories);
...

void mappingChanged();

主要使用 VariantBarDataMapping 对象的方法是在构造函数中提供映射,尽管您还可以使用 remap() 方法稍后设置它们,可以是单独的,也可以是全部。如果映射更改,则发出信号。结果是 QItemModelBarDataProxy 映射功能的简化版本,适用于变体列表而不是项目模型。

RainfallData

RainfallData 类中,使用自定义代理设置 QBar3DSeries

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

addDataSet() 方法中填充变体数据集。

void RainfallData::addDataSet()
{
    // Create a new variant data set and data item list
    m_dataSet =  new VariantDataSet;
    auto *itemList = new VariantDataItemList;

    // Read data from a data file into the data item list
    QFile dataFile(":/data/raindata.txt");
    if (dataFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QTextStream stream(&dataFile);
        while (!stream.atEnd()) {
            QString line = stream.readLine();
            if (line.startsWith('#')) // Ignore comments
                continue;
            const auto strList = QStringView{line}.split(',', Qt::SkipEmptyParts);
            // Each line has three data items: Year, month, and rainfall value
            if (strList.size() < 3) {
                qWarning() << "Invalid row read from data:" << line;
                continue;
            }
            // Store year and month as strings, and rainfall value as double
            // into a variant data item and add the item to the item list.
            auto *newItem = new VariantDataItem;
            for (int i = 0; i < 2; ++i)
                newItem->append(strList.at(i).trimmed().toString());
            newItem->append(strList.at(2).trimmed().toDouble());
            itemList->append(newItem);
        }
    } else {
        qWarning() << "Unable to open data file:" << dataFile.fileName();
    }
    ...

将数据集添加到自定义代理并设置映射。

// Add items to the data set and set it to the proxy
m_dataSet->addItems(itemList);
m_proxy->setDataSet(m_dataSet);

// Create new mapping for the data and set it to the proxy
m_mapping = new VariantBarDataMapping(0, 1, 2, m_years, m_numericMonths);
m_proxy->setMapping(m_mapping);

最后,添加一个获取创建的系列以进行显示的功能。

QBar3DSeries *customSeries() { return m_series; }

散点图

散点图 选项卡中,使用 Q3DScatter 创建一个三维散点图。该示例展示了如何

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

设置散点图

首先,在ScatterDataModifier的构造函数中,为图表设置一些视觉特性。

m_graph->activeTheme()->setType(Q3DTheme::ThemeStoneMoss);
m_graph->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftHigh);
m_graph->scene()->activeCamera()->setCameraPreset(Q3DCamera::CameraPresetFront);
m_graph->scene()->activeCamera()->setZoomLevel(80.f);

这些选项都不是必需的,但可以用来覆盖图表的默认值。您可以取消注释上面的代码块来尝试使用预设的默认值。

接下来,创建一个QScatterDataProxy以及关联的QScatter3DSeries。为该系列设置自定义标签格式和网格平滑度,并将其添加到图表中

auto *proxy = new QScatterDataProxy;
auto *series = new QScatter3DSeries(proxy);
series->setItemLabelFormat(u"@xTitle: @xLabel @yTitle: @yLabel @zTitle: @zLabel"_s);
series->setMeshSmooth(m_smooth);
m_graph->addSeries(series);
添加散点数据

ScatterDataModifier构造函数中完成的最后一件事情是将数据添加到图表中

addData();

实际上添加数据是通过addData()方法来完成的。首先,配置轴

m_graph->axisX()->setTitle("X");
m_graph->axisY()->setTitle("Y");
m_graph->axisZ()->setTitle("Z");

您也可以在ScatterDataModifier的构造函数中这样做。在这里执行可以保持构造函数更简单,并将轴配置靠近数据。

接下来,创建一个数据数组并将其填充

auto *dataArray = new QScatterDataArray;
dataArray->reserve(m_itemCount);
    ...
const float limit = qSqrt(m_itemCount) / 2.0f;
for (int i = -limit; i < limit; ++i) {
    for (int j = -limit; j < limit; ++j) {
        const float x = float(i) + 0.5f;
        const float y = qCos(qDegreesToRadians(float(i * j) / m_curveDivider));
        const float z = float(j) + 0.5f;
        dataArray->append(QScatterDataItem({x, y, z}));
    }
}

最后,让代理开始使用我们给出的数据

m_graph->seriesList().at(0)->dataProxy()->resetArray(dataArray);

现在,图表有了数据,可以使用了。有关添加控件以控制图表的信息,请参阅使用控件控制图表

替换默认输入处理

在构造函数中初始化m_inputHandler,使其指向散点图实例的指针

m_inputHandler(new AxesInputHandler(scatter))

通过将Q3DScatter的当前活动输入处理程序设置为AxesInputHandler(它实现了自定义行为)来替换默认的输入处理机制

// Give ownership of the handler to the graph and make it the active handler
m_graph->setActiveInputHandler(m_inputHandler);

输入处理程序需要访问图表的轴,因此将它们传递给它

// Give our axes to the input handler
m_inputHandler->setAxes(m_graph->axisX(), m_graph->axisZ(), m_graph->axisY());
扩展鼠标事件处理

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

class AxesInputHandler : public Q3DInputHandler

通过重新实现一些鼠标事件来开始扩展默认功能。首先,扩展mousePressEvent。为此添加一个表示左键点击的m_mousePressed标志,并保留 默认功能的其他部分

void AxesInputHandler::mousePressEvent(QMouseEvent *event, const QPoint &mousePos)
{
    Q3DInputHandler::mousePressEvent(event, mousePos);
    if (Qt::LeftButton == event->button())
        m_mousePressed = true;
}

接下来,修改mouseReleaseEvent以清除标志,并重置内部状态

void AxesInputHandler::mouseReleaseEvent(QMouseEvent *event, const QPoint &mousePos)
{
    Q3DInputHandler::mouseReleaseEvent(event, mousePos);
    m_mousePressed = false;
    m_state = StateNormal;
}

然后,修改mouseMoveEvent。检查m_mousePressed标志是true,并且内部状态不是StateNormal。如果是这样,为鼠标移动距离计算设置输入位置,并调用轴拖动函数(有关详细信息,请参阅实现轴拖动

void AxesInputHandler::mouseMoveEvent(QMouseEvent *event, const QPoint &mousePos)
{
    // Check if we're trying to drag axis label
    if (m_mousePressed && m_state != StateNormal) {
        setPreviousInputPos(inputPosition());
        setInputPosition(mousePos);
        handleAxisDragging();
    } else {
        Q3DInputHandler::mouseMoveEvent(event, mousePos);
    }
}
实现轴拖动

首先,开始监听来自图表的选择信号。在构造函数中这样做,并将其连接到handleElementSelected方法

// Connect to the item selection signal from graph
connect(graph, &QAbstract3DGraph::selectedElementChanged, this,
        &AxesInputHandler::handleElementSelected);

handleElementSelected中,检查选择类型,并根据该类型设置内部状态

switch (type) {
case QAbstract3DGraph::ElementAxisXLabel:
    m_state = StateDraggingX;
    break;
case QAbstract3DGraph::ElementAxisYLabel:
    m_state = StateDraggingY;
    break;
case QAbstract3DGraph::ElementAxisZLabel:
    m_state = StateDraggingZ;
    break;
default:
    m_state = StateNormal;
    break;
}

实际的拖动逻辑在handleAxisDragging方法中实现,该方法从满足条件时的mouseMoveEvent调用

// Check if we're trying to drag axis label
if (m_mousePressed && m_state != StateNormal) {

handleAxisDragging中,首先从活动相机获取场景方向

// Get scene orientation from active camera
float xRotation = scene()->activeCamera()->xRotation();
float yRotation = scene()->activeCamera()->yRotation();

然后,根据方向计算基于鼠标移动方向的调节器

// Calculate directional drag multipliers based on rotation
float xMulX = qCos(qDegreesToRadians(xRotation));
float xMulY = qSin(qDegreesToRadians(xRotation));
float zMulX = qSin(qDegreesToRadians(xRotation));
float zMulY = qCos(qDegreesToRadians(xRotation));

之后,计算鼠标移动,并根据相机的y旋转修改它

// Get the drag amount
QPoint move = inputPosition() - previousInputPos();

// Flip the effect of y movement if we're viewing from below
float yMove = (yRotation < 0) ? -move.y() : move.y();

然后,将移动距离应用到正确的轴上

// Adjust axes
switch (m_state) {
case StateDraggingX:
    distance = (move.x() * xMulX - yMove * xMulY) / m_speedModifier;
    m_axisX->setRange(m_axisX->min() - distance, m_axisX->max() - distance);
    break;
case StateDraggingZ:
    distance = (move.x() * zMulX + yMove * zMulY) / m_speedModifier;
    m_axisZ->setRange(m_axisZ->min() + distance, m_axisZ->max() + distance);
    break;
case StateDraggingY:
    distance = move.y() / m_speedModifier; // No need to use adjusted y move here
    m_axisY->setRange(m_axisY->min() + distance, m_axisY->max() + distance);
    break;
default:
    break;
}

最后,添加一个用于设置拖动速度的函数

inline void setDragSpeedModifier(float modifier) { m_speedModifier = modifier; }

这是必要的,因为鼠标移动距离在屏幕坐标上是绝对的,您需要将其调整到轴范围。值越大,拖动速度越慢。注意,在这个示例中,场景缩放级别在确定拖动速度时没有考虑,所以当您改变缩放级别时,您会注意到范围调整的变化。

您还可以根据轴范围和摄像机缩放级别自动调整修饰符。

表面图形

表面图形 标签下,使用 Q3DSurface 创建一个 3D 表面图形。示例展示了如何

  • 设置基本的 QSurfaceDataProxy 并为其设置数据。
  • 使用 QHeightMapSurfaceDataProxy 来显示 3D 高度图。
  • 使用地形数据来创建 3D 高度图。
  • 使用三种不同的选择模式来研究图形。
  • 使用轴范围来显示图形的选择部分。
  • 设置自定义表面梯度。
  • 使用 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);
多序列高度图数据

通过实例化一个包含高度数据的 QImageQHeightMapSurfaceDataProxy 来创建高度图。使用 QHeightMapSurfaceDataProxy::setValueRanges() 定义地图的值范围。在这个示例中,地图是从34.0° N - 40.0° N和18.0° E - 24.0° E的想象位置开始的。这些值用于定位地图在坐标系上的位置。

// Create the first surface layer
QImage heightMapImageOne(":/data/layer_1.png");
m_heightMapProxyOne = new QHeightMapSurfaceDataProxy(heightMapImageOne);
m_heightMapSeriesOne = new QSurface3DSeries(m_heightMapProxyOne);
m_heightMapSeriesOne->setItemLabelFormat(u"(@xLabel, @zLabel): @yLabel"_s);
m_heightMapProxyOne->setValueRanges(34.f, 40.f, 18.f, 24.f);

以相同的方式添加其他表面层,通过创建使用高度图图像的代理和系列来实现。

地形图数据

地形数据来自芬兰国家土地测绘。它提供了一个称为 Elevation Model 2 m 的产品,适用于此示例。地形数据来自利韦山顶。数据的精度远远超过了需求,因此它被压缩并编码成一个 PNG 文件。原始 ASCII 数据的高度值是通过乘数编码到 RGB 格式的,这个乘数您稍后会在代码片段中看到。乘数是通过将最大的 24 位值除以芬兰的最高点来计算的。

QHeightMapSurfaceDataProxy 只转换单字节值。为了利用芬兰国家土地测绘数据的高精度,从 PNG 文件中读取数据并解码成 QSurface3DSeries

首先,定义编码乘数

// Value used to encode height data as RGB value on PNG file
const float packingFactor = 11983.f;

然后执行实际解码

QImage heightMapImage(file);
uchar *bits = heightMapImage.bits();
int imageHeight = heightMapImage.height();
int imageWidth = heightMapImage.width();
int widthBits = imageWidth * 4;
float stepX = width / float(imageWidth);
float stepZ = height / float(imageHeight);

auto *dataArray = new QSurfaceDataArray;
dataArray->reserve(imageHeight);
for (int i = 0; i < imageHeight; ++i) {
    int p = i * widthBits;
    float z = height - float(i) * stepZ;
    auto *newRow = new QSurfaceDataRow;
    newRow->reserve(imageWidth);
    for (int j = 0; j < imageWidth; ++j) {
        uchar aa = bits[p + 0];
        uchar rr = bits[p + 1];
        uchar gg = bits[p + 2];
        uint color = uint((gg << 16) + (rr << 8) + aa);
        float y = float(color) / packingFactor;
        newRow->append(QSurfaceDataItem({float(j) * stepX, y, z}));
        p += 4;
    }
    dataArray->append(newRow);
}

dataProxy()->resetArray(dataArray);

现在,数据可供代理使用。

选择数据集

为了演示不同的代理,表面图形 有三个单选按钮来切换系列。

使用平方根 & 正弦,将简单生成的系列激活。首先,设置装饰功能,例如启用表面网格,并选择平面着色模式。接下来,定义坐标轴标签格式和值范围。设置自动标签旋转以提高低摄像头角度下的标签可读性。最后,确保已将正确的系列添加到图表中,而其他未添加。

m_sqrtSinSeries->setDrawMode(QSurface3DSeries::DrawSurfaceAndWireframe);
m_sqrtSinSeries->setFlatShadingEnabled(true);

m_graph->axisX()->setLabelFormat("%.2f");
m_graph->axisZ()->setLabelFormat("%.2f");
m_graph->axisX()->setRange(sampleMin, sampleMax);
m_graph->axisY()->setRange(0.f, 2.f);
m_graph->axisZ()->setRange(sampleMin, sampleMax);
m_graph->axisX()->setLabelAutoRotation(30.f);
m_graph->axisY()->setLabelAutoRotation(90.f);
m_graph->axisZ()->setLabelAutoRotation(30.f);

m_graph->removeSeries(m_heightMapSeriesOne);
m_graph->removeSeries(m_heightMapSeriesTwo);
m_graph->removeSeries(m_heightMapSeriesThree);
m_graph->removeSeries(m_topography);
m_graph->removeSeries(m_highlight);

m_graph->addSeries(m_sqrtSinSeries);

使用多系列高度图,激活高度图系列并禁用其他系列。对于高度图表面,自动调整Y轴范围效果良好,因此请确保已设置。

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

使用纹理地形,激活地形系列并禁用其他系列。为此系列启用自定义输入处理器,以便能够突出显示其上的区域。

m_graph->setActiveInputHandler(m_customInputHandler);

有关此数据集自定义输入处理器的信息,请参阅使用自定义输入处理器启用缩放和平移

选择模式

Q3DSurface 支持的三个选择模式可以使用单选按钮使用。要激活选择模式或清除它,请添加以下行方法

void toggleModeNone() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionNone); }
void toggleModeItem() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItem); }
void toggleModeSliceRow() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItemAndRow
                                                      | QAbstract3DGraph::SelectionSlice
                                                      | QAbstract3DGraph::SelectionMultiSeries); }
void toggleModeSliceColumn() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItemAndColumn
                                                         | QAbstract3DGraph::SelectionSlice
                                                         | QAbstract3DGraph::SelectionMultiSeries); }

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

将支持从小部件控件设置X范围到图表中。

void SurfaceGraphModifier::setAxisXRange(float min, float max)
{
    m_graph->axisX()->setRange(min, max);
}

以相同方式添加对Z范围的支持。

自定义表面渐变

对于Sqrt & Sin数据集,可以使用两个按钮使用自定义表面渐变。使用QLinearGradient定义渐变,其中设置了所需颜色。此外,将颜色样式更改为Q3DTheme::ColorStyleRangeGradient以使用渐变。

QLinearGradient gr;
gr.setColorAt(0.f, Qt::black);
gr.setColorAt(0.33f, Qt::blue);
gr.setColorAt(0.67f, Qt::red);
gr.setColorAt(1.f, Qt::yellow);

m_sqrtSinSeries->setBaseGradient(gr);
m_sqrtSinSeries->setColorStyle(Q3DTheme::ColorStyleRangeGradient);
将自定义网格添加到应用程序

将网格文件添加到CMakeLists.txt中进行cmake构建

set(graphgallery_resource_files
    ...
    "data/oilrig.obj"
    "data/pipe.obj"
    "data/refinery.obj"
    ...
)

qt6_add_resources(graphgallery "graphgallery"
    PREFIX
        "/"
    FILES
        ${graphgallery_resource_files}
)

此外,在qrc资源文件中添加它们以供qmake使用

<RCC>
    <qresource prefix="/">
        ...
        <file>data/refinery.obj</file>
        <file>data/oilrig.obj</file>
        <file>data/pipe.obj</file>
        ...
    </qresource>
</RCC>
将自定义项添加到图表中

对于多系列高度图数据集,自定义项被插入到图表中,可以使用复选框切换开启或关闭。其他视觉质量也可以通过另一组复选框控制,包括两个顶层可见性和底部层突出显示。

首先创建一个小的QImage。用单色填充以用作自定义对象的颜色

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

© 2024 Qt公司有限公司。本文档中包含的文档贡献属于各自所有者。提供的文档受自由软件基金会发布、根据GNU自由文档许可证版本1.3条款许可。Qt及其相关标志是芬兰及其它国家或地区的Qt公司有限公司的商标。商标。所有其他商标均为各自所有者的财产。