2016-12-15 87 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