PyQt5 中的 Matplotlib 十字线光标

Matplotlib cross hair cursor in PyQt5

我想添加一个十字准线来捕捉数据点并在鼠标移动时更新。我发现 this example 效果很好:

import numpy as np
import matplotlib.pyplot as plt

class SnappingCursor:
    """
    A cross hair cursor that snaps to the data point of a line, which is
    closest to the *x* position of the cursor.

    For simplicity, this assumes that *x* values of the data are sorted.
    """
    def __init__(self, ax, line):
        self.ax = ax
        self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
        self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
        self.x, self.y = line.get_data()
        self._last_index = None
        # text location in axes coords
        self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)

    def set_cross_hair_visible(self, visible):
        need_redraw = self.vertical_line.get_visible() != visible
        self.vertical_line.set_visible(visible)
        self.horizontal_line.set_visible(visible)
        self.text.set_visible(visible)
        return need_redraw

    def on_mouse_move(self, event):
        if not event.inaxes:
            self._last_index = None
            need_redraw = self.set_cross_hair_visible(False)
            if need_redraw:
                self.ax.figure.canvas.draw()
        else:
            self.set_cross_hair_visible(True)
            x, y = event.xdata, event.ydata
            index = min(np.searchsorted(self.y, y), len(self.y) - 1)
            if index == self._last_index:
                return  # still on the same data point. Nothing to do.
            self._last_index = index
            x = self.x[index]
            y = self.y[index]
            # update the line positions
            self.horizontal_line.set_ydata(y)
            self.vertical_line.set_xdata(x)
            self.text.set_text('x=%1.2f, y=%1.2f' % (x, y))
            self.ax.figure.canvas.draw()


y = np.arange(0, 1, 0.01)
x = np.sin(2 * 2 * np.pi * y)

fig, ax = plt.subplots()
ax.set_title('Snapping cursor')
line, = ax.plot(x, y, 'o')
snap_cursor = SnappingCursor(ax, line)
fig.canvas.mpl_connect('motion_notify_event', snap_cursor.on_mouse_move)
plt.show()

但是当我想用 PyQt5 调整代码并在 GUI 中显示绘图时,我遇到了麻烦。我的代码是:

from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout
import sys
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import numpy as np

class SnappingCursor:
    """
    A cross hair cursor that snaps to the data point of a line, which is
    closest to the *x* position of the cursor.

    For simplicity, this assumes that *x* values of the data are sorted.
    """
    def __init__(self, ax, line):
        self.ax = ax
        self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
        self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
        self.x, self.y = line.get_data()
        self._last_index = None
        # text location in axes coords
        self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)

    def set_cross_hair_visible(self, visible):
        need_redraw = self.vertical_line.get_visible() != visible
        self.vertical_line.set_visible(visible)
        self.horizontal_line.set_visible(visible)
        self.text.set_visible(visible)
        return need_redraw

    def on_mouse_move(self, event):
        if not event.inaxes:
            self._last_index = None
            need_redraw = self.set_cross_hair_visible(False)
            if need_redraw:
                self.ax.figure.canvas.draw()
        else:
            self.set_cross_hair_visible(True)
            x, y = event.xdata, event.ydata
            index = min(np.searchsorted(self.y, y), len(self.y) - 1)
            if index == self._last_index:
                return  # still on the same data point. Nothing to do.
            self._last_index = index
            x = self.x[index]
            y = self.y[index]
            # update the line positions
            self.horizontal_line.set_ydata(y)
            self.vertical_line.set_xdata(x)
            self.text.set_text('x=%1.2f, y=%1.2f' % (x, y))
            self.ax.figure.canvas.draw()

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        
        widget=QWidget()
        vbox=QVBoxLayout()
        
        
        plot1 = FigureCanvas(Figure(tight_layout=True, linewidth=3))
        ax = plot1.figure.subplots()
        x = np.arange(0, 1, 0.01)
        y = np.sin(2 * 2 * np.pi * x)
        line, = ax.plot(x, y, 'o')

        snap_cursor = SnappingCursor(ax, line)
        plot1.mpl_connect('motion_notify_event', snap_cursor.on_mouse_move)

        vbox.addWidget(plot1)
        widget.setLayout(vbox)

        self.setCentralWidget(widget)
        self.setWindowTitle('Example')
        self.show()

App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())

通过运行以上代码,数据绘制正确,但十字线仅显示在其初始位置,不会因鼠标移动而移动。数据值也不显示。

我也找到了类似的问题here,但是问题没有回答清楚

有2个问题:

  • snap_cursor是一个局部变量,当__init__执行完后会被移除。您必须让他成为 class.

    的成员
  • 本教程的初始代码设计为显示信息的点是穿过光标并与曲线相交的水平线。在您的初始代码中,它与示例不同,也不适用于您的新曲线,因此我恢复了教程的逻辑。

import sys

from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget

import numpy as np

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure


class SnappingCursor:
    """
    A cross hair cursor that snaps to the data point of a line, which is
    closest to the *x* position of the cursor.

    For simplicity, this assumes that *x* values of the data are sorted.
    """

    def __init__(self, ax, line):
        self.ax = ax
        self.horizontal_line = ax.axhline(color="k", lw=0.8, ls="--")
        self.vertical_line = ax.axvline(color="k", lw=0.8, ls="--")
        self.x, self.y = line.get_data()
        self._last_index = None
        # text location in axes coords
        self.text = ax.text(0.72, 0.9, "", transform=ax.transAxes)

    def set_cross_hair_visible(self, visible):
        need_redraw = self.vertical_line.get_visible() != visible
        self.vertical_line.set_visible(visible)
        self.horizontal_line.set_visible(visible)
        self.text.set_visible(visible)
        return need_redraw

    def on_mouse_move(self, event):
        if not event.inaxes:
            self._last_index = None
            need_redraw = self.set_cross_hair_visible(False)
            if need_redraw:
                self.ax.figure.canvas.draw()
        else:
            self.set_cross_hair_visible(True)
            x, y = event.xdata, event.ydata
            index = min(np.searchsorted(self.x, x), len(self.x) - 1)
            if index == self._last_index:
                return  # still on the same data point. Nothing to do.
            self._last_index = index
            x = self.x[index]
            y = self.y[index]
            # update the line positions
            self.horizontal_line.set_ydata(y)
            self.vertical_line.set_xdata(x)
            self.text.set_text("x=%1.2f, y=%1.2f" % (x, y))
            self.ax.figure.canvas.draw()


class Window(QMainWindow):
    def __init__(self):
        super().__init__()

        widget = QWidget()
        vbox = QVBoxLayout(widget)

        x = np.arange(0, 1, 0.01)
        y = np.sin(2 * 2 * np.pi * x)

        canvas = FigureCanvas(Figure(tight_layout=True, linewidth=3))
        ax = canvas.figure.subplots()
        ax.set_title("Snapping cursor")
        (line,) = ax.plot(x, y, "o")
        self.snap_cursor = SnappingCursor(ax, line)
        canvas.mpl_connect("motion_notify_event", self.snap_cursor.on_mouse_move)

        vbox.addWidget(canvas)
        self.setCentralWidget(widget)
        self.setWindowTitle("Example")


app = QApplication(sys.argv)
w = Window()
w.show()
app.exec()