You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyQt+pyqtgraph实现散点图鼠标十字光标选点功能

PyQtGraph散点图十字光标精准吸附+点击选点实现方案

原有代码问题

  • 点位匹配仅计算X轴方向差值,完全忽略Y轴偏移,X坐标接近的散点会匹配错误
  • 未设置吸附距离阈值,鼠标远离散点时也会强制跳转十字线,体验不符合预期
  • 直接用数据坐标计算距离,未考虑X/Y轴量程差异,匹配结果和视觉感知的最近点不一致

实现逻辑

  • 绘图时将散点实例、坐标数据挂载为控件成员变量,方便交互逻辑调用
  • 鼠标移动时统一将鼠标位置、散点位置转换为屏幕像素坐标,计算欧氏距离找最近点
  • 设置像素级吸附阈值,仅当最近点距离小于阈值时触发十字线吸附,否则十字线跟随鼠标自由移动
  • 绑定鼠标左键点击事件,吸附状态下点击即可保存当前点位坐标供后续逻辑调用

参考代码

1. 改造散点绘制函数

import pyqtgraph as pg
import numpy as np
from PyQt5.QtCore import Qt

def plot_2d_scatter(graphWidget,x,z,color=(66, 245, 72)):
    graphWidget.clear()
    brush = pg.mkBrush(color)
    scatter = pg.ScatterPlotItem(size=5, brush=brush)
    scatter.addPoints(x,z)
    graphWidget.addItem(scatter)
    
    # 存储交互所需的资源
    graphWidget.scatter_item = scatter
    graphWidget.plot_data = np.column_stack((np.array(x), np.array(z)))
    graphWidget.selected_point = None
    graphWidget.hover_point = None
    graphWidget.snap_threshold = 10  # 吸附触发的像素距离阈值,可根据散点大小调整
    
    # 初始化十字光标
    graphWidget.vLine = pg.InfiniteLine(angle=90, movable=False, pen=pg.mkPen('#ffffff', width=1, style=Qt.DashLine))
    graphWidget.hLine = pg.InfiniteLine(angle=0, movable=False, pen=pg.mkPen('#ffffff', width=1, style=Qt.DashLine))
    graphWidget.addItem(graphWidget.vLine, ignoreBounds=True)
    graphWidget.addItem(graphWidget.hLine, ignoreBounds=True)

2. 修正鼠标移动吸附逻辑

def mouseMoved(self, evt):
    pos = evt
    if not self.plotWidget.sceneBoundingRect().contains(pos):
        return
    
    # 坐标转换
    mouse_view_pos = self.plotWidget.plotItem.vb.mapSceneToView(pos)
    mx, my = mouse_view_pos.x(), mouse_view_pos.y()
    points = self.plotWidget.plot_data
    if len(points) == 0:
        return
    
    # 转像素坐标计算屏幕实际距离,避免轴量程差异导致匹配错误
    mouse_pixel = self.plotWidget.plotItem.vb.mapViewToScene(mouse_view_pos)
    m_px, m_py = mouse_pixel.x(), mouse_pixel.y()
    min_dist = float('inf')
    target_idx = -1
    
    for idx, (px, py) in enumerate(points):
        p_pixel = self.plotWidget.plotItem.vb.mapViewToScene(pg.Point(px, py))
        dist = np.sqrt((m_px - p_pixel.x())**2 + (m_py - p_pixel.y())**2)
        if dist < min_dist:
            min_dist = dist
            target_idx = idx
    
    if min_dist <= self.plotWidget.snap_threshold:
        # 满足阈值,吸附到散点
        target_x, target_y = points[target_idx]
        self.vLine.setPos(target_x)
        self.hLine.setPos(target_y)
        self.plotWidget.hover_point = (target_x, target_y)
        self.cursorlabel.setHtml(
            "<span style='font-size: 12pt'>x={:0.1f}, <span style='color: red'>y={:0.1f}</span>".format(target_x, target_y)
        )
    else:
        # 不满足阈值,十字线跟随鼠标
        self.vLine.setPos(mx)
        self.hLine.setPos(my)
        self.plotWidget.hover_point = None
        self.cursorlabel.setHtml(
            "<span style='font-size: 12pt'>x={:0.1f}, <span style='color: red'>y={:0.1f}</span>".format(mx, my)
        )

3. 增加左键点击选点逻辑

def mouseClicked(self, evt):
    if evt.button() != Qt.LeftButton:
        return
    # 仅当处于吸附状态时保存点位
    if self.plotWidget.hover_point is not None:
        self.plotWidget.selected_point = self.plotWidget.hover_point
        sel_x, sel_y = self.plotWidget.selected_point
        # 此处插入点位选中后的业务逻辑
        print(f"已保存选中点:x={sel_x:.2f}, y={sel_y:.2f}")

4. 事件绑定

在绘图控件初始化完成后,绑定事件信号:

self.plotWidget.scene().sigMouseMoved.connect(self.mouseMoved)
self.plotWidget.scene().sigMouseClicked.connect(self.mouseClicked)

调优提示

  • 吸附阈值可根据散点大小调整,散点size为5时设置8-12像素体验最佳
  • 如果散点数据量超过10万,遍历计算距离会有卡顿,可以替换成ScatterPlotItem自带的pointsAt()方法做近邻查询提升效率
  • 可以给选中的散点单独设置高亮笔刷,给用户更明确的选中反馈

内容的提问来源于stack exchange,提问作者Arjun Warrier

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 12:24:59