轴线处理

在 QML 中使用自定义输入处理程序实现轴线拖动,并创建自定义的轴线格式化程序。

轴线处理 展示了与轴线相关的两个不同的自定义功能。该应用程序中为这两个功能提供了自己的标签页。

以下部分仅集中讨论这些功能,跳过了基本功能的解释 - 对于更详细的 QML 示例文档,请参阅 简单散点图

运行示例

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

轴线拖动

轴线拖动 标签页中,在 QML 中实现一个自定义输入处理程序,从而使您可以拖动轴线标签以更改轴线范围。此外,使用正交投影并动态更新自定义项目的属性。

覆盖默认输入处理

要禁用默认输入处理机制,请将 Scatter3D 图表的活跃输入处理程序设置为 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.ElementType.AxisXLabel
            && selectedElement <= AbstractGraph3D.ElementType.AxisZLabel) {
        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.ElementType.AxisXLabel:
        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.ElementType.AxisYLabel:
        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.ElementType.AxisZLabel:
        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.ShadowQuality.Medium
        } else {
            text = "Display Perspective";
            scatterGraph.orthoProjection = true;
        }
    }
}

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

customItemList: [
    Custom3DItem {
        id: qtCube
        meshFile: ":/qml/axishandling/cube.mesh"
        textureFile: ":/qml/axishandling/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需要重写一些虚拟方法。

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

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

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

void CustomFormatter::populateCopy(QValue3DAxisFormatter &copy)
{
    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;

    QList<float> gridPositions;
    QList<float> subGridPositions;
    QList<float> labelPositions;
    QStringList labelStrings;

    // 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;
            }
        }
    }
    setGridPoitions(gridPositions);
    setSubGridPositions(subGridPositions);
    setlabelPositions(labelPositions);
    setLabelStrings(labelStrings);
}

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

Q_PROPERTY(QString selectionFormat READ selectionFormat WRITE setSelectionFormat NOTIFY
               selectionFormatChanged)

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

QString CustomFormatter::stringForValue(qreal value, const QString &format)
{
    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显示对数刻度,指定LogValueAxis3DFormatter作为轴的formatter属性

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 Qt公司有限公司。本文件中包含的文档贡献是各自所有者的版权。所提供的文档是根据自由软件基金会发布的GNU自由文档许可协议版本1.3许可的。Qt及其 respective 商标是芬兰及/或世界各地的Qt公司有限公司的商标。所有其他商标均为其各自所有者的财产。