坐标处理

在 QML 中使用自定义输入处理器实现坐标拖动,并创建自定义坐标格式器。

坐标处理演示了与坐标相关的两种不同的自定义功能。这些功能在应用程序中都有自己独立的选项卡。

以下章节只集中介绍这些功能,省略了基本功能的解释 - 更多详细的 QML 示例文档,请参阅 简单散点图

运行示例

要从 Qt Creator 运行示例,请打开 欢迎 模式并从 示例 中选择示例。更多信息,请访问 构建和运行示例

坐标拖动

坐标拖动 选项卡中,使用 QML 实现 自定义输入处理器,从而可以通过拖动坐标标签来更改坐标范围。此外,使用正交投影并动态更新自定义项的属性。

覆盖默认输入处理

要禁用默认输入处理机制,将 Scatter3D 图表的 active 输入处理器设置为 null

Scatter3D {
    id: scatterGraph
    inputHandler: null
    ...

然后,添加一个 MouseArea 并将其设置为填充父项,也就是包含 scatterGraph 的同一 Item。同时,设置它仅接受鼠标左键点击,如本例中其他按钮不需要

MouseArea {
    anchors.fill: parent
    hoverEnabled: true
    acceptedButtons: Qt.LeftButton
    ...

然后,监听鼠标点击,并在捕获到后向图表发送选择查询

onPressed: (mouse)=> {
               scatterGraph.scene.selectionQueryPosition = Qt.point(mouse.x, mouse.y);
           }

onPositionChanged 信号处理器捕获当前鼠标位置,该位置将被用于计算移动距离

onPositionChanged: (mouse)=> {
                       currentMouseX = mouse.x;
                       currentMouseY = mouse.y;
    ...

onPositionChanged 的末尾,保存之前的鼠标位置以用于后续的移动距离计算

...
previousMouseX = currentMouseX;
previousMouseY = currentMouseY;
}
将鼠标移动转换为坐标范围更改

scatterGraph 中,监听 onSelectedElementChanged 信号。该信号在 inputAreaonPressed 中发出选择查询之后被触发。将元素类型设置为您在主组件中定义的属性中(property int selectedAxisLabel: -1),因为这个类型正是您感兴趣的类型

onSelectedElementChanged: {
    if (selectedElement >= AbstractGraph3D.ElementAxisXLabel
            && selectedElement <= AbstractGraph3D.ElementAxisZLabel) {
        selectedAxisLabel = selectedElement;
    } else {
        selectedAxisLabel = -1;
    }
}

然后,回到 inputAreaonPositionChanged,检查是否按下了鼠标按钮并且您已选择当前的坐标标签。如果条件满足,调用从鼠标移动到坐标范围更新的转换函数

...
if (pressed && selectedAxisLabel != -1)
    axisDragView.dragAxis();
...

在这种情况下,转换很简单,因为相机旋转是固定的。您可以使用一些预先计算好的值,计算鼠标移动距离,并将这些值应用于所选坐标范围

function dragAxis() {
    // Do nothing if previous mouse position is uninitialized
    if (previousMouseX === -1)
        return;

    // Directional drag multipliers based on rotation. Camera is locked to 45 degrees, so we
    // can use one precalculated value instead of calculating xx, xy, zx and zy individually
    var cameraMultiplier = 0.70710678;

    // Calculate the mouse move amount
    var moveX = currentMouseX - previousMouseX;
    var moveY = currentMouseY - previousMouseY;

    // Adjust axes
    switch (selectedAxisLabel) {
    case AbstractGraph3D.ElementAxisXLabel:
        var distance = ((moveX - moveY) * cameraMultiplier) / dragSpeedModifier;
        // Check if we need to change min or max first to avoid invalid ranges
        if (distance > 0) {
            scatterGraph.axisX.min -= distance;
            scatterGraph.axisX.max -= distance;
        } else {
            scatterGraph.axisX.max -= distance;
            scatterGraph.axisX.min -= distance;
        }
        break;
    case AbstractGraph3D.ElementAxisYLabel:
        distance = moveY / dragSpeedModifier;
        // Check if we need to change min or max first to avoid invalid ranges
        if (distance > 0) {
            scatterGraph.axisY.max += distance;
            scatterGraph.axisY.min += distance;
        } else {
            scatterGraph.axisY.min += distance;
            scatterGraph.axisY.max += distance;
        }
        break;
    case AbstractGraph3D.ElementAxisZLabel:
        distance = ((moveX + moveY) * cameraMultiplier) / dragSpeedModifier;
        // Check if we need to change min or max first to avoid invalid ranges
        if (distance > 0) {
            scatterGraph.axisZ.max += distance;
            scatterGraph.axisZ.min += distance;
        } else {
            scatterGraph.axisZ.min += distance;
            scatterGraph.axisZ.max += distance;
        }
        break;
    }
}

欲进行更复杂的鼠标移动到轴范围更新的转换,请参阅图库

其他特性

示例还演示了如何使用正交投影以及如何动态更新自定义项目的属性。

正交投影非常简单。您只需修改scatterGraphorthoProjection属性。示例中有一个用于切换正交投影的按钮。

Button {
    id: orthoToggle
    width: axisDragView.portraitMode ? parent.width : parent.width / 3
    text: "Display Orthographic"
    anchors.left: axisDragView.portraitMode ? parent.left : rangeToggle.right
    anchors.top: axisDragView.portraitMode ? rangeToggle.bottom : parent.top
    onClicked: {
        if (scatterGraph.orthoProjection) {
            text = "Display Orthographic";
            scatterGraph.orthoProjection = false;
            // Orthographic projection disables shadows, so we need to switch them back on
            scatterGraph.shadowQuality = AbstractGraph3D.ShadowQualityMedium
        } else {
            text = "Display Perspective";
            scatterGraph.orthoProjection = true;
        }
    }
}

对于自定义项,将其添加到scatterGraphcustomItemList中。

customItemList: [
    Custom3DItem {
        id: qtCube
        meshFile: ":/qml/qmlaxishandling/cube.obj"
        textureFile: ":/qml/qmlaxishandling/cubetexture.png"
        position: Qt.vector3d(0.65, 0.35, 0.65)
        scaling: Qt.vector3d(0.3, 0.3, 0.3)
    }
]

您实现一个定时器来添加、删除和旋转图中的所有项,并使用相同的定时器来旋转自定义项。

onTriggered: {
    rotationAngle = rotationAngle + 1;
    qtCube.setRotationAxisAndAngle(Qt.vector3d(1, 0, 1), rotationAngle);
    ...

轴格式化器

在“轴格式化器”选项卡中,创建自定义轴格式化器。它还展示了如何使用预定义的轴格式化器。

自定义轴格式化器

自定义轴格式化器需要继承QValue3DAxisFormatter,这不能仅使用QML代码完成。在此示例中,轴将浮点值解释为时间戳,并在轴标签中显示日期。为此,引入了一个名为CustomFormatter的新类,它是QValue3DAxisFormatter的子类

class CustomFormatter : public QValue3DAxisFormatter
{
...

由于QScatter3DSeries的浮点值无法直接转换为QDateTime值,因为数据宽度不同,需要在这两个值之间进行某种映射。为了进行映射,指定格式化器的起始日期,并将QScatter3DSeries中的浮点值解释为相对于该起始值的日期偏移。起始日期作为属性给出

Q_PROPERTY(QDate originDate READ originDate WRITE setOriginDate NOTIFY originDateChanged)

对于从值到QDateTime的映射,使用valueToDateTime()方法。

QDateTime CustomFormatter::valueToDateTime(qreal value) const
{
    return m_originDate.startOfDay().addMSecs(qint64(oneDayMs * value));
}

为了让CustomFormatter作为轴格式化器工作,需要重写一些虚拟方法

virtual QValue3DAxisFormatter *createNewInstance() const;
virtual void populateCopy(QValue3DAxisFormatter &copy) const;
virtual void recalculate();
virtual QString stringForValue(qreal value, const QString &format) const;

前两个很简单,只需创建一个CustomFormatter的新实例,并将必要的数据复制到其中。使用这些方法创建和更新用于渲染的格式化器的缓存。记得调用populateCopy()的方法的超类实现。

QValue3DAxisFormatter *CustomFormatter::createNewInstance() const
{
    return new CustomFormatter();
}

void CustomFormatter::populateCopy(QValue3DAxisFormatter &copy) const
{
    QValue3DAxisFormatter::populateCopy(copy);

    CustomFormatter *customFormatter = static_cast<CustomFormatter *>(&copy);
    customFormatter->m_originDate = m_originDate;
    customFormatter->m_selectionFormat = m_selectionFormat;
}

CustomFormatter的大部分工作都在recalculate()方法中完成,在其中我们的格式化器计算网格、子网格和标签位置,格式化标签字符串。在自定义格式化器中,忽略轴的段数,总是在午夜绘制网格线。子段数和标签定位是正常处理的。

void CustomFormatter::recalculate()
{
    // We want our axis to always have gridlines at date breaks

    // Convert range into QDateTimes
    QDateTime minTime = valueToDateTime(qreal(axis()->min()));
    QDateTime maxTime = valueToDateTime(qreal(axis()->max()));

    // Find out the grid counts
    QTime midnight(0, 0);
    QDateTime minFullDate(minTime.date(), midnight);
    int gridCount = 0;
    if (minFullDate != minTime)
        minFullDate = minFullDate.addDays(1);
    QDateTime maxFullDate(maxTime.date(), midnight);

    gridCount += minFullDate.daysTo(maxFullDate) + 1;
    int subGridCount = axis()->subSegmentCount() - 1;

    // Reserve space for position arrays and label strings
    gridPositions().resize(gridCount);
    subGridPositions().resize((gridCount + 1) * subGridCount);
    labelPositions().resize(gridCount);
    labelStrings().reserve(gridCount);

    // Calculate positions and format labels
    qint64 startMs = minTime.toMSecsSinceEpoch();
    qint64 endMs = maxTime.toMSecsSinceEpoch();
    qreal dateNormalizer = endMs - startMs;
    qreal firstLineOffset = (minFullDate.toMSecsSinceEpoch() - startMs) / dateNormalizer;
    qreal segmentStep = oneDayMs / dateNormalizer;
    qreal subSegmentStep = 0;
    if (subGridCount > 0)
        subSegmentStep = segmentStep / qreal(subGridCount + 1);

    for (int i = 0; i < gridCount; i++) {
        qreal gridValue = firstLineOffset + (segmentStep * qreal(i));
        gridPositions()[i] = float(gridValue);
        labelPositions()[i] = float(gridValue);
        labelStrings() << minFullDate.addDays(i).toString(axis()->labelFormat());
    }

    for (int i = 0; i <= gridCount; i++) {
        if (subGridPositions().size()) {
            for (int j = 0; j < subGridCount; j++) {
                float position;
                if (i)
                    position =  gridPositions().at(i - 1) + subSegmentStep * (j + 1);
                else
                    position =  gridPositions().at(0) - segmentStep + subSegmentStep * (j + 1);
                if (position > 1.0f || position < 0.0f)
                    position = gridPositions().at(0);
                subGridPositions()[i * subGridCount + j] = position;
            }
        }
    }
}

轴标签以仅显示日期的方式格式化。然而,为了提高选择标签时间戳的分辨率,为自定义格式化器指定另一个属性,允许用户自定义它。

Q_PROPERTY(QString selectionFormat READ selectionFormat WRITE setSelectionFormat NOTIFY selectionFormatChanged)

此选择格式属性用于重写的stringToValue方法,其中提交的格式被忽略,并替换为自定义选择格式。

QString CustomFormatter::stringForValue(qreal value, const QString &format) const
{
    Q_UNUSED(format);

    return valueToDateTime(value).toString(m_selectionFormat);
}

要将我们自己的新自定义格式化器公开到QML中,声明它并将其作为QML模块。有关如何做到这一点的信息,请参阅表面图库

QML

在QML代码中,为每个维度定义不同的轴

axisZ: valueAxis
axisY: logAxis
axisX: dateAxis

Z轴只是一个常规的ValueAxis3D

ValueAxis3D {
    id: valueAxis
    segmentCount: 5
    subSegmentCount: 2
    labelFormat: "%.2f"
    min: 0
    max: 10
}

对于Y轴,定义一个对数轴。要让ValueAxis3D显示对数刻度,指定对数轴的formatter属性为LogValueAxis3DFormatter

ValueAxis3D {
    id: logAxis
    formatter: LogValueAxis3DFormatter {
        id: logAxisFormatter
        base: 10
        autoSubGrid: true
        showEdgeLabels: true
    }
    labelFormat: "%.2f"
}

最后,对于X轴使用新的CustomFormatter

ValueAxis3D {
    id: dateAxis
    formatter: CustomFormatter {
        originDate: "2023-01-01"
        selectionFormat: "yyyy-MM-dd HH:mm:ss"
    }
    subSegmentCount: 2
    labelFormat: "yyyy-MM-dd"
    min: 0
    max: 14
}

应用程序的其余部分包含用于修改轴和显示图表的相对直观的逻辑。

示例内容

示例项目 @ code.qt.io

© 2024 The Qt Company Ltd. 本文档中包含的贡献的版权属于其各自的所有者。本提供的文档是根据自由软件基金会发布的条款,并根据GNU自由文档许可证版本1.3许可的。Qt及其相关标志是The Qt Company Ltd.在芬兰及/或在其他国家/地区的商标。所有其他商标均属于其各自的所有者。