如何在PyQt5和matplotlib中同时处理和显示数据

How to process data and display it simultaneously in PyQt5 and matplotlib

我正在尝试扩展 here 中关于如何将 matplotlib 图集成到 PyQt5 window 中的示例,并在应用程序 运行 中更新它。

我对上面链接的代码所做的更改是情节更新时的条件,即我向应用程序添加了一个按钮,该按钮链接到一个名为 simulate 的函数,而不是计时器并运行 for 循环。在循环内部,我们调用 update_plot 函数,其他一切保持不变。

def simulate(self, ydata):
    for k in trange(0, 500, 10):
        tmp = np.random.uniform(0, 1, size=(2, 1000))
        self.xdata = tmp[0]
        self.ydata = tmp[1]
        self.update_plot()

完整代码为

import sys
from PyQt5.QtWidgets import QVBoxLayout, QSizePolicy, QWidget, QPushButton, QHBoxLayout, QSpacerItem
from PyQt5 import QtWidgets

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import random

import numpy as np
from time import sleep

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from matplotlib.figure import Figure


class MplCanvas(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        widget =  QWidget(self)
        self.setCentralWidget(widget)
        vlay = QVBoxLayout(widget)
        hlay = QHBoxLayout()
        vlay.addLayout(hlay)


        pybutton = QPushButton('Fit!', self)
        pybutton.clicked.connect(self.simulate)
        hlay2 = QHBoxLayout()
        hlay2.addWidget(pybutton)
        hlay2.addItem(QSpacerItem(1000, 10, QSizePolicy.Expanding))
        vlay.addLayout(hlay2)

        self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
        vlay.addWidget(self.canvas)
        
        self.n_data = 999
        self.xdata = list(range(self.n_data))
        self.ydata = [random.uniform(0, 1) for i in range(self.n_data)]

        # We need to store a reference to the plotted line
        # somewhere, so we can apply the new data to it.
        self._plot_ref = None
        self.update_plot()

        self.show()


    def simulate(self):
        for k in range(0, 5):
            print(k)
            tmp = np.random.uniform(0, 1, size=(2,self.n_data))
            self.xdata = tmp[0]
            self.ydata = tmp[1]
            self.update_plot()
            sleep(0.5)
            

    def update_plot(self):
        # Drop off the first y element, append a new one.
        #self.ydata = self.ydata[1:] + [random.randint(0, 10)]

        # Note: we no longer need to clear the axis.
        if self._plot_ref is None:
            plot_refs = self.canvas.axes.plot(self.xdata, self.ydata, 'r')
            self._plot_ref = plot_refs[0]
        else:
            self._plot_ref.set_ydata(self.ydata)

        # Trigger the canvas to update and redraw.
        self.canvas.draw()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    app.exec_()

现在发生的事情是,无论何时单击按钮 self.simulate() 都会被调用,因此 for 循环开始......正如您在函数中看到的那样,我调用 self.update_plot 每次,但显示的图只有在循环结束后才会更新,即函数完成...

有人能解释一下这是怎么回事吗?

我认为 Qt 只在主事件循环空闲时才刷新显示。因此,当您使用 运行 simulate 方法时,它无法刷新。快速修复是在 update_plot().

之后添加 app.processEvents()

如果您有多个 GUI 项目需要经常更新,那么 processEvents 技巧将不会很好地发挥作用,您将不得不开始研究线程。特别是 QThreads,因为它们被设计为与 Qt 配合得很好。

[编辑:使用 QThread:] 使用 QThread 的代码如下。我借用了 this answer and this answer 的方法。设置线程有很多开销。而且您必须 非常 小心处理数据。将数据传入和传出线程和工作者的首选方法是使用槽和信号。还有信号量系统,我没用过

虽然这段代码很乏味,但它提供了一个高性能的 GUI。当 simulate 为 运行 时,您可以在屏幕上移动 window,情节将继续更新。

import sys
from PyQt5.QtWidgets import (QVBoxLayout, QSizePolicy, QWidget, QPushButton,
                             QHBoxLayout, QSpacerItem)
from PyQt5 import QtWidgets
from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import random

import numpy as np
from time import sleep

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from matplotlib.figure import Figure

class Worker(QObject):
    dataReady = pyqtSignal(object)
    finished  = pyqtSignal()

    def __init__(self, n_data):
        super().__init__()
        self.n_data = n_data

    def simulate(self):
        for k in range(0, 5):
            print(k)
            tmp = np.random.uniform(0, 1, size=(2,self.n_data))
            self.dataReady.emit(tmp)
            sleep(0.5)
        self.finished.emit()

class MplCanvas(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        widget =  QWidget(self)
        self.setCentralWidget(widget)
        vlay = QVBoxLayout(widget)
        hlay = QHBoxLayout()
        vlay.addLayout(hlay)


        pybutton = QPushButton('Fit!', self)
        pybutton.clicked.connect(self.simulate)
        hlay2 = QHBoxLayout()
        hlay2.addWidget(pybutton)
        hlay2.addItem(QSpacerItem(1000, 10, QSizePolicy.Expanding))
        vlay.addLayout(hlay2)

        self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
        vlay.addWidget(self.canvas)

        self.n_data = 999
        self.xdata = list(range(self.n_data))
        self.ydata = [random.uniform(0, 1) for i in range(self.n_data)]

        # We need to store a reference to the plotted line
        # somewhere, so we can apply the new data to it.
        self._plot_ref = None
        self.update_plot([self.xdata, self.ydata])

        self.show()

    def simulate(self):
        # now simulate from MainWindow will call run simulate in Worker

        self.thread = QThread()
        self.worker = Worker(n_data=self.n_data)
        self.worker.dataReady.connect(self.update_plot)
        self.worker.moveToThread(self.thread)
        self.worker.finished.connect(self.thread.quit)
        self.thread.started.connect(self.worker.simulate)
        self.thread.start()



    @pyqtSlot(object)
    def update_plot(self, data):
        # Drop off the first y element, append a new one.
        #self.ydata = self.ydata[1:] + [random.randint(0, 10)]
        print('got new data')
        xdata = data[0]
        ydata = data[1]

        # Note: we no longer need to clear the axis.
        if self._plot_ref is None:
            plot_refs = self.canvas.axes.plot(xdata, ydata, 'r')
            self._plot_ref = plot_refs[0]
        else:
            self._plot_ref.set_ydata(ydata)

        # Trigger the canvas to update and redraw.
        self.canvas.draw()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    app.exec_()