如何在 pyqt5 中使 matplotlib 绘图具有交互性

How to make a matplotlib plot interactive in pyqt5

背景

我目前正在做一个项目,我想将 matplotlib 绘图嵌入到 pyqt5 GUI 中。该图是交互式的,允许绘制阴影矩形。

问题

问题是当它被嵌入到 pyqt 中时,绘图是不可交互的 window。当我 运行 程序时,下面代码中的第 147 行(mplWidget class 中的 plt.show() )显示了 matplotlib 图,然后我可以绘制一个矩形,如您在此处看到的:

然而,当这个 window 关闭并且绘图嵌入到 pyqt 中时 window,它变得不可编辑

我希望 GUI 图能够像 matplotlib 图一样运行。

建议的解决方案/问题

我知道这与我必须使用 connect() 语句提供 pyqt 功能这一事实有关,但我不知道这些语句的去向/它们如何适合该程序。
我不知道如何连接到 matplotlib。我是否只对 mplWidget class 函数使用 connect() 语句?
任何帮助表示赞赏!
(我意识到我需要把第147行去掉(plt.show())这样图框就不会在gui之前弹出了,不过我只是暂时用它来显示mpl class 仍然按预期运行,问题是它在嵌入时变成“静态”)

代码

import numpy as np
import matplotlib.pyplot as plt
from PyQt5 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import (FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar)

class topLevelWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        # Add a central widget to the main window and lay it out in a grid
        self.centralwidget = QtWidgets.QWidget(self)
        self.centralwidget.setObjectName("centralwidget")
        self.gridLayout_5 = QtWidgets.QGridLayout(self.centralwidget)
        self.gridLayout_5.setObjectName("gridLayout_5")

        # Create mainTabs object as well as the displayFrame and displaySettingsFrame that go in the central widget
        # Display Frame and Display settings frames
        self.displayFrame = QtWidgets.QFrame(self.centralwidget)
        self.verticalLayout_22 = QtWidgets.QVBoxLayout(self.displayFrame)
        self.verticalLayout_22.setObjectName("verticalLayout_22")

        self.gridLayout_5.addWidget(self.displayFrame, 1, 0, 1, 1)

        self.devConstructor = mplWidget()
        self.dynamic_canvas = FigureCanvasQTAgg(self.devConstructor.fig)
        self.verticalLayout_22.addWidget(self.dynamic_canvas)

        self._dynamic_ax = self.devConstructor.ax
        self.setCentralWidget(self.centralwidget)


        # Perform final windows setup (set buddies, translate, tab order, initial tabs, etc)
        _translate = QtCore.QCoreApplication.translate
        self.setWindowTitle(_translate("MainWindow", "MainWindow"))  # Was self.setWindowTitle

        QtCore.QMetaObject.connectSlotsByName(self)

        self.show()


class mplWidget(QtWidgets.QWidget): # n.b. changed this from Object to QWidget and added a super()

    def setSnapBase(self, base):
        return lambda value: int(base*round(float(value)/base))

    def onclick(self, event):
        if self.plotSnap is False:
            self.bottomLeftX = event.xdata
            self.bottomLeftY = event.ydata
        else:
            self.calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
            self.bottomLeftX = self.calculateSnapCoordinates(event.xdata)
            self.bottomLeftY = self.calculateSnapCoordinates(event.ydata)

        try:
            self.aspan.remove()
        except:
            pass

        self.moving = True

    def onrelease(self, event):
        if self.plotSnap is False:
            self.topRightX = event.xdata
            self.topRightY = event.ydata
        else:
            try:
                calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
                self.topRightX = calculateSnapCoordinates(event.xdata)
                self.topRightY = calculateSnapCoordinates(event.ydata)
            except:
                pass

        self.x = np.array([self.bottomLeftX, self.bottomLeftX, self.topRightX, self.topRightX, self.bottomLeftX])
        self.y = np.array([self.bottomLeftY, self.topRightY, self.topRightY, self.bottomLeftY, self.bottomLeftY])

        self.myPlot.set_xdata(self.x)
        self.myPlot.set_ydata(self.y)
        # ax.fill_between(x, y, color=defaultColors[0, :], alpha=.25)
        ylimDiff = self.ax.get_ylim()[1] - self.ax.get_ylim()[0]
        self.aspan = self.ax.axvspan(self.bottomLeftX, self.topRightX,
                                     (self.bottomLeftY-self.ax.get_ylim()[0])/ylimDiff,
                                    (self.topRightY-self.ax.get_ylim()[0])/ylimDiff,
                                     color=self.defaultColors[0, :], alpha=.25)

        self.moving = False

        self.fig.canvas.draw()

    def onmotion(self, event):
        if self.moving is False:
            return
        if event.inaxes is None:
            return
        if event.button != 1:
            return

        if self.plotSnap is False:
            self.topRightX = event.xdata
            self.topRightY = event.ydata
        else:
            self.calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
            self.topRightX = self.calculateSnapCoordinates(event.xdata)
            self.topRightY = self.calculateSnapCoordinates(event.ydata)

        self.x = np.array([self.bottomLeftX, self.bottomLeftX, self.topRightX, self.topRightX, self.bottomLeftX])
        self.y = np.array([self.bottomLeftY, self.topRightY, self.topRightY, self.bottomLeftY, self.bottomLeftY])

        self.myPlot.set_xdata(self.x)
        self.myPlot.set_ydata(self.y)

        self.fig.canvas.draw()

    def __init__(self):
        super(mplWidget, self).__init__()
        # Set default colors array
        self.defaultColors = np.array([[0, 0.4470, 0.7410], [0.8500, 0.3250, 0.0980], [0.9290, 0.6940, 0.1250],
                                  [0.4660, 0.6740, 0.1880], [0.6350, 0.0780, 0.1840], [0.4940, 0.1840, 0.5560],
                                  [0.3010, 0.7450, 0.9330]])

        # Create a figure with axes
        self.fig = plt.figure()
        self.ax = self.fig.gca()

        # Form the plot and shading
        self.bottomLeftX = 0; self.bottomLeftY = 0; self.topRightX = 0; self.topRightY = 0
        self.x = np.array([self.bottomLeftX, self.bottomLeftX, self.topRightX, self.topRightX, self.bottomLeftX])
        self.y = np.array([self.bottomLeftY, self.topRightY, self.topRightY, self.bottomLeftY, self.bottomLeftY])

        self.myPlot, = self.ax.plot(self.x, self.y, color=self.defaultColors[0, :])
        self.aspan = self.ax.axvspan(self.bottomLeftX, self.topRightX, color= self.defaultColors[0, :], alpha=0)

        # Set moving flag false (determines if mouse is being clicked and dragged inside plot). Set graph snap
        self.moving = False
        self.plotSnap = 5

        # Set up connectivity
        self.cid = self.fig.canvas.mpl_connect('button_press_event', self.onclick)
        self.cid = self.fig.canvas.mpl_connect('button_release_event', self.onrelease)
        self.cid = self.fig.canvas.mpl_connect('motion_notify_event', self.onmotion)


        # Set plot limits and show it
        plt.ylim((-100, 100))
        plt.xlim((-100, 100))
        plt.show()

if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)

    MainWindow = topLevelWindow()

    sys.exit(app.exec_())

在你的代码中有很多,所以我只列出它们:

  • 如果您打算使用 FigureCanvasQTAgg,那么您不应该再使用 pyplot。
  • "mplWidget" 是一个 class,其唯一任务是重绘 canvas,所以它必须是 QWidget 吗?
  • 如果您要比较布尔值,请不要使用“is”,例如 if self.plotSnap is False:,只需 if not self.plotSnap: 我也认为认为“plotSnap”为 False 是不合逻辑的,如果您想要禁用然后设置一个不可能的值,例如 0 或负数。

考虑到上面我已经让 MplWidget 继承自 FigureCanvasQTAgg,我已经取消了 pyplot 的使用:

import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import (
    FigureCanvasQTAgg,
    NavigationToolbar2QT as NavigationToolbar,
)
from matplotlib.figure import Figure


class MplWidget(FigureCanvasQTAgg):
    def __init__(self, parent=None):
        fig = Figure()
        super(MplWidget, self).__init__(fig)
        self.setParent(parent)
        # Set default colors array
        self.defaultColors = np.array(
            [
                [0, 0.4470, 0.7410],
                [0.8500, 0.3250, 0.0980],
                [0.9290, 0.6940, 0.1250],
                [0.4660, 0.6740, 0.1880],
                [0.6350, 0.0780, 0.1840],
                [0.4940, 0.1840, 0.5560],
                [0.3010, 0.7450, 0.9330],
            ]
        )

        # Create a figure with axes

        self.ax = self.figure.add_subplot(111)

        # Form the plot and shading
        self.bottomLeftX = 0
        self.bottomLeftY = 0
        self.topRightX = 0
        self.topRightY = 0
        self.x = np.array(
            [
                self.bottomLeftX,
                self.bottomLeftX,
                self.topRightX,
                self.topRightX,
                self.bottomLeftX,
            ]
        )
        self.y = np.array(
            [
                self.bottomLeftY,
                self.topRightY,
                self.topRightY,
                self.bottomLeftY,
                self.bottomLeftY,
            ]
        )

        (self.myPlot,) = self.ax.plot(self.x, self.y, color=self.defaultColors[0, :])
        self.aspan = self.ax.axvspan(
            self.bottomLeftX, self.topRightX, color=self.defaultColors[0, :], alpha=0
        )
        self.ax.set_xlim((-100, 100))
        self.ax.set_ylim((-100, 100))

        # Set moving flag false (determines if mouse is being clicked and dragged inside plot). Set graph snap
        self.moving = False
        self.plotSnap = 5

        # Set up connectivity
        self.cid1 = self.mpl_connect("button_press_event", self.onclick)
        self.cid2 = self.mpl_connect("button_release_event", self.onrelease)
        self.cid3 = self.mpl_connect("motion_notify_event", self.onmotion)

    def setSnapBase(self, base):
        return lambda value: int(base * round(float(value) / base))

    def onclick(self, event):
        if self.plotSnap <= 0:
            self.bottomLeftX = event.xdata
            self.bottomLeftY = event.ydata
        else:
            self.calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
            self.bottomLeftX = self.calculateSnapCoordinates(event.xdata)
            self.bottomLeftY = self.calculateSnapCoordinates(event.ydata)

        try:
            self.aspan.remove()
        except:
            pass

        self.moving = True

    def onrelease(self, event):
        if self.plotSnap <= 0:
            self.topRightX = event.xdata
            self.topRightY = event.ydata
        else:
            try:
                calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
                self.topRightX = calculateSnapCoordinates(event.xdata)
                self.topRightY = calculateSnapCoordinates(event.ydata)
            except:
                pass

        self.x = np.array(
            [
                self.bottomLeftX,
                self.bottomLeftX,
                self.topRightX,
                self.topRightX,
                self.bottomLeftX,
            ]
        )
        self.y = np.array(
            [
                self.bottomLeftY,
                self.topRightY,
                self.topRightY,
                self.bottomLeftY,
                self.bottomLeftY,
            ]
        )

        self.myPlot.set_xdata(self.x)
        self.myPlot.set_ydata(self.y)
        # ax.fill_between(x, y, color=defaultColors[0, :], alpha=.25)
        ylimDiff = self.ax.get_ylim()[1] - self.ax.get_ylim()[0]
        self.aspan = self.ax.axvspan(
            self.bottomLeftX,
            self.topRightX,
            (self.bottomLeftY - self.ax.get_ylim()[0]) / ylimDiff,
            (self.topRightY - self.ax.get_ylim()[0]) / ylimDiff,
            color=self.defaultColors[0, :],
            alpha=0.25,
        )

        self.moving = False
        self.draw()

    def onmotion(self, event):
        if not self.moving:
            return
        if event.inaxes is None:
            return
        if event.button != 1:
            return

        if self.plotSnap <= 0:
            self.topRightX = event.xdata
            self.topRightY = event.ydata
        else:
            self.calculateSnapCoordinates = self.setSnapBase(self.plotSnap)
            self.topRightX = self.calculateSnapCoordinates(event.xdata)
            self.topRightY = self.calculateSnapCoordinates(event.ydata)

        self.x = np.array(
            [
                self.bottomLeftX,
                self.bottomLeftX,
                self.topRightX,
                self.topRightX,
                self.bottomLeftX,
            ]
        )
        self.y = np.array(
            [
                self.bottomLeftY,
                self.topRightY,
                self.topRightY,
                self.bottomLeftY,
                self.bottomLeftY,
            ]
        )
        self.myPlot.set_xdata(self.x)
        self.myPlot.set_ydata(self.y)

        self.draw()


class TopLevelWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.canvas = MplWidget()
        self.setCentralWidget(self.canvas)


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)

    w = TopLevelWindow()
    w.show()

    sys.exit(app.exec_())