¿Cómo usar un Qthread para actualizar una figura de Matplotlib con PyQt?

Realmente me está costando entender cómo usar Threads en PyQt. Hice un ejemplo simple de lo que me gustaría hacer en mi interfaz de usuario. En el código que puede ver a continuación, quiero que el usuario ingrese un indicador de acciones (por ejemplo, puede ingresar “bby”, “goog” o “v”) y trazar el valor de las acciones durante un período determinado. La cosa está en una Ui más compleja o durante un largo período de tiempo la UI se congela mientras la ttwig se está actualizando. Así que hice una clase de “trazador” que actualiza la ttwig cuando recibe una cierta señal (al anular Qthread.run aparentemente no fue la forma correcta en que lo estás haciendo mal ). Me gustaría hacer que este “Plotter” se ejecute en otro hilo que no sea el principal.

Tan pronto como descomente las líneas de subproceso, el progtwig deja de funcionar. He intentado mover el lanzamiento del nuevo hilo y también el “conectar” pero nada está funcionando. Creo que no entiendo bien cómo funciona Qthread incluso después de leer la documentación y ver los ejemplos en el sitio web de Qt.

Si alguna vez sabes cómo hacerlo, ¡te ayudaría mucho! (Estoy trabajando con Python 3.5 y PyQt5)

from PyQt5.QtCore import * from PyQt5.QtWidgets import * from matplotlib.axes._subplots import Axes from matplotlib.figure import Figure from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas import sys from datetime import datetime, timedelta import time import quandl 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().__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) self.plotter = Plotter() self.send_fig.connect(self.plotter.replot) self.plotter.return_fig.connect(self.myplot.update_plot) def updatePlot(self): ticker = self.editor.text() self.editor.clear() self.display.setText(ticker) # thread = QThread() # self.plotter.moveToThread(thread) self.send_fig.emit(self.myplot.axes, ticker) # thread.start() 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 data = quandl.get("YAHOO/"+ticker+".6", start_date=d.strftime("%d-%m-%Y"), end_date=time.strftime("%d-%m-%Y")) axes.plot(data) self.return_fig.emit(axes) if __name__ == '__main__': app = QApplication(sys.argv) win = MainWindow() sys.exit(app.exec_()) 

El primer problema es que pierdes la referencia al thread una vez que se inicia. Para mantener una referencia use una variable de clase, es decir, self.thread lugar de thread .

A continuación, el hilo debe iniciarse antes de hacer nada. Así que necesitas poner self.thread.start() frente a la emisión de la señal.

Ahora, ya funcionaría, pero el siguiente problema ocurre una vez que desea iniciar un nuevo hilo. Entonces, primero debes matar al viejo. Dado que el viejo Plotter quedaría sin hogar, una solución es crear un nuevo Plotter así como un nuevo hilo cada vez que desee trazar. Esta es la forma en que funciona la solución a continuación.
Alternativamente, también puedes usar siempre el mismo trazador e hilo. Lo único que hay que recordar es que siempre hay exactamente un trabajador (trazador) y un subproceso, si elimina uno de ellos, el otro está triste.

Para probarlo, necesitaba cambiar algunas cosas pequeñas, como usar PyQt4 en lugar de 5 y reemplazar la generación de datos. Aquí está el código de trabajo.

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

Aquí hay una solución para la segunda opción mencionada, es decir, crear un solo trabajador y un subproceso y usarlos durante todo el tiempo de ejecución del progtwig.

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