图例库

展示条形图、散点图和表面图的图库。

图例库 展示了所有三种图表类型以及部分特殊功能。这些图表在应用程序中都有自己的标签页。

运行示例

要从 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);
QSize screenSize = m_barsGraph->screen()->size();
m_barsGraph->setMinimumSize(QSize(screenSize.width() / 2, screenSize.height() / 1.75));
m_barsGraph->setMaximumSize(screenSize);
m_barsGraph->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_barsGraph->setFocusPolicy(Qt::StrongFocus);
hLayout->addWidget(m_barsGraph, 1);

auto *vLayout = new QVBoxLayout();
hLayout->addLayout(vLayout);

接下来,创建一个类来处理数据的添加以及其他与图表的交互

m_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::ShadowQuality::SoftMedium);
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::Mesh::BevelBar);
m_primarySeries->setMeshSmooth(false);

m_secondarySeries->setItemLabelFormat(u"Helsinki - @colLabel @rowLabel: @valueLabel"_s);
m_secondarySeries->setMesh(QAbstract3DSeries::Mesh::BevelBar);
m_secondarySeries->setMeshSmooth(false);
m_secondarySeries->setVisible(false);

将系列添加到图表中

m_graph->addSeries(m_primarySeries);
m_graph->addSeries(m_secondarySeries);

最后,通过调用 UI 中用于循环各种相机角度的相同方法来设置相机角度

changePresetCamera();

相机通过图形的场景对象进行控制

static int preset = int(QAbstract3DGraph::CameraPreset::Front);

m_graph->setCameraPreset((QAbstract3DGraph::CameraPreset) preset);

if (++preset > int(QAbstract3DGraph::CameraPreset::DirectlyBelow))
    preset = int(QAbstract3DGraph::CameraPreset::FrontLow);

有关使用场景的更多信息,请参阅Q3DScene

向图形中添加数据

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

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
...
QBarDataArray dataSet;
QBarDataArray dataSet2;

dataSet.reserve(m_years.size());
for (qsizetype year = 0; year < m_years.size(); ++year) {
    // Create a data row
    QBarDataRow dataRow(m_months.size());
    QBarDataRow dataRow2(m_months.size());
    for (qsizetype month = 0; month < m_months.size(); ++month) {
        // Add data to the row
        dataRow[month].setValue(tempOulu[year][month]);
        dataRow2[month].setValue(tempHelsinki[year][month]);
    }
    // Add the row to the set
    dataSet.append(dataRow);
    dataSet2.append(dataRow2);
}

// Add data to the data proxy (the data proxy assumes ownership of it)
m_primarySeries->dataProxy()->resetArray(dataSet, m_years, m_months);
m_secondarySeries->dataProxy()->resetArray(dataSet2, m_years, m_months);
使用小部件控制图形

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, m_modifier, &GraphModifier::rotateX);

GraphModifier中创建一个用于信号连接的槽。相机通过场景对象控制。这一次,指定沿中心点轨道的实际相机位置,而不是指定预设的相机角度

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

现在,您可以使用滑动条来旋转图形。

添加更多小部件以进行控制

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

自定义代理数据数据模式下,故意禁用了某些小部件控件。

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

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

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

只要将SelectionRowSelectionColumn之一设置,该方法与SelectionSliceSelectionItem标志相同。

缩放到选择

作为调整相机目标的示例,实现通过按钮点击进行缩放到选择的动画。动画初始化在构造函数中完成

m_defaultAngleX = m_graph->cameraXRotation();
m_defaultAngleY = m_graph->cameraYRotation();
m_defaultZoom = m_graph->cameraZoomLevel();
m_defaultTarget = m_graph->cameraTargetPosition();

m_animationCameraX.setTargetObject(m_graph);
m_animationCameraY.setTargetObject(m_graph);
m_animationCameraZoom.setTargetObject(m_graph);
m_animationCameraTarget.setTargetObject(m_graph);

m_animationCameraX.setPropertyName("cameraXRotation");
m_animationCameraY.setPropertyName("cameraYRotation");
m_animationCameraZoom.setPropertyName("cameraZoomLevel");
m_animationCameraTarget.setPropertyName("cameraTargetPosition");

int duration = 1700;
m_animationCameraX.setDuration(duration);
m_animationCameraY.setDuration(duration);
m_animationCameraZoom.setDuration(duration);
m_animationCameraTarget.setDuration(duration);

// The zoom always first zooms out above the graph and then zooms in
qreal zoomOutFraction = 0.3;
m_animationCameraX.setKeyValueAt(zoomOutFraction, QVariant::fromValue(0.0f));
m_animationCameraY.setKeyValueAt(zoomOutFraction, QVariant::fromValue(90.0f));
m_animationCameraZoom.setKeyValueAt(zoomOutFraction, QVariant::fromValue(50.0f));
m_animationCameraTarget.setKeyValueAt(zoomOutFraction,
                                      QVariant::fromValue(QVector3D(0.0f, 0.0f, 0.0f)));

函数GraphModifier::zoomToSelectedBar()包含缩放功能。QPropertyAnimation m_animationCameraTarget以 normalized value in range (-1, 1) 的 cameraTargetPosition 属性为目标。

确定所选条形图相对于轴的位置,并将此值用作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;
自定义数据代理

通过开启自定义代理数据数据模式,使用自定义数据集和相关代理。

定义一个简单的灵活的数据集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派生并提供一个简单的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();
        return;
    }
    const VariantDataItemList &itemList = m_dataSet->itemList();

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

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

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

    // Finally, reset the data array in the parent class
    resetArray(newProxyArray);
}
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创建3D散点图。示例展示了如何

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

设置散点图

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

m_graph->activeTheme()->setType(Q3DTheme::Theme::StoneMoss);
m_graph->setShadowQuality(QAbstract3DGraph::ShadowQuality::SoftHigh);
m_graph->setCameraPreset(QAbstract3DGraph::CameraPreset::Front);
m_graph->setCameraZoomLevel(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的构造函数中这样做。在这里操作可以使构造函数更简单,并将轴线配置靠近数据。

接下来,创建一个数据数组并填充它。

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

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

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

现在,图表已具备数据,可用于使用。有关添加小部件以控制图表的信息,请参阅使用小部件控制图表

替换默认输入处理

在构造函数中使用指向散点图实例的指针初始化 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::ElementType::AxisXLabel:
    m_state = StateDraggingX;
    break;
case QAbstract3DGraph::ElementType::AxisYLabel:
    m_state = StateDraggingY;
    break;
case QAbstract3DGraph::ElementType::AxisZLabel:
    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 = this->cameraXRotation();
float yRotation = this->cameraYRotation();

然后,根据方向计算鼠标移动方向的修改器。

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

之后,计算鼠标移动量,并根据相机的 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等高图。
  • 使用三种不同的选择模式来研究图形。
  • 使用坐标范围显示图形的选定部分。
  • 设置自定义表面梯度。
  • 使用QCustom3DItemQCustom3DLabel添加自定义项和标签。
  • 使用自定义输入处理器启用缩放和平移。
  • 突出显示表面上的一个区域。

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

简单表面与生成数据

首先,实例化一个新的QSurfaceDataProxy并将其附加到一个新的QSurface3DSeries

m_sqrtSinProxy = new QSurfaceDataProxy();
m_sqrtSinSeries = new QSurface3DSeries(m_sqrtSinProxy);

然后,用简单的平方根和正弦波数据填充代理。创建一个新的QSurfaceDataArray实例,并将QSurfaceDataRow元素添加到其中。通过调用resetArray()将创建的QSurfaceDataArray设置为QSurfaceDataProxy的数据数组。

QSurfaceDataArray dataArray;
dataArray.reserve(sampleCountZ);
for (int i = 0; i < sampleCountZ; ++i) {
    QSurfaceDataRow newRow;
    newRow.reserve(sampleCountX);
    // Keep values within range bounds, since just adding step can cause minor
    // drift due to the rounding errors.
    float z = qMin(sampleMax, (i * stepZ + sampleMin));
    for (int j = 0; j < sampleCountX; ++j) {
        float x = qMin(sampleMax, (j * stepX + sampleMin));
        float R = qSqrt(z * z + x * x) + 0.01f;
        float y = (qSin(R) / R + 0.24f) * 1.61f;
        newRow.append(QSurfaceDataItem(x, y, z));
    }
    dataArray.append(newRow);
}

m_sqrtSinProxy->resetArray(dataArray);
多系列高度图数据

通过实例化一个包含高度数据的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的产品,适用于此示例。地形数据来自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);

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

dataProxy()->resetArray(dataArray);

现在,数据可以通过代理使用。

选择数据集

为了展示不同的代理,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::ColorStyle::RangeGradient`以使用渐变。

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::ColorStyle::RangeGradient);
将自定义网格添加到应用程序中

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

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

qt6_add_resources(widgetgraphgallery "widgetgraphgallery"
    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.mesh",
                               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文件。使用该方法设置空文件可以清除纹理,表面则使用主题中的渐变或颜色。

使用自定义输入处理器启用缩放和拖动

使用纹理地形数据集,创建自定义输入处理器以在图上突出显示所选内容并允许拖动图表。

拖动实现类似于实现轴拖动中显示的实现。不同之处在于,在这个例子中,您只跟踪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.x() - halfWidth;
    if (startX < 0)
        startX = 0;
    int endX = position.x() + halfWidth;
    if (endX > (m_srcWidth - 1))
        endX = m_srcWidth - 1;
    int startZ = position.y() - halfHeight;
    if (startZ < 0)
        startZ = 0;
    int endZ = position.y() + halfHeight;
    if (endZ > (m_srcHeight - 1))
        endZ = m_srcHeight - 1;

    QSurfaceDataProxy *srcProxy = m_topographicSeries->dataProxy();
    const QSurfaceDataArray &srcArray = srcProxy->array();

    QSurfaceDataArray dataArray;
    dataArray.reserve(endZ - startZ);
    for (int i = startZ; i < endZ; ++i) {
        QSurfaceDataRow newRow;
        newRow.reserve(endX - startX);
        QSurfaceDataRow srcRow = srcArray.at(i);
        for (int j = startX; j < endX; ++j) {
            QVector3D pos = srcRow.at(j).position();
            pos.setY(pos.y() + m_heightAdjustment);
            newRow.append(QSurfaceDataItem(pos));
        }
        dataArray.append(newRow);
    }

    dataProxy()->resetArray(dataArray);
    setVisible(true);
}
为高亮系列添加渐变

由于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::ColorStyle::RangeGradient);

    handleZoomChange(ratio);
}

示例内容

示例项目 @ code.qt.io

© 2024 The Qt Company Ltd。包括在此处的文档贡献的版权属于各自的所有者。提供的文档受自由软件基金会发布的GNU自由文档许可版本1.3条款许可。Qt和相应的标志是芬兰以及其他全球国家The Qt Company Ltd.的商标。所有其他商标属于各自的所有者。