PyQt5 Memory Management - python

I would like to understand how Python's and PyQt's garbage collectors work. In the following example, I create a QWidget (named TestWidget) that has a python attribute 'x'. I create TestWidget, interact with it, then close its window. Since I have set WA_DeleteOnClose, this should signal the Qt event loop to destroy my instance of TestWidget. Contrary to what I expect, at this point (and even after the event loop has finished) the python object referenced by TestWidget().x still exists.
I am creating an app with PyQt where the user opens and closes many many widgets. Each widget has attributes that take up a substantial amount of memory. Thus, I would like to garbage collect (in both Qt and Python) this widget and its attributes when the user closes it. I have tried overriding closeEvent and deleteEvent to no success.
Can someone please point me in the right direction? Thank you! Example code below:
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit
from PyQt5.QtCore import Qt
class TestWidget(QWidget):
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
self.widget = None
self.x = '1' * int(1e9)
def load(self):
layout = QVBoxLayout(self)
self.widget = QTextEdit(parent=self)
layout.addWidget(self.widget)
self.setLayout(layout)
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
import gc
app = QApplication([])
widgets = []
widgets.append(TestWidget(parent=None))
widgets[-1].load()
widgets[-1].show()
widgets[-1].activateWindow()
app.exec()
print(gc.get_referrers(gc.get_referrers(widgets[-1].x)[0]))

An important thing to remember is that PyQt is a binding, any python object that refers to an object created on Qt (the "C++ side") is just a wrapper.
The WA_DeleteOnClose only destroys the actual QWidget, not its python object (the TestWidget instance).
In your case, what's happening is that Qt releases the widget, but you still have a reference on the python side (the element in your list): when the last line is executed, the widgets list and its contents still exist in that scope.
In fact, you can try to add the following at the end:
print(widgets[-1].objectName())
And you'll get the following exception:
Exception "unhandled RuntimeError"
wrapped C/C++ object of type TestWidget has been deleted
When also the python object is deleted, then all its attributes are obviously deleted as well.
To clarify, see the following:
class Attribute(object):
def __del__(self):
print('Deleting Attribute...')
class TestWidget(QWidget):
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
self.widget = None
self.x = Attribute()
def load(self):
layout = QVBoxLayout(self)
self.widget = QTextEdit(parent=self)
layout.addWidget(self.widget)
self.setLayout(layout)
def __del__(self):
print('Deleting TestWidget...')
You'll see that __del__ doesn't get called in any case with your code.
Actual deletion will happen if you add del widgets[-1] instead.

Explanation
To understand the problem, you must know the following concepts:
Python objects are eliminated only when they no longer have references, for example in your example when adding the widget to the list then a reference is created, another example is that if you make an attribute of the class then a new reference is created.
PyQt (and also PySide) are wrappers of the Qt library (See 1 for more information), that is, when you access an object of the QFoo class from python, you do not access the C++ object but the handle of that object. For this reason, all the memory logic that Qt creates is handled by Qt but those that the developer creates has to be handled by himself.
Considering the above, what the WA_DeleteOnClose flag does is eliminate the memory of the C++ object but not the python object.
To understand how memory is being handled, you can use the memory-profiler tool with the following code:
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit
from PyQt5.QtCore import Qt, QTimer
from PyQt5 import sip
class TestWidget(QWidget):
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
self.widget = None
self.x = ""
period = 1000
QTimer.singleShot(4 * period, self.add_memory)
QTimer.singleShot(8 * period, self.close)
QTimer.singleShot(12 * period, QApplication.quit)
def load(self):
layout = QVBoxLayout(self)
self.widget = QTextEdit()
layout.addWidget(self.widget)
def add_memory(self):
self.x = "1" * int(1e9)
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication
import gc
app = QApplication([])
app.setQuitOnLastWindowClosed(False)
widgets = []
widgets.append(TestWidget(parent=None))
widgets[-1].load()
widgets[-1].show()
widgets[-1].activateWindow()
app.exec()
In the previous code, in second 4 the memory of "x" is created, in second 8 the C++ object is eliminated but the memory associated with "x" is not eliminated and this is only eliminated when the program is closed since it is clear the list and hence the python object reference.
Solution
In this case a possible solution is to use the destroyed signal that is emitted when the C++ object is deleted to remove all references to the python object:
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit
from PyQt5.QtCore import Qt, QTimer
from PyQt5 import sip
class TestWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self.widget = None
self.x = ""
period = 1000
QTimer.singleShot(4 * period, self.add_memory)
QTimer.singleShot(8 * period, self.close)
QTimer.singleShot(12 * period, QApplication.quit)
def load(self):
layout = QVBoxLayout(self)
self.widget = QTextEdit()
layout.addWidget(self.widget)
def add_memory(self):
self.x = "1" * int(1e9)
class Manager:
def __init__(self):
self._widgets = []
#property
def widgets(self):
return self._widgets
def add_widget(self, widget):
self._widgets.append(widget)
widget.destroyed.connect(self.handle_destroyed)
def handle_destroyed(self):
self._widgets = [widget for widget in self.widgets if not sip.isdeleted(widget)]
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication
app = QApplication([])
app.setQuitOnLastWindowClosed(False)
manager = Manager()
manager.add_widget(TestWidget())
manager.widgets[-1].load()
manager.widgets[-1].show()
manager.widgets[-1].activateWindow()
app.exec()

Related

Accessing and Editing UI Elements between Different Classes in PyQT5

I am in the process of writing code that generates a main 'TopLevel' window where information is entered and then, upon the click of a pushbutton, another window opens and this information is sent through and accessed/edited/interacted with in that window. I have seemingly tried every way I can find on this website and others to try to access ui elements (be they labels, text inputs or, in this specific case, a dictionary) between different classes and nothing has worked so far.
I've edited the code below so that all the relevant elements are available and this version runs up to the point that the error is produced. The user types whatever they want in the box underneath "Input dictionary elements", clicks "Done" to add it to the list underneath "List of dictionary elements" and can then press "Expand" under the leftmost box to open the second window that I want to pass the list to.
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QDesktopWidget, QMessageBox, QPushButton, QAction, qApp, QMenu, QLineEdit, QLabel, QTextEdit, QComboBox, QScrollArea, QVBoxLayout, QScrollBar, QListWidget
from PyQt5.QtGui import QFont, QIcon
from PyQt5.QtCore import pyqtSlot, QCoreApplication, QMetaObject, QRect
def setKey(dictionary, key, value):
if key not in dictionary:
dictionary[key] = value
elif type(dictionary[key]) == list:
dictionary[key].append(value)
else:
dictionary[key] = [dictionary[key], value]
class TopLevel(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('Top Level')
self.resize(800,550)
topLevelLabel = QLabel(self)
topLevelLabel.setText('Top level of tree')
self.topLevelBox = QTextEdit(self)
self.topLevelBox.resize(200,50)
self.topLevelBox.move(60,100)
topLevelLabel.move(60,70)
topLevelExpandButton = QPushButton('Expand', self)
topLevelExpandButton.resize(50,30)
topLevelExpandButton.move(100,150)
topLevelExpandButton.pressed.connect(self.open_New_Window_Expand)
selectSCLabel = QLabel(self)
selectSCLabel.setText('Input dictionary elements')
self.selectSCBox = QTextEdit(self)
self.selectSCBox.resize(300,50)
self.selectSCBox.move(300,100)
selectSCLabel.move(300,60)
selectSCLabel.resize(400,50)
selectSCConfirmationButton = QPushButton('Done', self)
selectSCConfirmationButton.resize(40,30)
selectSCConfirmationButton.move(560,150)
selectSCConfirmationButton.pressed.connect(self.selectSCConfirmationButtonPressed)
listOfSelectedSCLabel = QLabel(self)
listOfSelectedSCLabel.setText('List of dictionary elements')
listOfSelectedSCLabel.setWordWrap(True)
listOfSelectedSCLabel.move(60,190)
listOfSelectedSCLabel.resize(200,30)
self.listOfSelectedSCArea = QScrollArea(self)
self.placeholderListItem = QWidget()
self.vboxOfListItems = QVBoxLayout()
self.listOfSelectedSCArea.move(60,220)
self.listOfSelectedSCArea.resize(200,300)
self.listOfSelectedSCArea.setVerticalScrollBarPolicy(0)
self.listOfSelectedSCArea.setHorizontalScrollBarPolicy(0)
self.listOfSelectedSCArea.setWidgetResizable(True)
self.listOfSelectedSCDictionary = {}
self.SCIterate = 1
self.show()
def open_New_Window_Expand(self):
self.new_window = QMainWindow
self.ui = NewLevelExpand()
self.show()
def selectSCConfirmationButtonPressed(self):
newSCName = self.selectSCBox.toPlainText()
nameToLabel = QLabel(self)
nameToLabel.setText(newSCName)
self.vboxOfListItems.addWidget(nameToLabel)
self.placeholderListItem.setLayout(self.vboxOfListItems)
self.listOfSelectedSCArea.setWidget(self.placeholderListItem)
setKey(self.listOfSelectedSCDictionary, self.SCIterate, newSCName)
self.SCIterate = self.SCIterate + 1
print(self.listOfSelectedSCDictionary)
class NewLevelExpand(TopLevel):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self, parent = TopLevel):
self.setWindowTitle('Expanded Level')
self.resize(800,550)
print(self.listOfSelectedSCDictionary)
self.show()
def main():
app = QApplication(sys.argv)
tl = TopLevel()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
With tinkering with this code, I have generated numerous different error messages but the code above produces the message: "AttributeError: 'NewLevelExpand' object has no attribute 'listOfSelectedSCDictionary'"
As I say, I feel I have tried everything relating to this topic on stackoverflow and nothing has worked. Any advice and guidance, pointing out stupid errors, suggestions to changing my coding style to make it clearer, etc. would be massively appreciated.
Premise: I cannot help you a lot with the logic behind your program; I've to admit, it's a bit confused, and I don't understand how you're going to pass data to the new window, nor how you are going to use it.
The error you're facing is because NewLevelExpand is a subclass of TopLevel, and when you call its super().__init__() this results in calling the ancestor's __init__().
In the TopLevel class, __init__() calls initUI, which creates the whole UI and the listOfSelectedSCDictionary dictionary.
The problem is that you are overriding initUI in the subclass, so the result is that the base class initUI is never called (meaning that the UI nor the dictionary are actually created).
The solution to this specific problem is to call the base class implementation of initUI in the subclass:
class NewLevelExpand(TopLevel):
# ...
def initUI(self):
super().initUI()
self.setWindowTitle('Expanded Level')
self.resize(800,550)
print(self.listOfSelectedSCDictionary)

The signal and slot within custom class including PyQt QWidget not working

I have some trouble customizing a class including a QPushButton and QLabel, and I just want to set the QPushButton checkable and define a slot to its toggled signal, in addition, the custom class inherents QObject.
The code is shown below,
import sys
from PyQt5 import QtGui
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import pyqtSlot, QObject
class CustomButton(QPushButton):
def __init__(self, label='', parent=None):
super().__init__(label, parent)
self.setCheckable(True)
self.toggled.connect(self.update)
def update(self, state):
if state:
self.setStyleSheet('background-color: green')
else:
self.setStyleSheet('background-color: red')
class ButtonCtrl(QObject):
def __init__(self, parent=None, label=''):
super().__init__()
if isinstance(parent, QLayout):
col = QVBoxLayout()
parent.addLayout(col)
else:
col = QVBoxLayout(parent)
self.text = ['ON', 'OFF']
self.label = QLabel(label)
self.button = QPushButton('ON')
# self.button = CustomButton('ON')
col.addWidget(self.label)
col.addWidget(self.button)
self.button.setCheckable(True)
self.button.setChecked(True)
self.button.toggled.connect(self.update)
self.update(True)
self.label.setFont(QFont('Microsoft YaHei', 14))
self.button.setFont(QFont('Microsoft YaHei', 12, True))
self.button.toggle()
# #pyqtSlot(bool)
def update(self, state):
if state:
self.button.setText(self.text[0])
self.button.setStyleSheet('background-color: green')
else:
self.button.setText(self.text[-1])
self.button.setStyleSheet('background-color: red')
class Window(QWidget):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
# set the layout
layout = QVBoxLayout(self)
but = ButtonCtrl(layout, "Test")
#self.but = ButtonCtrl(layout, "Test")
btn = CustomButton()
layout.addWidget(btn)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle('Fusion')
main = Window()
main.show()
sys.exit(app.exec_())
I have customized two buttons, named CustomButton(QPushButton) and ButtonCtrl(QObject), and I have tested the slot in the main window, however the background update slot works for CustomButton(QPushbutton) and does not work for ButtonCtrl(QObject), the slot function is not even invoked.
However, if I change the button member of ButtonCtrl(QObject) from QPushButton into my CustomButton(QPushButton), the it will work well in the main window. Furthermore, if the but in main window becomes a member of the main window class by setting self.but=ButtonCtrl(layout, "Test"), it will work as well.
I didn't find direct answer to it in Qt documentation which explains that
Signals are emitted by an object when its internal state has changed in some way that might be interesting to the object's client or owner. Signals are public access functions and can be emitted from anywhere, but we recommend to only emit them from the class that defines the signal and its subclasses.
I am not sure if the lifetime of the but causing this effect, hope to get an answer, thank you.
The problem is simple: The ButtonCtrl class object has a local scope so it will be destroyed, and why doesn't the same happen with the CustomButton class object? Well, because the ownership of a QWidget has its parent, and the parent of that button is the window, instead the ButtonCtrl object does not have it. In this case the solution is to extend the life cycle of the variable, and in the case of a QObject there are several options:
make the class member variable,
Place it in a container with a longer life cycle, or
establish a QObject parent with a longer life cycle.
Using the third alternative is just to change to:
class ButtonCtrl(QObject):
def __init__(self, parent=None, label=''):
super().__init__(parent)
# ...
The first option is the one commented on in your code:
self.but = ButtonCtrl(layout, "Test")
and the second is similar:
self.container = list()
but = ButtonCtrl(layout, "Test")
self.container.append(but)

Sending Data from Child to Parent Window in PyQt5

What I can't do
I'm not able to send data back from a child to a parent window.
What I have
I've got a complex GUI with several windows sendíng data to child windows. Each window represents a unique Python-script in the same directory. There was no need to explicitely specify parents and childs, as the communication was always unidirectional (parent to child). However, now I need to send back data from childs to parents and can't figure out how to do this as each window (i.e. each class) has its own file.
Example
Here's a minimal example showing the base of what I want to accomplish.
What it does: win01 opens win02 and win02 triggers func in win01.
# testfile01.py
import sys
from PyQt5.QtWidgets import *
import testfile02 as t02
class win01(QWidget):
def __init__(self, parent=None):
super(win01, self).__init__(parent)
self.win02 = t02.win02()
self.button = QPushButton("open win02", self)
self.button.move(100, 100)
self.button.clicked.connect(self.show_t02)
def initUI(self):
self.center
def show_t02(self):
self.win02.show()
def func(self):
print("yes!")
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = win01()
ex.show()
sys.exit(app.exec_())
##########################################################################
# testfile02.py
from PyQt5.QtWidgets import *
import testfile01 as t01
class win02(QWidget):
def __init__(self, parent=None):
super(win02, self).__init__(parent)
self.win01 = t01.win01()
self.button = QPushButton()
self.button.clicked.connect(self.win01.func)
def initUI(self):
self.center()
What I tried
Importing testfile01 in the second window always leads to the error:
RecursionError: maximum recursion depth exceeded.
Then, I tried the following approaches, but they didn't work either:
Not importing testfile01 in win02 and adjusting parent=None to different other objects
Importing testfile01 within the __init__ call of win02
Creating a signal in win02 to trigger func in win01
The Question
Is there a solution how to properly trigger func in win01 from win02?
Why are you getting RecursionError: maximum recursion depth exceeded?
You are getting it because you have a circular import that generates an infinite loop, in testfile01 you are importing the file testfile02, and in testfile02 you are importing testfile01, .... So that is a shows of a bad design.
Qt offers the mechanism of signals for objects to communicate information to other objects, and this has the advantage of non-dependence between classes that is a great long-term benefit (as for example that avoids circular import), so for that reason I think it is the most appropriate.
For this I will create the clicked signal in the class win02 that will be triggered by the clicked signal of the button, and make that clicked signal call the func:
testfile01.py
import sys
from PyQt5.QtWidgets import QWidget, QPushButton, QApplication
import testfile02 as t02
class win01(QWidget):
def __init__(self, parent=None):
super(win01, self).__init__(parent)
self.win02 = t02.win02()
self.win02.clicked.connect(self.func)
self.button = QPushButton("open win02", self)
self.button.move(100, 100)
self.button.clicked.connect(self.show_t02)
def show_t02(self):
self.win02.show()
def func(self):
print("yes!")
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = win01()
ex.show()
sys.exit(app.exec_())
testfile02.py
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout
class win02(QWidget):
clicked = pyqtSignal()
def __init__(self, parent=None):
super(win02, self).__init__(parent)
self.button = QPushButton("call to func")
self.button.clicked.connect(self.clicked)
lay = QVBoxLayout(self)
lay.addWidget(self.button)
I recommend you read:
https://doc.qt.io/qt-5/signalsandslots.html
Both the Widgets are independent and have no link in between.
Set win01 parent of win02.
In class win01
Replace :
self.win01 = t02.win02()
#and
self.win02.show()
with:
self.win01 = t02.win02(self)
#and
self.win01.show()
and in class win02
Replace:
self.win02 = t01.win01()
#and
self.button.clicked.connect(self.win01.func)
with:
self.win02 = self.parent()
#and
self.button.clicked.connect(self.win02.func)

PySide : Children objects not destroyed on close of parent object

I have the following code :
import sys
from PySide import QtGui
from PySide import QtCore
class Main_Window(QtGui.QMainWindow):
def __init__(self):
super(Main_Window,self).__init__()
self.initUI()
def initUI(self):
self.navigateur=QtGui.QMdiArea()
self.setCentralWidget(self.navigateur)
self.setGeometry(50, 50, 600, 600)
self.window =QtGui.QWidget(None,QtCore.Qt.WA_DeleteOnClose)
self.window.grid=QtGui.QGridLayout()
self.window.button=QtGui.QPushButton("quit",parent=self.window)
self.window.button.setObjectName("test")
self.window.button.clicked.connect(self.try_close)
self.window.grid.addWidget(self.window.button)
self.window.setLayout(self.window.grid)
self.window.setFixedSize(self.window.sizeHint())
self.fwindow=self.navigateur.addSubWindow(self.window,QtCore.Qt.WA_DeleteOnClose)
self.show()
def try_close(self):
self.fwindow.close()
print(self.window.findChild(QtGui.QPushButton,"test"))
def main():
app=QtGui.QApplication(sys.argv)
main_wdw=Main_Window()
sys.exit(app.exec_())
if __name__=="__main__":
main()
According to the documentation, when I close self.window, all children of self.window should be deleted however it doesn't seem to be the case since the print function prints something like PySide.QtGui.QPushButton object at...
What is going wrong ?
In Qt, the QObject are not deleted immediatly (see QObject::deleteLater() method). In Python, the object are deleted by the garbage collector.
So, your widget could be stayed in memory during a laps before the deletion.
The try_close method is not a good test, because it does not allow the necessary events to be processed before checking for child objects.
If a separate method is added for the check, e.g:
def initUI(self):
...
menu = self.menuBar().addMenu('File')
menu.addAction('Test', self.test)
def test(self):
for w in QtGui.qApp.allWidgets():
print(w.objectName(), w)
You will see that the widget with objectName "test" and its QPushButton child do get deleted once the close/deletion events have been processed.

Proper parenting of QWidgets generated from a QThread to be inserted into a QTreeWidget

I have a QTreeWidget which needs to be populated with a large sum of information. So that I can style it and set it up the way I really wanted, I decided I'd create a QWidget which was styled and dressed all pretty-like. I would then populate the TreeWidget with generic TreeWidgetItems and then use setItemWidget to stick the custom QWidgets in the tree. This works when the QWidgets are called inside the main PyQt thread, but since there is a vast sum of information, I'd like to create and populate the QWidgets in the thread, and then emit them later on to be added in the main thread once they're all filled out. However, when I do this, the QWidgets appear not to be getting their parents set properly as they all open in their own little window. Below is some sample code recreating this issue:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class ItemWidget(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
btn = QPushButton(self)
class populateWidgets(QThread):
def __init__(self):
QThread.__init__(self)
def run(self):
widget = ItemWidget()
for x in range(5):
self.emit(SIGNAL("widget"), widget)
class MyMainWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.tree = QTreeWidget(self)
self.tree.setColumnCount(2)
self.setCentralWidget(self.tree)
self.pop = populateWidgets()
self.connect(self.pop, SIGNAL("widget"), self.addItems)
self.pop.start()
itemWidget = QTreeWidgetItem()
itemWidget.setText(0, "This Works")
self.tree.addTopLevelItem(itemWidget)
self.tree.setItemWidget(itemWidget, 1, ItemWidget(self))
def addItems(self, widget):
itemWidget = QTreeWidgetItem()
itemWidget.setText(0, "These Do Not")
self.tree.addTopLevelItem(itemWidget)
self.tree.setItemWidget(itemWidget, 1, widget)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
ui = MyMainWindow()
ui.show()
sys.exit(app.exec_())
As you can see, doing it inside MyMainWindow is fine, but somehow things go awry once it gets processed in the thread and returns. Is this possible to do? If so, how do I properly parent the ItemWidget class inside the QTreeWidgetItem? Thanks in advance.
AFAICT Qt doesn't support the creation of QWidgets in a thread other than the thread where the QApplication object was instantiated (i.e. usually the main() thread). Here are some posts on the subject with responses from Qt developers:
http://www.qtcentre.org/archive/index.php/t-27012.html
http://www.archivum.info/qt-interest#trolltech.com/2009-07/00506/Re-(Qt-interest)-QObject-moveToThread-Widgets-cannot-be-moved-to-a-new-thread.html
http://www.archivum.info/qt-interest#trolltech.com/2009-07/00055/Re-(Qt-interest)-QObject-moveToThread-Widgets-cannot-be-moved-to-a-new-thread.html
http://www.archivum.info/qt-interest#trolltech.com/2009-07/00712/Re-(Qt-interest)-QObject-moveToThread-Widgets-cannot-be-moved-toa-new-thread.html
(if it were possible, the way to do it would be to call moveToThread() on the QWidgets from within the main thread to move them to the main thread -- but apparently that technique doesn't work reliably, to the extent that QtCore has a check for people trying to do that prints a warning to stdout to tell them not to do it)

Categories

Resources