How to use a Qthread to update a Matplotlib figure with PyQt?

前端 未结 1 1661
自闭症患者
自闭症患者 2020-12-19 18:45

I\'m really having a hard time to understand how to use Threads in PyQt. I made a simple example of what I would like to do in my UI. In the code you can see below, I want t

相关标签:
1条回答
  • 2020-12-19 19:24

    The first problem is that you lose the reference to thread once it's started. To keep a reference use a class variable, i.e. self.thread instead of thread.

    Next, the thread has to be started before doing anything. So you need to put self.thread.start() in front of the signal emission.

    Now, it would work already, but a next problem occurs once you want to start a new thread. So, you need to first kill the old one. Since the old Plotter would then be homeless, a solution is to create a new Plotter as well as a new thread each time you want to plot. This is the way the solution below works.
    Alternatively, you could also always use the same plotter and thread. The only thing to remember is that there is always exactly one worker (plotter) and one thread, if you delete one of them, the other is sad.

    In order to test it, I needed to change some small things, like using PyQt4 instead of 5 and replace the data generation. Here is the working code.

    from PyQt4.QtCore import *
    from PyQt4.QtGui import *
    from matplotlib.axes._subplots import Axes
    from matplotlib.figure import Figure
    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
    import sys
    from datetime import datetime, timedelta
    import numpy as np
    
    
    
    class MyMplCanvas(FigureCanvas):
        """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.)."""
        send_fig = pyqtSignal(Axes, str, name="send_fig")
    
        def __init__(self, parent=None):
            self.fig = Figure()
            self.axes = self.fig.add_subplot(111)
    
            # We want the axes cleared every time plot() is called
            self.axes.hold(False)
    
            FigureCanvas.__init__(self, self.fig)
            self.setParent(parent)
    
            FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding)
            FigureCanvas.updateGeometry(self)
    
        def update_plot(self, axes):
            self.axes = axes
            self.draw()
    
    class MainWindow(QMainWindow):
        send_fig = pyqtSignal(Axes, str, name="send_fig")
    
        def __init__(self):
            super(MainWindow, self).__init__()
    
            self.main_widget = QWidget(self)
            self.myplot = MyMplCanvas(self.main_widget)
            self.editor = QLineEdit()
            self.display = QLabel("Vide")
    
            self.layout = QGridLayout(self.main_widget)
            self.layout.addWidget(self.editor)
            self.layout.addWidget(self.display)
            self.layout.addWidget(self.myplot)
    
            self.main_widget.setFocus()
            self.setCentralWidget(self.main_widget)
    
            self.move(500, 500)
            self.show()
    
            self.editor.returnPressed.connect(self.updatePlot)
    
            # plotter and thread are none at the beginning
            self.plotter = None 
            self.thread = None
    
    
    
        def updatePlot(self):
            ticker = self.editor.text()
            self.editor.clear()
            self.display.setText(ticker)
    
            # if there is already a thread running, kill it first
            if self.thread != None and self.thread.isRunning():
                self.thread.terminate()
    
            # initialize plotter and thread
            # since each plotter needs its own thread
            self.plotter = Plotter()
            self.thread = QThread()
            # connect signals
            self.send_fig.connect(self.plotter.replot)
            self.plotter.return_fig.connect(self.myplot.update_plot)
            #move to thread and start
            self.plotter.moveToThread(self.thread)
            self.thread.start()
            # start the plotting
            self.send_fig.emit(self.myplot.axes, ticker)
    
    
    
    class Plotter(QObject):
        return_fig = pyqtSignal(Axes)
    
        @pyqtSlot(Axes, str)
        def replot(self, axes, ticker):  # A slot takes no params
            print(ticker)
            d = datetime.today() - timedelta(weeks=52)  # data from 1week ago
            # do some random task
            data = np.random.rand(10000,10000)
            axes.plot(data.mean(axis=1))
            self.return_fig.emit(axes)
    
    
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        win = MainWindow()
        sys.exit(app.exec_())
    

    Here is a solution for the second option mentionned, i.e. create a single worker and a thread and use those throughout the program's runtime.

    from PyQt4.QtCore import *
    from PyQt4.QtGui import *
    from matplotlib.figure import Figure
    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
    import sys
    import numpy as np
    
    
    
    class MyMplCanvas(FigureCanvas):
    
        def __init__(self, parent=None):
            self.fig = Figure()
            self.axes = self.fig.add_subplot(111)
            # plot empty line 
            self.line, = self.axes.plot([],[], color="orange")
    
            FigureCanvas.__init__(self, self.fig)
            self.setParent(parent)
    
            FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding)
            FigureCanvas.updateGeometry(self)
    
    
    class MainWindow(QMainWindow):
        send_fig = pyqtSignal(str)
    
        def __init__(self):
            super(MainWindow, self).__init__()
    
            self.main_widget = QWidget(self)
            self.myplot = MyMplCanvas(self.main_widget)
            self.editor = QLineEdit()
            self.display = QLabel("Vide")
    
            self.layout = QGridLayout(self.main_widget)
            self.layout.addWidget(self.editor)
            self.layout.addWidget(self.display)
            self.layout.addWidget(self.myplot)
    
            self.main_widget.setFocus()
            self.setCentralWidget(self.main_widget)
            self.show()
    
            # plotter and thread are none at the beginning
            self.plotter = Plotter()
            self.thread = QThread()
    
            # connect signals
            self.editor.returnPressed.connect(self.start_update)
            self.send_fig.connect(self.plotter.replot)
            self.plotter.return_fig.connect(self.plot)
            #move to thread and start
            self.plotter.moveToThread(self.thread)
            self.thread.start()
    
        def start_update(self):
            ticker = self.editor.text()
            self.editor.clear()
            self.display.setText(ticker)
            # start the plotting
            self.send_fig.emit(ticker)
    
    
        # Slot receives data and plots it
        def plot(self, data):
            # plot data
            self.myplot.line.set_data([np.arange(len(data)), data])
            # adjust axes
            self.myplot.axes.set_xlim([0,len(data) ])
            self.myplot.axes.set_ylim([ data.min(),data.max() ])
            self.myplot.draw()
    
    
    class Plotter(QObject):
        return_fig = pyqtSignal(object)
    
        @pyqtSlot(str)
        def replot(self, ticker):
            print(ticker)
            # do some random task
            data = np.random.rand(10000,10000)
            data = data.mean(axis=1)
            self.return_fig.emit(data)
    
    
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        win = MainWindow()
        sys.exit(app.exec_())
    
    0 讨论(0)
提交回复
热议问题