PySide2 和 Matplotlib:如何让 MatPlotLib 在单独的进程中运行? ..因为它不能在单独的线程中运行

Posted

技术标签:

【中文标题】PySide2 和 Matplotlib:如何让 MatPlotLib 在单独的进程中运行? ..因为它不能在单独的线程中运行【英文标题】:PySide2 and Matplotlib: How to make MatPlotLib run in a separate Process? ..as it cannot run in a separate Thread 【发布时间】:2019-09-24 08:10:53 【问题描述】:

我不是经验丰富的程序员,我正在尝试使用 Qt for python (PySide2) 在 python 中创建一种数据记录器程序来构建 GUI。我能够使用 Designer 创建一个 gui 并将其加载到 python 中。 gui 现在只是一个空白窗口。然后我创建了一个在显示图形的窗口中启动 MatplotLib 的函数,并使用 Qt 计时器更新主程序的每个循环中的数据。

一切正常,但 MatPlotLib 的重绘时间太慢了 gui 刷新。所以我尝试将 MatPlotLib 放在一个单独的线程中,经过大量试验后我明白它不能在单独的线程中运行.. 最后我决定尝试使用多处理。现在 MatPlotLib 在单独的进程中运行良好(我使用队列将数据发送到 MatPlotLib)并在进程完成后正确退出,但是当我关闭主窗口时,程序更新完全关闭,并且还键入 Ctrl+C 提示是被屏蔽了。

这是我的代码:

#!/usr/bin/env python3
import sys
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication, QWidget
from PySide2.QtCore import QFile, QTimer

import matplotlib.pyplot as plt
from multiprocessing import Process, Queue, freeze_support
import random


class DSL(QWidget):
    def __init__(self):
        # LOAD HMI
        QWidget.__init__(self)
        designer_file = QFile('userInterface.ui')
        designer_file.open(QFile.ReadOnly)
        loader = QUiLoader()
        self.ui = loader.load(designer_file, self)
        designer_file.close()
        self.ui.show()

        # Data to be visualized
        self.data = []

    def mainLoop(self):
        self.data = []
        for i in range(10):
            self.data.append(random.randint(0, 10))

        # Send data to graph process
        queue.put(self.data)

        # LOOP repeater
        QTimer.singleShot(10, self.mainLoop)


def graphprocess(queue):
    for i in range(10):
        # Get data
        data = queue.get()

        # MatPlotLib
        plt.ion()
        plt.clf()
        plt.plot(data)
        plt.show()
        plt.pause(0.1)

    print('process end')


if __name__ == '__main__':
    # MatPlotLib Process
    queue = Queue()
    freeze_support()
    p = Process(target=graphProcess, args=(queue,))
    p.daemon = True
    p.start()

    # PySide2 Process
    app = QApplication(sys.argv)
    dsl = DSL()
    dsl.mainLoop()
    sys.exit(app.exec_())

【问题讨论】:

【参考方案1】:

与其在辅助进程中使用 matplotlib 不如在 QWidget 中嵌入一个画布,使其可以在相同的 PySide2 进程中运行:

#!/usr/bin/env python3
import sys

from PySide2.QtCore import QFile, QObject, Signal, Slot, QTimer
from PySide2.QtWidgets import QApplication, QVBoxLayout, QWidget
from PySide2.QtUiTools import QUiLoader

import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure

import random


class DSL(QObject):
    dataChanged = Signal(list)

    def __init__(self, parent=None):
        # LOAD HMI
        super().__init__(parent)
        designer_file = QFile("userInterface.ui")
        if designer_file.open(QFile.ReadOnly):
            loader = QUiLoader()
            self.ui = loader.load(designer_file)
            designer_file.close()
            self.ui.show()
        # Data to be visualized
        self.data = []

    def mainLoop(self):
        self.data = []
        for i in range(10):
            self.data.append(random.randint(0, 10))
        # Send data to graph
        self.dataChanged.emit(self.data)
        # LOOP repeater
        QTimer.singleShot(10, self.mainLoop)


class MatplotlibWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        fig = Figure(figsize=(7, 5), dpi=65, facecolor=(1, 1, 1), edgecolor=(0, 0, 0))
        self.canvas = FigureCanvas(fig)
        self.toolbar = NavigationToolbar(self.canvas, self)
        lay = QVBoxLayout(self)
        lay.addWidget(self.toolbar)
        lay.addWidget(self.canvas)

        self.ax = fig.add_subplot(111)
        self.line, *_ = self.ax.plot([])

    @Slot(list)
    def update_plot(self, data):
        self.line.set_data(range(len(data)), data)

        self.ax.set_xlim(0, len(data))
        self.ax.set_ylim(min(data), max(data))
        self.canvas.draw()


if __name__ == "__main__":

    app = QApplication(sys.argv)
    dsl = DSL()
    dsl.mainLoop()

    matplotlib_widget = MatplotlibWidget()
    matplotlib_widget.show()

    dsl.dataChanged.connect(matplotlib_widget.update_plot)
    sys.exit(app.exec_())

【讨论】:

嗨 eyllanesc,非常感谢您的快速回答,这是我第一次使用 ***,我很高兴它的帮助。您编写的代码完美运行,这对我来说是向前迈出的一大步。我有一些问题想请教您的选择: - 我的选择不适合你吗? (了解我的错误..) - 是否可以同时在图中绘制更多线(我需要两条) - 如果绘图窗口关闭,我使用按钮再次显示绘图窗口,使用 matplotlib_widget.show(),是吗对 ? - 为什么如果我关闭主窗口,grap 会继续绘制? @Emilio 1) 我不会指出它是否正确,但正如您所见,还有另一个更简单的选择。 2)如果可以绘制任何图,在下面的链接中有一个示例:gist.github.com/eyllanesc/e5a2f0c86d80f4a2f241d6e3584032a9,3)是的,它是正确的,代码必须是:your_button.clicked.connect(matplotlib_widget.show) 4)尝试使用此功能的链接示例 /--------- 问题 ----------/ \n 不幸的是,更深入地测试你的代码,我在第一次实现时遇到了同样的问题创建绘图的代码只是在 DSL 类的一个函数中。如果你放更多、更多的点(例如 1000+),整个程序会变慢,包括主 GUI。这是因为正如您所说,两个窗口都在同一个进程中。这就是我试图避免的多处理。我在尝试多处理之前的代码是: @Emilio 问题在于你的 for 循环耗时,检查链接的更新内容,你会发现它的作用相同,但效率更高。总之,分析瓶颈在哪里,尝试改进它,不要责怪代码的另一部分。

以上是关于PySide2 和 Matplotlib:如何让 MatPlotLib 在单独的进程中运行? ..因为它不能在单独的线程中运行的主要内容,如果未能解决你的问题,请参考以下文章

带有 Pyside2 和 Matplotlib 的 Pyinstaller 无法正常工作

由于 PySide2 和 Matplotlib,Travis-CI 无法构建

在 Pyside2 中嵌入 Matplotlib 的释放错误

在使用 matplotlib 和 PySide2 运行的应用程序中使用 pdb 调试器时出现“事件循环已在运行”

PySide2,如何拉伸 QTableWidget 以适应窗口宽度?

Matplotlib NavigationToolbar2QT 仅显示禁用的左右箭头。如何显示缩放和平移按钮?