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
相关产品推荐
相关产品推荐

