2016-12-15 5 views
1

J'ai vraiment du mal à comprendre comment utiliser Threads dans PyQt. J'ai fait un exemple simple de ce que je voudrais faire dans mon interface utilisateur. Dans le code que vous pouvez voir ci-dessous, je veux que l'utilisateur entre un ticker boursier (vous pouvez entrer "bby", "goog" ou "v" par exemple) et tracer la valeur du stock sur une certaine période. La chose est dans un Ui plus complexe ou pendant une longue période de temps, l'interface utilisateur gèle pendant que l'intrigue est mise à jour. J'ai donc fait une classe "Plotter" qui met à jour le tracé quand il reçoit un certain signal (surcharger Qthread.run n'était apparemment pas le bon chemin you're doing it wrong). Je voudrais faire ce "Plotter" courir dans un autre fil que le principal.Comment utiliser un Qthread pour mettre à jour une figure Matplotlib avec PyQt?

Dès que je décommente les lignes de fil, le programme cesse de fonctionner. J'ai essayé de déplacer le lancement du nouveau thread et aussi le "connecter" mais rien ne fonctionne. Je pense que je ne comprends pas bien comment Qthread fonctionne même après avoir lu le documentation et en regardant les exemples sur le site Web Qt.

Si vous en savez quelque chose, cela aiderait beaucoup! (Je travaille avec Python 3.5 et 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_()) 
+0

Votre code n'est pas sécurisé aux discussions. Vous ne pouvez pas effectuer d'appels matplotlib (ou Qt GUI) à partir d'un thread secondaire. Vous pouvez récupérer les données dans un thread, mais vous devrez le renvoyer au thread principal pour le traçage en émettant un signal personnalisé (donc renvoyez les données pour le traçage plutôt que l'objet des axes que vous renvoyez maintenant) –

Répondre

0

Le premier problème est que vous perdez la référence à thread une fois qu'il a commencé. Pour conserver une référence, utilisez une variable de classe, c'est-à-dire self.thread au lieu de thread.

Ensuite, le thread doit être démarré avant de faire quoi que ce soit. Donc, vous devez mettre self.thread.start() devant l'émission du signal. Maintenant, cela fonctionnerait déjà, mais un problème suivant se produit lorsque vous voulez démarrer un nouveau thread. Donc, vous devez d'abord tuer l'ancien. Puisque l'ancien Plotter serait alors sans-abri, une solution consiste à créer un nouveau Plotter ainsi qu'un nouveau thread chaque fois que vous voulez tracer. C'est ainsi que fonctionne la solution ci-dessous.
Vous pouvez également toujours utiliser le même traceur et le même thread. La seule chose à retenir est qu'il y a toujours exactement un ouvrier (traceur) et un thread, si vous en supprimez un, l'autre est triste. Pour le tester, j'avais besoin de changer quelques petites choses, comme utiliser PyQt4 au lieu de 5 et remplacer la génération de données. Voici le code de travail.

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

Voici une solution pour la deuxième option mentionné, à savoir créer un seul travailleur et un fil et utiliser ceux tout au long de l'exécution du programme.

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

Merci! Votre code fonctionne et veut ce que je veux, mais il semble que les threads ne finissent jamais. J'ai ajouté une impression (True) dans l'instruction if et le programme va dans cette boucle chaque fois que vous entrez un ticker (sauf la première fois). En outre, si vous entrez rapidement 2 tickers, l'intrigue cessera de se mettre à jour pour toujours. – BillyBoom

+0

En outre, l'utilisation de terminate n'est pas recommandée dans la documentation. Peut-être que la bonne solution est de faire la deuxième alternative que vous avez proposée, mais je ne suis pas sûr de la façon de l'implémenter. – BillyBoom

+0

Mis à jour avec une solution pour la deuxième option. Cette nouvelle solution est également compatible avec les threads. – ImportanceOfBeingErnest