Как сделать график matplotlib интерактивным в pyqt5 - PullRequest
1 голос
/ 14 июля 2020

Фон

В настоящее время я работаю над проектом, в котором я хочу встроить график matplotlib в pyqt5 GUI. Сюжет является интерактивным и позволяет рисовать закрашенные прямоугольники.

Проблема

Проблема в том, что график не интерактивен, когда он встроен в окно pyqt. Когда я запускаю программу, строка 147 в приведенном ниже коде (plt.show () в классе mplWidget) отображает график matplotlib, и затем я могу нарисовать прямоугольник, как вы можете видеть здесь:

matplotlib plot

However, when this window closes and the plot is embedded in the pyqt window, it becomes uneditable

pyqt uneditable plot

I want the GUI plot to function as the matplotlib figure does.

Proposed solution / Question

I know that this has to do with the fact that I have to provide the pyqt functionality with connect() statements, but I don't know where those go / how they would fit in to this program.
I don't know how to connect to a matplotlib. Do I just use connect() statements to the mplWidget class functions?
Any help is appreciated!
(I realize that I will need to take line 147 out (plt.show()) so that the figure frame does not pop up before the gui, but I just had it temporarily to show that the mpl class still functions as intended, and the problem is that it becomes "static" upon embedding)

Code

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

1 Ответ

2 голосов
/ 14 июля 2020

В вашем коде их много, поэтому я только перечислю их:

  • Если вы собираетесь использовать 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_())
...