QtWidget.grab() 在循环中调用时逐渐消耗内存

QtWidget.grab() consumes gradually memory when called in the loop

我有 PyQt5 GUI,我在其中加载了一些数据,然后将其绘制成图表

不要上传我创建的整个应用程序只是示例,其中使用什么崩溃...

一次,我需要将“GUI-visible”图形保存为图片(以备后用),所以调用:

grabbed = some_graphically_visible_widget.grab()

grabbed.save("My_name.png")

这两个方法在循环中被调用多达 350 次,并且在 循环,python 将抓取的“对象”保存在某处,因为 memory_profiler 显示,我发现,每个 .grab() 周期内存消耗增加 ~ 1.5MB

此外,我尝试了多种使用方式:

del grabbed

循环结束,或者播放

gc.collect()

但没有任何帮助,调用这个循环总是吃掉“它的一部分”。

以下是完整的示例应用程序,一次即可完全运行 PyQt5 和 pyqtgraph 模块被提供为“导入”:

import sys
import os
from random import randint

from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtWidgets import QShortcut, QMessageBox
from PyQt5.QtGui import QKeySequence
import pyqtgraph

app = QtWidgets.QApplication(sys.argv)

class Ui_MainWindow(object):

    def __init__(self):
        self.graph_list = []
    def setupUi(self, MainWindow):

        MainWindow.setObjectName("Example")
        MainWindow.resize(750, 750)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        MainWindow.setCentralWidget(self.centralwidget)
        self.tabWidget = QtWidgets.QTabWidget(self.centralwidget)
        self.tabWidget.setGeometry(QtCore.QRect(5, 5, 740, 740))
        self.tabWidget.setObjectName("tabWidget")
        self.shortcut_CtrlL = QShortcut(QKeySequence('Ctrl+E'),self.centralwidget)
        self.shortcut_CtrlL.activated.connect(self.doExport)

        progress = QtWidgets.QProgressDialog("Creating enough graphs to simulate my case ... (350) ", None, 0, 350, self.centralwidget)
        progress.setWindowTitle("...")
        progress.show()

        "Typical amount of graphs in application"
        for tab_idx in range(350):

            font = QtGui.QFont()
            font.setPixelSize(15)

            tab = QtWidgets.QWidget()
            graph = pyqtgraph.PlotWidget(tab)
            self.graph_list.append(graph)
            graph.setGeometry(QtCore.QRect(5, 5, 740, 740))
            graph.addLegend(size=None, offset=(370, 35))
            x = []
            y = []
            min = []
            max = []
            for num in range(10):
                x.append(num)
                y.append(randint(0, 10))
                min.append(0)
                max.append(10)
            graph.plot(x, y, symbol='o', symbolPen='b', symbolBrush='b', name = "List of randomized values")
            graph.plot(x, min, pen=pyqtgraph.mkPen('r', width=3, style=QtCore.Qt.DashLine))
            graph.plot(x, max, pen=pyqtgraph.mkPen('r', width=3, style=QtCore.Qt.DashLine))
            graph.showGrid(x=True)
            graph.showGrid(y=True)
            graph.setTitle(str(graph))

            self.tabWidget.addTab(tab, str(tab_idx))
            progress.setValue(tab_idx)
            app.processEvents()

        msgBox = QMessageBox()
        msgBox.setIcon(QMessageBox.Information)
        str_to_show = "Once you see GUI, press CTRL+E and watch memory consumption in task manager"
        msgBox.setText(str_to_show)
        msgBox.setWindowTitle("Information")
        msgBox.setStandardButtons(QMessageBox.Ok)
        msgBox.exec()

        progress.close()


    def doExport(self):

        iterations = 0
        progress = QtWidgets.QProgressDialog("Doing .grab() and .save() iterations \nnow you may watch increase RAM consumption - you must open taskmgr", None, 0, 350, self.centralwidget)
        progress.setWindowTitle("...")
        progress.show()
        for graph in self.graph_list:
            iterations += 1
            grabbed = graph.grab()
            grabbed.save("Dont_worry_I_will_be_multiple_times_rewritten.png")
            progress.setValue(iterations)
            app.processEvents()
        progress.close()

        msgBox = QMessageBox()
        msgBox.setIcon(QMessageBox.Information)
        str_to_show = str(iterations) + ' graphs was grabbed and converted into .png and \n python\'s RAM consumption had to increase ...'
        msgBox.setText(str_to_show)
        msgBox.setWindowTitle("Information")
        msgBox.setStandardButtons(QMessageBox.Ok)
        msgBox.exec()


if __name__ == "__main__":

    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

这与grab无关(至少,不是直接的),而是与QGraphicsViewcaching(pyqtgraph PlotWidgets实际上是QGraphicsView subclasses)。

事实上,如果您对整个抓取进行注释并改用 self.tabWidget.setCurrentIndex(iterations),无论如何您都会看到内存激增,那是因为 grab() 显然会导致小部件被绘制,因此, 创建图形视图缓存。

您的问题的解决方案是禁用每个图的缓存:

    def setupUi(self, MainWindow):
        # ...
        for tab_idx in range(350):
            # ...
            graph = pyqtgraph.PlotWidget(tab)
            self.graph_list.append(graph)
            graph.setCacheMode(graph.CacheNone)

真正的问题是:您真的需要添加这么多图表吗?如果您只需要抓取每个图形,请使用单个绘图小部件,并在 for 循环中设置每个 plot/grab。老实说,我不明白一次显示这么多图表有什么好处:350 个 QGraphicsViews 很多,我真诚地怀疑你真的需要用户同时访问它们一次,特别是考虑到使用 QTabWidget 会使它们难以访问。

还有:

  1. 您正在为每个选项卡创建一个 tab QWidget,但您只是添加了一个图形视图,没有任何布局管理器;这会导致在调整主要 window 大小时出现问题(绘图小部件不调整其大小)并且无论如何都是不必要的:只需将绘图小部件添加到 QTabWidget:self.tabWidget.addTab(graph, str(tab_idx))
  2. 从不修改pyuic生成的文件,也不要试图模仿他们的行为;如果您从代码构建 UI,只需子 class 将充当 container/window 的小部件(在您的情况下为 QMainWindow),向其添加子小部件(使用中央小部件对于 main windows) 并实现 class 中的所有方法,否则请遵循关于 using Designer.
  3. 的官方指南