2016-12-15 4 views
1

Мне действительно трудно понять, как использовать Threads в PyQt. Я сделал простой пример того, что я хотел бы сделать в своем интерфейсе. В коде, который вы видите ниже, я хочу, чтобы пользователь вводил тикер акций (например, вы можете ввести «bby», «goog» или «v») и составить график стоимости акций за определенный период. Дело в более сложном Ui или в течение длительного периода времени пользовательский интерфейс замирает, пока сюжет обновляется. Поэтому я создал класс «Plotter», который обновляет график, когда он получает определенный сигнал (переопределение Qthread.run, по-видимому, было неправильным способом you're doing it wrong). Я бы хотел, чтобы этот «Плоттер» работал в другом потоке, кроме основного.Как использовать Qthread для обновления фигуры Matplotlib с помощью PyQt?

Как только я раскомментирую нитки, программа перестает работать. Я попытался переместить запуск нового потока, а также «подключиться», но ничего не работает. Я думаю, что не понимаю, как работает Qthread даже после прочтения documentation и просмотра примеров на веб-сайте Qt.

Если вы знаете, как это сделать, это поможет! (Я работаю с Python 3.5 и 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

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

ответ

0

Первая проблема заключается в том, что вы теряете ссылку на thread после ее начала. Для сохранения ссылки используйте переменную класса, то есть self.thread вместо thread.

Далее, нить должна быть запущена, прежде чем что-либо делать. Поэтому вам нужно поставить self.thread.start() перед сигнальной эмиссией.

Теперь это уже сработает, но следующая проблема возникает, если вы хотите начать новый поток. Итак, вам нужно сначала убить старое. Так как старый Plotter был бы бездомным, решение должно создать новый Плоттер, а также новый поток каждый раз, когда вы захотите построить. Так работает нижеследующее решение.
В качестве альтернативы вы также можете использовать один и тот же плоттер и нить. Единственное, что нужно помнить, это то, что всегда есть ровно один рабочий (плоттер) и один поток, если вы удаляете один из них, другой - грустный.

Чтобы проверить его, мне нужно было изменить некоторые мелочи, например, использовать PyQt4 вместо 5 и заменить генерацию данных. Вот рабочий код.

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

Вот решение для второго варианта, т.е. упоминалось создание единого рабочего и нить и использовать их на протяжении выполнения программы.

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

Спасибо! Ваш код действительно работает и хочет, я хочу, но кажется, что нити никогда не заканчиваются. Я добавил print (True) в оператор if, и программа переходит в этот цикл каждый раз, когда вы вводите тикер (кроме первого раза). Кроме того, если вы быстро введете 2 тикера, сюжет перестанет обновляться навсегда. – BillyBoom

+0

Кроме того, использование терминалов не рекомендуется в документации. Возможно, хорошим решением является вторая предложенная вами альтернатива, но я не уверен, как ее реализовать. – BillyBoom

+0

Обновлен решением для второго варианта. Это новое решение также является потокобезопасным. – ImportanceOfBeingErnest

Смежные вопросы