如何封装含PlotCurveItem与ScatterPlotItem的Pyqtgraph可悬停曲线类?
问题描述
我想要实现一个显示多条曲线的实时图表,并具备以下鼠标交互功能:
- 悬停在曲线上时,曲线高亮,且tooltip显示最近数据点的数值;
- tooltip对应的数值点同步高亮。
目前我已通过以下组件组合实现单条可悬停曲线的行为:
- 用PlotCurveItem显示全数据曲线;
- 用透明度为0的ScatterPlotItem检测悬停点并显示tooltip;
- 用第二个ScatterPlotItem显示高亮的悬停数据点。
作为PyQt和pyqtgraph新手,希望将该交互行为封装为可复用的类,请问该如何实现?需要继承哪个类?以下是我编写的示例代码:
import pyqtgraph as pg from pyqtgraph import QtCore, QtGui from PyQt5.QtWidgets import QApplication, QMainWindow import numpy as np class HoverableCurveItem(pg.PlotCurveItem): sigCurveHovered = QtCore.Signal(object, object) sigCurveNotHovered = QtCore.Signal(object, object) def __init__(self, hoverable=True, *args, **kwargs): super(HoverableCurveItem, self).__init__(*args, **kwargs) self.hoverable = hoverable self.setAcceptHoverEvents(True) def hoverEvent(self, ev): if self.hoverable: if self.mouseShape().contains(ev.pos()): self.sigCurveHovered.emit(self, ev) else: self.sigCurveNotHovered.emit(self, ev) class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) print('he') self.view = pg.GraphicsLayoutWidget() self.setCentralWidget(self.view) self.makeplot() def makeplot(self): x = list(range(10)) y = [np.random.randint(10) for _ in x] self.data = y plot = self.view.addPlot() self.plotitem = HoverableCurveItem(x, y, pen=pg.mkPen('w', width=10)) self.scatter = pg.ScatterPlotItem(pen=pg.mkPen('g', width=25)) self.scatter.setOpacity(0.0) self.scatter.setData(x, y, hoverable=True, tip=lambda x, y, data: f"{y}°C" ) self.plotitem.setClickable(True, width=10) self.plotitem.sigCurveHovered.connect(self.hovered) self.plotitem.sigCurveNotHovered.connect(self.leaveHovered) self.hovered = pg.ScatterPlotItem(pen=pg.mkPen('g', width=5)) self.hovered.setOpacity(0.) plot.addItem(self.plotitem) plot.addItem(self.scatter) plot.addItem(self.hovered) self.plot = plot def hovered(self, item, event): x = int(np.round(event.pos()[0])) y = self.data[int(np.round(x))] self.plotitem.setToolTip(f"{x}: {y}") self.plotitem.setPen(pg.mkPen('b', width=10)) self.hovered.setData([x], [y]) self.hovered.setOpacity(1) def leaveHovered(self): self.plotitem.setPen(pg.mkPen('w', width=10)) self.point.setOpacity(0) if __name__ == '__main__': import sys app = QApplication(sys.argv) w = MainWindow() w.show() sys.exit(app.exec())
解决方案
封装思路
要实现可复用的带悬停交互的曲线组件,推荐继承**pg.GraphicsObject**——它可以作为容器容纳多个子组件(曲线、悬停检测散点、高亮散点),统一管理交互逻辑,对外只暴露数据更新、样式配置等核心接口,避免代码分散。
封装后的可复用类
import pyqtgraph as pg from pyqtgraph import QtCore, QtGui import numpy as np from PyQt5.QtWidgets import QApplication, QMainWindow class InteractiveCurve(pg.GraphicsObject): def __init__(self, x, y, normal_pen, hover_pen, highlight_point_pen, *args, **kwargs): super().__init__(*args, **kwargs) # 存储原始数据 self.x_data = np.array(x) self.y_data = np.array(y) # 基础曲线 self.curve = pg.PlotCurveItem(x, y, pen=normal_pen) # 透明悬停检测散点(用于精准捕捉鼠标悬停) self.hover_scatter = pg.ScatterPlotItem(x, y, pen=pg.mkPen(None), brush=pg.mkBrush(None), size=10) self.hover_scatter.setOpacity(0) # 高亮数据点 self.highlight_point = pg.ScatterPlotItem(pen=highlight_point_pen, size=8) self.highlight_point.setOpacity(0) # 保存原始画笔样式 self.normal_pen = normal_pen self.hover_pen = hover_pen # 连接悬停信号 self.hover_scatter.sigHovered.connect(self.on_hover) self.hover_scatter.sigHoverFinished.connect(self.on_hover_finish) # 把所有子元素添加到当前容器 self.addItem(self.curve) self.addItem(self.hover_scatter) self.addItem(self.highlight_point) def boundingRect(self): # 返回包含所有子元素的边界矩形,确保交互区域正确 return self.curve.boundingRect() def on_hover(self, points): # 悬停时的逻辑:高亮曲线、显示tooltip和高亮点 if points: point = points[0] idx = point.index() x_val = self.x_data[idx] y_val = self.y_data[idx] # 切换曲线样式为高亮状态 self.curve.setPen(self.hover_pen) # 更新高亮点位置并显示 self.highlight_point.setData([x_val], [y_val]) self.highlight_point.setOpacity(1) # 设置tooltip内容 self.setToolTip(f"坐标: ({x_val:.2f}, {y_val:.2f})") def on_hover_finish(self): # 离开悬停时的逻辑:恢复曲线样式、隐藏高亮点 self.curve.setPen(self.normal_pen) self.highlight_point.setOpacity(0) self.setToolTip("") def update_data(self, x, y): # 对外暴露的实时更新数据接口 self.x_data = np.array(x) self.y_data = np.array(y) self.curve.setData(x, y) self.hover_scatter.setData(x, y) # 测试多条曲线的示例窗口 class MainWindow(QMainWindow): def __init__(self): super().__init__() self.view = pg.GraphicsLayoutWidget() self.setCentralWidget(self.view) self.init_plots() def init_plots(self): plot = self.view.addPlot(title="多条可交互曲线") # 生成测试数据 x = np.arange(0, 10, 0.1) y1 = np.sin(x) y2 = np.cos(x) y3 = np.sin(x) + np.cos(x) # 创建三条独立的可交互曲线 curve1 = InteractiveCurve( x, y1, normal_pen=pg.mkPen('white', width=2), hover_pen=pg.mkPen('blue', width=3), highlight_point_pen=pg.mkPen('red', width=2) ) curve2 = InteractiveCurve( x, y2, normal_pen=pg.mkPen('green', width=2), hover_pen=pg.mkPen('yellow', width=3), highlight_point_pen=pg.mkPen('orange', width=2) ) curve3 = InteractiveCurve( x, y3, normal_pen=pg.mkPen('purple', width=2), hover_pen=pg.mkPen('cyan', width=3), highlight_point_pen=pg.mkPen('magenta', width=2) ) # 添加到图表中 plot.addItem(curve1) plot.addItem(curve2) plot.addItem(curve3) if __name__ == '__main__': import sys app = QApplication(sys.argv) w = MainWindow() w.show() sys.exit(app.exec())
关键说明
- 继承选择:
pg.GraphicsObject作为容器类,能将曲线、悬停检测散点、高亮散点整合为一个独立组件,交互逻辑完全内部处理,外部调用更简洁。 - 交互优化:用透明散点检测悬停比直接检测曲线更灵敏,避免鼠标靠近曲线但未精确命中的问题。
- 复用性提升:每条曲线的样式、数据完全独立,创建多条曲线只需重复实例化
InteractiveCurve类即可。 - 实时支持:提供
update_data方法,只需传入新的x、y数据就能更新曲线,适配实时数据场景。
对原代码的修复
- 修复了原代码
leaveHovered方法中未定义self.point的错误。 - 移除了对主窗口全局变量的依赖,所有状态都封装在组件内部。
内容的提问来源于stack exchange,提问作者Nick Skywalker
相关产品推荐
相关产品推荐

