QML Charts组件之折线图的鼠标交互

发布于:2025-09-15 ⋅ 阅读:(19) ⋅ 点赞:(0)

前言

接上文(QML Charts组件之折线图的基础属性),本文将重点介绍LineSeries的鼠标交互,包括:鼠标拖拽平移、滚轮缩放等操作。


相关系列


代码示例详解(LineSeriesDemo3.qml)

示例文件:Series/LineSeriesDemo3.qml

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtCharts

Rectangle {
    Layout.fillWidth: true
    Layout.fillHeight: true

    ColumnLayout {
        anchors.fill: parent

        ChartView {
            id: chartView
            title: "折线图示例"
            titleFont.bold: true
            titleFont.pointSize: 14

            Layout.fillWidth: true
            Layout.fillHeight: true
            antialiasing: true

            ValueAxis {
                id: axis_x
                min: 0
                max: 10
                tickCount: 11
            }

            ValueAxis {
                id: axis_y
                min: 0
                max: 10
                tickCount: 11
            }

            LineSeries {
                id: series
                name: "line"

                color: "#1296FF"
                width: 3
                bestFitLineColor : "#00FF00"
                bestFitLineVisible : true

                // 设置点是否可见
                pointsVisible : true

                // 设置点标签是否可见
                pointLabelsVisible : true

                // 设置点标签文本颜色
                pointLabelsColor: "#FF0000"

                // 设置点标签字体
                pointLabelsFont.bold: true
                pointLabelsFont.pointSize: 12
                pointLabelsFont.family: "Courier"

                // 设置点标签显示格式
                // 具体格式可能受Qt版本或平台影响
                // 如果数据太多,会影响性能
                // 格式标签限制:不支持更复杂的运算或格式化指令,如果是小数,可能需要对点数据预处理
                pointLabelsFormat : "(@xPoint,@yPoint)"

                // 控制当点标签超出绘图区域时是否被裁剪,默认为true
                // 设为false可允许标签显示在绘图区域之外,但需要注意可能布局重叠。
                pointLabelsClipping : true

                axisX: axis_x
                axisY: axis_y

                onClicked: function(point){
                    console.log("onClicked: " + Math.round(point.x) + ", " + Math.round(point.y));
                }
            }

            // 交互:拖拽平移与滚轮缩放
            property real __panStartX: 0
            property real __panStartY: 0
            property real __panStartMinX: 0
            property real __panStartMaxX: 0
            property real __panStartMinY: 0
            property real __panStartMaxY: 0

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

                onPressed: function(mouse) {
                    var pa = chartView.plotArea
                    if (!(mouse.x >= pa.x && mouse.x <= pa.x + pa.width &&
                          mouse.y >= pa.y && mouse.y <= pa.y + pa.height)) {
                        return
                    }
                    chartView.__panStartX = mouse.x
                    chartView.__panStartY = mouse.y
                    chartView.__panStartMinX = axis_x.min
                    chartView.__panStartMaxX = axis_x.max
                    chartView.__panStartMinY = axis_y.min
                    chartView.__panStartMaxY = axis_y.max
                }

                onPositionChanged: function(mouse) {
                    if (!pressed) return
                    var pa = chartView.plotArea
                    if (pa.width <= 0 || pa.height <= 0) return

                    var dx = mouse.x - chartView.__panStartX
                    var dy = mouse.y - chartView.__panStartY

                    var rangeX = chartView.__panStartMaxX - chartView.__panStartMinX
                    var rangeY = chartView.__panStartMaxY - chartView.__panStartMinY

                    // 像素 -> 数值映射(注意Y轴方向)
                    var valueDeltaX = -dx * rangeX / pa.width
                    var valueDeltaY =  dy * rangeY / pa.height

                    axis_x.min = chartView.__panStartMinX + valueDeltaX
                    axis_x.max = chartView.__panStartMaxX + valueDeltaX

                    axis_y.min = chartView.__panStartMinY + valueDeltaY
                    axis_y.max = chartView.__panStartMaxY + valueDeltaY
                }
            }

            WheelHandler {
                // 以光标为中心缩放,支持鼠标与触控板
                acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
                target: chartView
                onWheel: function(event) {
                    var pa = chartView.plotArea
                    //var pos = event.point
                    var x = event.x
                    var y = event.y
                    if (!(x >= pa.x && x <= pa.x + pa.width &&
                          y >= pa.y && y <= pa.y + pa.height)) {
                        return
                    }

                    // 计算缩放步数(15度/步),向上滚动放大
                    var degrees = event.angleDelta.y / 8.0
                    var steps = degrees / 15.0
                    var factor = Math.pow(1.2, -steps)

                    var minX = axis_x.min
                    var maxX = axis_x.max
                    var minY = axis_y.min
                    var maxY = axis_y.max
                    var rangeX = maxX - minX
                    var rangeY = maxY - minY

                    if (rangeX <= 0 || rangeY <= 0) return

                    var fx = (x - pa.x) / pa.width
                    var fy = (y - pa.y) / pa.height
                    fx = Math.max(0, Math.min(1, fx))
                    fy = Math.max(0, Math.min(1, fy))

                    var newRangeX = rangeX * factor
                    var newRangeY = rangeY * factor

                    // 以光标数值位置为锚点缩放
                    var newMinX = minX + fx * (rangeX - newRangeX)
                    var newMinY = minY + fy * (rangeY - newRangeY)

                    axis_x.min = newMinX
                    axis_x.max = newMinX + newRangeX
                    axis_y.min = newMinY
                    axis_y.max = newMinY + newRangeY

                    event.accepted = true
                }
            }

            // Add data dynamically to the series
            Component.onCompleted: {
                for (var i = 0; i <= 5; i++) {
                    var num = Math.floor((Math.random()*10))
                    series.append(i, num);
                }
            }
        }

        Row {
            Layout.minimumHeight: 50

            Button {
                text: "append"
                width: 100
                height: 25

                onClicked: {
                    var num = Math.floor((Math.random()*10))
                    series.append(series.count, num)
                    axis_x.min++;
                    axis_x.max++;
                }
            }

            Button {
                text: "remove"
                width: 100
                height: 25

                onClicked: {
                    if (series.count > 0) {
                        series.removePoints(series.count-1, 1)
                        axis_x.min--;
                        axis_x.max--;
                    }
                }
            }
        }
    }
}

功能概览

这段 QML 代码实现了一个可交互的折线图窗口,能够进行鼠标拖拽平移和滚轮缩放操作。

功能 实现方式
显示折线图 使用ChartViewLineSeries
动态追加 / 删除数据 使用 series.appendseries.removePoints 方法。
鼠标拖拽平移 使用MouseArea ,记录按下/移动坐标。
滚轮缩放 使用WheelHandler ,以光标为中心缩放,支持鼠标与触控板。
point标签显示 使用pointLabelsFormat : "(@xPoint,@yPoint)", 显示坐标。
点击数据点 触发onClicked 信号,打印坐标。

运行效果

请添加图片描述


代码说明

折线属性: 详细的属性描述见上文 — QML Charts组件之折线图的基础属性

拖拽平移(鼠标左键按住拖图表):

MouseArea {
    anchors.fill: parent
    onPressed:  { /* 记录初始状态 */ }
    onPositionChanged: { /* 根据位移更新轴范围 */ }
}

1. 按下瞬间干什么?
记住四件事:

  • 鼠标当时在屏幕的 x、y 坐标。
  • 当时 X 轴的最小值、最大值。
  • 当时 Y 轴的最小值、最大值。
    它们一起构成初始快照,后面所有计算都以这个快照为基准,不会累积误差。

2. 拖动过程中干什么?

  • 把鼠标当前位置跟按下时的位置做减法,得到像素位移 dx、dy。
  • 用 dx 除以绘图区宽度,得到横向走了百分之几;同样处理 dy。
  • 把百分比乘以当时的轴范围,就换算成全局坐标该走多少。
  • 右拖 → 画面要向右,于是把轴整体向左平移相同的量;左拖相反。
  • 下拖 → 画面要向上,于是把轴整体向下平移;上拖相反。
  • 每移动一次鼠标,就重新给轴的最小、最大值赋一次新结果,图就实时跟过来了。

简而言之 鼠标拖拽 是把鼠标像素差按宽高比例变成轴坐标差,然后整体平移轴范围。


滚轮缩放(以光标为中心放大/缩小):

WheelHandler {
    onWheel: function(event) {
    		// 计算缩放步数(15度/步),向上滚动放大
    		...
			// 以光标数值位置为锚点缩放
			...
    }
}

1. 什么时候生效?
只在绘图区内部滚轮才处理,滚到外边就不管,避免整个窗口一起乱动。

2. 滚一次算几步?
系统告诉你这次滚了多少度,15° 算一步。向上滚一步算 +1,向下滚一步算 −1。
把步数代进公式 1.2^(-步数) 得到缩放因子:

  • 向上滚一步 → 因子 ≈ 1.2,表示放大 20%。
  • 向下滚一步 → 因子 ≈ 0.83,表示缩小 17%。

3. 怎样以光标为中心?

  • 先算出光标在绘图区里的横向百分比 fx、纵向百分比 fy。
  • 缩放前后,光标对应的那个数据值必须保持不变。
  • 于是用百分比做插值:
    • 新范围 = 旧范围 × 因子。
    • 新最小值 = 旧最小值 + fx × (旧范围 − 新范围)。
    • 这样光标在旧矩形里占多少比例,在新矩形里还是同样比例,视觉上就是以光标为锚点放大或缩小。

简而言之 滚轮缩放 是把滚了几步变成缩放因子,再用光标位置做插值,重新算轴边界。


工程下载

Git Code 下载链接:QML Charts组件之折线图的基础属性示例

在这里插入图片描述


参考