【问题标题】:How to make a matplotlib plot interactive in pyqt5如何在 pyqt5 中使 matplotlib 绘图交互
【发布时间】:2020-11-04 01:53:26
【问题描述】:

背景

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

问题

问题是当它嵌入到pyqt窗口中时,它不是交互式的。当我运行程序时,下面代码中的第 147 行(mplWidget 类中的 plt.show())显示了 matplotlib 图,然后我可以绘制一个矩形,如下所示:

但是,当这个窗口关闭并且绘图嵌入到 pyqt 窗口中时,它变得不可编辑

我希望 GUI 绘图能够像 matplotlib 图形那样发挥作用。

建议的解决方案/问题

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

代码

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

【问题讨论】:

    标签: python matplotlib user-interface pyqt5 embed


    【解决方案1】:

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

    • 如果你打算使用 FigureCanvasQTAgg 那么你不应该再使用 pyplot。
    • “mplWidget”是一个类,它的唯一任务是重绘画布,所以它必须是一个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_())
    

    【讨论】:

    • 使用 FigureCanvasQTAgg 的替代方法是什么,或者为什么它与 pyplot 不兼容?我把它做成了一个小部件,因为这将在一个更大的 GUI 的一个框架中,我认为把它做成一个 QWidget 是把它插入到一个框架中的最好方法。好的,我将更改 plotSnap 部分,这很快就完成了,你说我不应该混合布尔值和浮点数
    • @WhoDatBoy pyplot 是一种创建绘图的简单方法,但由于这个原因,它已经创建了一个与画布相关联的图形,该图形具有自己的事件循环,可能与 Qt 事件循环发生冲突。如果要使用 FigureCanvasQTAgg,那么最好让画布处理自己的逻辑。另一方面,每个类都应该有自己的职责,所以 MplWidget 更自然地继承自 FigureCanvasQTAgg。我建议您查看官方示例:matplotlib.org/3.2.1/gallery/user_interfaces/…
    • 哦,我明白了。我查看了文档,但我没有意识到 pyplot 有自己的事件循环,感谢您的建议!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-01-31
    • 1970-01-01
    • 2019-08-14
    • 2012-05-29
    • 1970-01-01
    • 2021-04-08
    • 1970-01-01
    相关资源
    最近更新 更多