PyQt5 中的 Matplotlib 十字准线光标

Posted

技术标签:

【中文标题】PyQt5 中的 Matplotlib 十字准线光标【英文标题】:Matplotlib cross hair cursor in PyQt5 【发布时间】:2021-09-06 21:32:21 【问题描述】:

我想添加一个捕捉数据点并在鼠标移动时更新的十字准线。我发现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,但是问题没有回答清楚。

【问题讨论】:

【参考方案1】:

有2个问题:

snap_cursor 是一个局部变量,当__init__ 执行完毕时将被删除。你必须让他成为班级成员。

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

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()

【讨论】:

以上是关于PyQt5 中的 Matplotlib 十字准线光标的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Unity3D 中制作平滑的十字准线?

Eclipse 光标变为十字准线

Highcharts 十字准线标签是传奇的背后

Unity中的抓钩射线投射方向问题

如何在pyqtgraph中绘制十字准线并绘制鼠标位置?

如何在剑道图表十字准线工具提示中提供 X 轴和 Y 轴值?