Python 如何使用Qthread使用PyQt更新Matplotlib图形?

Python 如何使用Qthread使用PyQt更新Matplotlib图形?,python,matplotlib,pyqt,qthread,Python,Matplotlib,Pyqt,Qthread,我真的很难理解如何在PyQt中使用线程。我举了一个简单的例子来说明我想在UI中做什么。在下面的代码中,我希望用户输入一个股票代码(例如,您可以输入“bby”、“goog”或“v”),并绘制特定时期的股票价值。事情发生在更复杂的Ui中,或者在很长一段时间内,当情节更新时,Ui冻结。因此,我创建了一个“Plotter”类,当它收到某个信号时更新绘图(覆盖Qthread.run显然不是正确的方法)。我想让这个“绘图仪”在主线程之外的另一个线程中运行 一旦我取消对线程行的注释,程序就会停止工作。我试着推

我真的很难理解如何在PyQt中使用线程。我举了一个简单的例子来说明我想在UI中做什么。在下面的代码中,我希望用户输入一个股票代码(例如,您可以输入“bby”、“goog”或“v”),并绘制特定时期的股票价值。事情发生在更复杂的Ui中,或者在很长一段时间内,当情节更新时,Ui冻结。因此,我创建了一个“Plotter”类,当它收到某个信号时更新绘图(覆盖Qthread.run显然不是正确的方法)。我想让这个“绘图仪”在主线程之外的另一个线程中运行

一旦我取消对线程行的注释,程序就会停止工作。我试着推动新线程和“连接”的启动,但没有任何效果。我想,即使在阅读了Qt网站上的示例并查看了之后,我仍然不太理解Qthread是如何工作的

如果你知道如何做到这一点,它将帮助很多!(我正在使用Python 3.5和PyQt5)


第一个问题是,一旦
线程启动,就会丢失对它的引用。要保留引用,请使用类变量,即
self.thread
而不是
thread

接下来,在执行任何操作之前,必须先启动线程。因此,您需要将
self.thread.start()
放在信号发射的前面

现在,它已经可以工作了,但是一旦您想要启动一个新线程,就会出现下一个问题。所以,你需要先杀了那个老家伙。由于旧的
绘图仪
将无法使用,因此解决方案是每次要打印时创建一个新绘图仪和一个新线程。这就是下面的解决方案的工作方式。
或者,也可以始终使用相同的绘图仪和线程。唯一需要记住的是,始终只有一个工作线程(绘图仪)和一个线程,如果您删除其中一个,另一个线程是悲伤的

为了测试它,我需要更改一些小事情,比如使用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_())

您的代码不是线程安全的。不能从辅助线程进行matplotlib(或任何Qt GUI)调用。您可以在线程中获取数据,但需要通过发出自定义信号将其发送回主线程进行打印(因此返回打印数据,而不是现在返回的轴对象),谢谢!您的代码确实有效,并且确实需要我想要的,但是线程似乎永远不会结束。我在if语句中添加了一个print(True),每次您输入一个ticker(第一次除外),程序都会进入该循环。此外,如果快速输入2个标记,绘图将永远停止更新。此外,文档中不建议使用terminate。也许好的解决方案是采用您提出的第二种方案,但我不确定如何实施。更新了第二种方案的解决方案。此新解决方案也是线程安全的。@importantanceofbeingernest是否也可以将.draw()移动到qthread?根据图表的复杂性,此函数需要花费大量时间。或者为所有子线程使用qthread draw_artist(),只在主线程中调用FigureCanvas.update()?我认为不可能在两个不同的线程上运行GUI。由于图形画布是GUI的一部分,因此必须在主线程内绘制。但是你可以试一下,看看哪一点失败了。当然不可能用不同的线画出图形的一部分。
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_())