I have PyQt5 GUI, where I load some data, which I consequently plot into graphs
to do not upload whole application I created just example where is used what crashes ...
once, I need to save "GUI-visible" graphs as pictures (for later usage), so I call:
grabbed = some_graphically_visible_widget.grab()
and
grabbed.save("My_name.png")
these two methods are called in the loop up to 350 times and during the loop, python saves the grabbed "object" somewhere because as memory_profiler showed and I found out, each .grab() cycle memory consumption increases ~ 1.5MB
also, I tried multiple variations of using:
del grabbed
in the end of loop, or playing with
gc.collect()
But nothing helped and calling this cycle always eats "it's part".
Below is full example application, which is fully working once the PyQt5 and pyqtgraph modules are provided to be "imported":
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_())
This has nothing to do with grab
(at least, not directly), but with QGraphicsView caching (pyqtgraph PlotWidgets are actually QGraphicsView subclasses).
In fact, if you comment the whole grabbing and use self.tabWidget.setCurrentIndex(iterations)
instead, you'll see the memory spiking anyway, and that's because grab()
obviously causes the widget to be painted and, therefore, create the graphics view cache.
The solution to your issue is to disable caching for each graph:
def setupUi(self, MainWindow):
# ...
for tab_idx in range(350):
# ...
graph = pyqtgraph.PlotWidget(tab)
self.graph_list.append(graph)
graph.setCacheMode(graph.CacheNone)
The real question is: do you really need to add so many graphs? If you only need to grab each graph, use a single plot widget, and set each plot/grab in a for cycle. Honestly, I don't understand what is the benefit of showing so many graphs all at once: 350 QGraphicsViews are a lot, and I sincerely doubt you really need the user to access them all at once, especially considering that using QTabWidget would make them hard to access.
Also:
tab
QWidget for each tab, but you're just adding a single graphics view and without any layout manager; this results in a problem when resizing the main window (the plot widgets don't adjust their size) and is anyway unnecessary: just add the plot widget to the QTabWidget: self.tabWidget.addTab(graph, str(tab_idx))
pyuic
, nor try to mimic their behavior; if you are building the UI from code, just subclass the widget that will act as a container/window (QMainWindow, in your case), add child widgets to it (using a central widget for main windows) and implement all methods from that class, otherwise follow the official guidelines about using Designer.