2016-12-15 85 views
1

我真的很難理解如何在PyQt中使用線程。我做了一個簡單的例子,說明我想在我的用戶界面中做什麼。在您可以在下面看到的代碼中,我希望用戶輸入股票行情(例如,您可以輸入「bby」,「goog」或「v」),並在特定時間段內繪製股票的價值。事情是在更復雜的用戶界面或很長一段時間的界面凍結,而情節正在更新。所以我做了一個「繪圖儀」課,當它收到一定的信號時更新繪圖(重寫Qthread.run顯然不是正確的方式you're doing it wrong)。我想讓這個「繪圖儀」在另一個線程中運行,而不是主線程。如何使用Qthread使用PyQt更新Matplotlib圖形?

只要取消註釋線程程序,程序就停止工作。我試圖移動新線程的啓動以及「連接」,但沒有任何工作。即使在閱讀了documentation並查看了Qt網站上的示例之後,我認爲我對Qthread的工作原理並不瞭解。

如果有任何你知道如何做到這一點,將會有很大的幫助! (我使用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_()) 

這裏是mentionned第二個選項,即解決方案建立一個單一的工人和一個線程,並使用那些在整個程序的運行時間。

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

謝謝!你的代碼確實有用,並且確實希望我想要,但似乎線程永遠不會結束。我在if語句中添加了一個print(True),並且每次輸入代碼時都會進入該循環(首次免除)。另外,如果您快速輸入2個代號,該圖將永久停止更新。 – BillyBoom

+0

另外,文檔中不建議使用terminate。也許最好的解決方案是做你提出的第二種選擇,但我不知道如何實現它。 – BillyBoom

+0

更新了第二個選項的解決方案。這個新的解決方案也是線程安全的。 – ImportanceOfBeingErnest