本页内容

轴处理

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

“轴处理”演示了轴的两种不同自定义功能。这些功能在应用程序中各有独立的选项卡。

以下各节仅专注于这些功能,不再赘述基本功能——如需更详细的 QML 示例文档,请参阅《简单的散点图》。

包含球体和基于日期的坐标轴的3D散点图

运行示例

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

轴拖动

在“Axis Dragging ”选项卡中,使用 QML 实现一个自定义输入处理程序,该处理程序可让您通过拖动坐标轴标签来更改坐标轴范围。此外,还需使用正交投影并动态更新自定义项的属性。

覆盖默认输入处理

要禁用默认输入处理机制,请取消设置Scatter3D 的默认输入处理程序:

unsetDefaultInputHandler();
    ...

然后,添加一个MouseArea ,并将其设置为填满父容器——即包含我们的scatterGraph 的Item 。此外,将其设置为仅接受鼠标左键点击,因为在此示例中不需要其他按键:

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

接着,监听鼠标点击事件,并在捕获事件时向图形发送选择查询:

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

onPositionChanged 信号处理程序会捕获当前鼠标位置,该位置将用于移动距离的计算:

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

在 `onPositionChanged` 结束时,保存上一次鼠标位置,以便用于稍后将介绍的移动距离计算:

...
    previousMouseX = currentMouseX;
    previousMouseY = currentMouseY;
}
将鼠标移动转换为轴范围变化

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

onSelectedElementChanged: {
    if (selectedElement >= Graphs3D.ElementType.AxisXLabel
            && selectedElement <= Graphs3D.ElementType.AxisZLabel) {
        selectedAxisLabel = selectedElement;
    } else {
        selectedAxisLabel = -1;
    }
}

然后,回到inputArea 的onPositionChanged 中,检查鼠标按钮是否被按下,以及当前是否选择了某个轴标签。如果条件满足,则调用将鼠标移动转换为轴范围更新的函数:

...
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 Graphs3D.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 Graphs3D.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 Graphs3D.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;
    }
}

有关将鼠标移动转换为轴范围更新的更复杂方法,请参阅“图形图库”。

其他功能

该示例还演示了如何使用正交投影,以及如何实时更新自定义项的属性。

正交投影非常简单。您只需更改scatterGraph 的orthoProjection 属性即可。示例中提供了一个按钮用于开启或关闭该功能:

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 = Graphs3D.ShadowQuality.Medium
        } else {
            text = "Display Perspective";
            scatterGraph.orthoProjection = true;
        }
    }
}

对于自定义项目,请在scatterGraph 的customItemList 中添加一项:

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);
    ...

坐标轴格式化器

在“Axis Formatter ”选项卡中,创建一个自定义坐标轴格式化器。该示例还演示了如何使用预定义的坐标轴格式化器。

自定义坐标轴格式器

要自定义坐标轴格式器,需要继承 `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 模块。有关具体操作方法,请参阅“Surface Graph Gallery”。

QML

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

axisZ: valueAxis
axisY: logAxis
axisX: dateAxis

Z 轴只是一个普通的 `Value3DAxis`:

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

对于 Y 轴,定义一个对数轴。要使Value3DAxis 显示对数刻度,请将轴的formatter 属性设置为LogValue3DAxisFormatter :

Value3DAxis {
    id: logAxis
    formatter: LogValue3DAxisFormatter {
        id: logAxisFormatter
        base: 10
        autoSubGrid: true
        edgeLabelsVisible: true
    }
    labelFormat: "%.2f"
}

最后,X 轴使用新的CustomFormatter :

Value3DAxis {
    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

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