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

如何封装含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())

关键说明

  1. 继承选择:pg.GraphicsObject作为容器类,能将曲线、悬停检测散点、高亮散点整合为一个独立组件,交互逻辑完全内部处理,外部调用更简洁。
  2. 交互优化:用透明散点检测悬停比直接检测曲线更灵敏,避免鼠标靠近曲线但未精确命中的问题。
  3. 复用性提升:每条曲线的样式、数据完全独立,创建多条曲线只需重复实例化InteractiveCurve类即可。
  4. 实时支持:提供update_data方法,只需传入新的x、y数据就能更新曲线,适配实时数据场景。

对原代码的修复

  • 修复了原代码leaveHovered方法中未定义self.point的错误。
  • 移除了对主窗口全局变量的依赖,所有状态都封装在组件内部。

内容的提问来源于stack exchange,提问作者Nick Skywalker

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 23:05:14