Is deleteLater() necessary in PyQt/PySide? - python

Since there is already a Garbage Collector in Python, is deleteLater() necessary in PyQt/PySide?

It depends what you mean by "necessary".
An application could potentially consume a lot of memory if (for example) care is not taken when closing widgets. The QObject-based classes are designed to be (optionally) linked together in a hierarchy. When a top-level object is deleted, Qt will automatically delete all its child objects as well. However, when closing widgets (which are sub-classes of QObject), automatic deletion will only happen if the Qt.WA_DeleteOnClose attribute is set (which, by default, it usually isn't).
To illustrate, try repeatedly opening and closing the dialog in this demo script, and watch how the global list of objects grows:
import sys
from PyQt5 import QtCore, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.checkbox = QtWidgets.QCheckBox('Delete')
self.button = QtWidgets.QPushButton('Open', self)
self.button.clicked.connect(self.openDialog)
layout = QtWidgets.QHBoxLayout(self)
layout.addWidget(self.checkbox)
layout.addWidget(self.button)
def openDialog(self):
widget = QtWidgets.QDialog(self)
if (self.checkbox.isChecked() and
not widget.testAttribute(QtCore.Qt.WA_DeleteOnClose)):
widget.setAttribute(QtCore.Qt.WA_DeleteOnClose)
for child in self.findChildren(QtWidgets.QDialog):
if child is not widget:
child.deleteLater()
label = QtWidgets.QLabel(widget)
button = QtWidgets.QPushButton('Close', widget)
button.clicked.connect(widget.close)
layout = QtWidgets.QVBoxLayout(widget)
layout.addWidget(label)
layout.addWidget(button)
objects = self.findChildren(QtCore.QObject)
label.setText('Objects = %d' % len(objects))
print(objects)
widget.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 100, 50)
window.show()
sys.exit(app.exec_())
With PyQt/PySide, there are two aspects to object ownership: the Python part, and the Qt part. Often, removing the last Python reference to an object won't be enough to fully clean up, because there could still be a reference held on the Qt side.
In general, Qt tends not to implicity delete objects. So if your application creates and removes lots of QObjects (or opens and closes lots of QWidgets), you may need to take steps to delete them explicitly if memory usage is a concern.
UPDATE:
Just to add to the points above on object ownership. Sometimes, it is possible to hold a Python reference to an object, whilst the Qt part gets deleted. When this happens, you will see an error like this:
RuntimeError: underlying C/C++ object has been deleted
Usually, the Qt documentation will give some hints about when this might happen. For instance, QAbstractItemView.setModel gives this warning:
The view does not take ownership of the model unless it is the model's parent object...
This is telling you that you must either keep a Python reference to the object, or pass a suitable parent object to the object's constructor, because Qt will not always automatically reparent it.

One application of deleteLater can be cleaning up of yourself, i.e. scheduling a delete of an QObject (for example in threading) to free resources from within the object itself.
Here for example someone is using it in connection with the signal thread.finished. It might be restricted to cases with heavy signalling though.

Related

In PyQT why somes widgets needs the "self" parameter before calling them, while others don't

I'm a little bit confused about the use of the "self" parameter with some widgets like (QLineEdit), indeed when learning to use the QLabel widget, I used to call the class without the self paramater, or when using the QLineEdit widget, the widget wouldn't work without the "self" parameter, here's the code I'm working on :
# Import necessary modules
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton
from PyQt5.QtCore import Qt
class EntryWindow(QWidget): # Inherits QWidget
def __init__(self): # Constructor
super().__init__() # Initializer which calls constructor for QWidget
self.initializeUI() # Call function used to set up window
def initializeUI(self):
"""
Initialize the window and display its contents to the screen
"""
self.setGeometry(400, 300, 400, 200)
self.setWindowTitle('QLineEdit Widget')
self.displayWidgets()
self.show() # Show everything
def displayWidgets(self):
'''
Setup the QLineEdit and other widgets.
'''
# Create name label and line edit widgets
QLabel("Please enter your name below.", self).move(100, 20)
name_label = QLabel("Name:", self)
name_label.move(55, 70)
self.name_entry = QLineEdit(self)
self.name_entry.move(120, 68)
self.name_entry.resize(200, 25) # Change size of entry field
self.name_entry.setAlignment(Qt.AlignLeft) # The default alignment
text_font = self.name_entry.font() # Get font option from the Qlineedit
text_font.setPointSize(12) # Modify font size
#text_font.setBold(True) # Bold
self.name_entry.setFont(text_font) # Apply font
self.clear_button = QPushButton('Clear text', self)
self.clear_button.clicked.connect(self.clearEntries)
self.clear_button.move(120, 130)
self.exit_button = QPushButton("Exit", self)
self.exit_button.clicked.connect(self.exitApplication)
self.exit_button.move(240, 130)
def clearEntries(self):
sender = self.sender()
if sender.text() == 'Clear text':
self.name_entry.clear()
def exitApplication(self):
sender = self.sender()
if sender.text() == "Exit":
self.close() # Close the window
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = EntryWindow()
sys.exit(app.exec_())
So here is where I'm confused, when using QLabel, I didn't have to put the "self" parameter before, or when using QLineEdit, I had to put "self" otherwise my code wouldn't work :
QLabel("Please enter your name below.", self).move(100, 20)
self.name_entry = QLineEdit(self)
First of all, the problem or difference has nothing to do with the "self", but what it is for, pre-established rules in Qt design.
In Qt there is a hierarchy tree between the QObjects where it is established that the parent QObject manages the memory (the life cycle of its children) so if the parent deleted the children, they will also be deleted. This allows avoiding memory leaks since many QObjects are generally used in many applications.
On the other hand, that concept of kinship is also passed to QWidgets since they are also QObjects, but there is also another characteristic: a QWidget in general will be drawn on top of its parent. So if you want the QLineEdit and QLabel to be part of the window then they must be children of the window so it is necessary that you pass the window object which is "self" as parent.
So when you go to the window (in this case "self") you avoid 2 problems:
That the object has a longer life cycle (the same as the window).
And you make the widget (either QLabel or QLineEdit) be placed on top of the window.
The self name is a Python convention to indicate the instance of an object. When set as argument in an object constructor, it's normally used to give the new object a reference to the one that created it, and that's normally called a "parent". Note that while self is usually used as parent argument, you can set any QWidget instance as parent.
In Qt terms, not only it indicates the roles in a "parentship" of the object structure, but also results in two important aspects:
In the world of QObjects (from which QWidget inherits) this helps for memory management: when a parent is deleted, all its children objects are deleted as well. It's also useful to find child objects (using findChild() or findChildren()) which are direct children or [great, ...] grandchildren of another.
When creating a widget with a QWidget parent, that widget is also only "drawn inside" it: it can only be visible within the boundaries of the parent, not outside it; note that this is usually optional, as most times you'll add a widget to a layout manager (but you didn't), which automatically reparents the widget to the widget that is managed by that layout: if it was a child of another widget, it's removed from that and added to the new one. Also consider that if a widget has no parent and is not added to a layout, it's considered a "top level widget" (a standalone window), and if show() or setVisible(True) is called it will have its own window.
This means that if the widget has a parent and the parent becomes visible, the child will be automatically visible too (unless hide() or setVisible() was explicitly called).
Note that while a QWidget that has no parent is always a top level window, a widger that has a parent could be a top level window, and that happens if it has the QtCore.Qt.Window flag set (using the f keyword in the constructor, or with setWindowFlags()). This is useful when the widget has to be a top level window, but the parentship must still be preserved, not only for memory management (when the parent window is closed and deleted, any child should be deleted as well) but for interaction purposes: for example, with QDialogs (which are standalone windows) it's important to set the parent, so that they can be modal to that parent, meaning that no keyboard/mouse interaction can happen on the parent until the dialog is closed.
Finally, it's important to remember that creating widgets "on the fly" in python has a (sometimes perceived incoherent) result: if no python reference is given to the widget but the widget has a parent, it won't garbage collected.
Consider the following:
def test1(self):
someDialog = QtWidgets.QDialog()
someDialog.show()
This will probably show a window just for an instant, and then it will disappear instantly.
def test2(self):
someDialog = QtWidgets.QDialog(self)
someDialog.show()
In this case, the dialog will not disappear, that's because it has a parent, PyQt knows it, and it will not try to delete it.
Even this is actually valid (but, no, don't do it):
def test2b(self):
QtWidgets.QDialog(self).show()
Similar result, but only apparently:
def test3(self):
self.someDialog = QtWidgets.QDialog()
self.someDialog.show()
In the case above, the visual result will be probably the same as test2, but if test3 is called again while the dialog is still visible, it will delete the current one and create a new one: that's because the dialog has no parent, and overwriting self.someDialog with a new instance will cause python to release the previous one, which will tell Qt that it can safely erase it, since it has no parent.
Finally, it's not necessary to set the parent to widgets, as long as they are added to a layout (which is the preferred approach, as using fixed geometries is highly discouraged), since the layout will automatically set the parent anyway as soon as it's [going to be] set on the actual parent.
Note that QDialog is normally shown with exec_() in order to correctly display it as modal of the parent and use it as it should.
Note the import statements at the top
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton
QLabel and QLineEdit as can be seen in the documentations
https://doc.qt.io/qtforpython/PySide2/QtWidgets/QLabel.html
https://doc.qt.io/qtforpython/PySide2/QtWidgets/QLineEdit.html
both take a QWidget as a parameter. The self passed as the second parameter indicates the current QWidget type. For other methods inside the class, self is used to invoke the instance methods.

QLayout Additem - prevent transfer of ownership

I am having problems with the object references when using the QLayout to arrange my widgets in on bigger window next to each other.
I have the following situation
class MyClass(QObject):
widgetCollection = []
def spawn(self):
widget = MyQWidget() #containing a QLineWidget called "nameEdit"
self.widgetCollection.append(widget)
self._window = QtGui.QWidget()
layout = QtGui.QHBoxLayout()
listView = QtGui.QListWidget()
for equation in self.wigdetCollection:
equationName = equation.nameEdit.text()
item = QtGui.QListWidgetItem(equationName)
listView.addItem(item)
layout.addWidget(listView)
layout.addWidget(widget)
self._window.setWindowTitle("Equation Editor")
self._window.setLayout(layout)
self._window.show()
def respawn(self):
self.spawn()
Each time I call spawn(), I want to add a new widget to the collection. Addionally, I want to open a new window where there is a ListView with all widget names on the left and the newly created widget on the right.
Now the first call to the spawn()-method works like expected. But the second call throws an exeption:
equationName = widget.nameEdit.text()
RuntimeError: wrapped C/C++ object of type QLineEdit has been deleted
I think it has something to do with the line
layout.addWidget(widget)
I have read somewhere that the layout takes ownership of the widget when added as an item. With that I loose the widget as soon as I am out of the scope of the layout-reference (which is local in this case). So it seems that the widget-item in my collection gets deleted too.
Can someone help? How do I prevent that.
The problem is that self._window is replaced every time spawn() is closed: Python discards the old window widget, and self._window is made to reference a new QWidget instance. Since the old window widget is parent to a list view and qlineedit widget (called -- confusingly -- just "widget" in the code), these will be destroyed in the Qt sense of the term -- i.e. their C++ portion will be destroyed. The list view is not referenced anywhere else in the shown code so the Python portion of the list view will be destroyed too, and this is the way it should be.
HOWEVER, the qlineedit is referenced in the class-wide registry (widgetCollection) and so the Python portion of the qlineedit will NOT be destroyed. This means that every time spawn() is called, the previous instance of QLineEdit (via MyQWidget in class-wide widgetCollection) becomes a zombie: it is dead at the C++ Qt level, but it is still alive at the Python level. A zombie's methods can still be accessed, but in many cases you will see the Qt error about C/C++ having been deleted, and you may get a crash or other undefined behavior.
It is not clear from the code posted what is the intent of the widgetCollection, but since the previous item appended will become a zombie during a spawn(), widgetCollection is useless as shown. The solution to the problem depends on details about how the collection is used, but since one can presume that the collection has a purpose, then the issue is the replacement of the window widget: perhaps the widget collection should be a window collection, then when self._window is replaced, the previous window stays alive so the child widgets stay alive too.
Solved it by myself :-)
My initial call to the spawn()-method was like that:
mc = MyClass()
mc.spawn()
First one okay. Now I wanted to spawn another from within inst1. For that I have used
self.spawn()
in the respawn()-method throwing the above mentioned error.
It has to be the following
def respawn(self):
mc = MyClass()
mc.spawn()
I had to create another instance of MyClass that shares the widgetCollection with all other instances. As desired....

PySide widgets using .ui files being garbage-collected unexpectedly

I have a PySide QMainWindow that I'm running in Nuke. Some widgets used by the application use .ui files created in Qt Designer.
Until recently, the QMainWindow class was not given a parent. Because of this, when Nuke was minimized or changed focus, the QMainWindow did not minimize or gain focus with it.
To fix that issue, when creating the QMainWindow, I used the QApplication.activeWindow() method to get an object to feed the QMainWindow as a parent.
parent = QApplication.activeWindow()
window = MyMainWindow(parent)
If I do this, the QMainWindow will minimize and change focus with Nuke. However, when accessing subwidgets of any widget created with .ui files, it will raise an Exception
Traceback (most recent call last):
...
RuntimeError: Internal C++ object (PySide.QtGui.QPushButton) already deleted.
I'm using a method very similar to this to load the .ui files onto my QWidget classes
Why are the C++ objects being deleted (garbage-collected)? Why does the behavior change when I specify a parent for the QMainWindow? Is there another way to parent the QMainWindow to Nuke so that in minimizes and focuses correctly or a different way to load the .ui files without experiencing this garbage collection issue?
The problem occurs when one of the parent widgets gets garbage-collected because no python object reference exists. For instance:
def create_window():
parent = QApplication.activeWindow()
window = MyMainWindow(parent)
return window
When the function returns, the parent object goes out of scope and the C++ pyobject it represents will eventually get garbage collected. It is strange that this is only a problem for widgets created with .ui files. The solution is to just keep a reference to all the parent objects. I'm using a global variable to store references to them.
GC_PROTECT = []
def create_window():
parent = QApplication.activeWindow()
GC_PROTECT.append(parent)
window = MyMainWindow(parent)
return window
if the window is declared as a Global, it won't get destroyed until the window itself is closed. So you want the reference to the window/widget itself to continue existing somewhere, not necesarily the parent (as mentioned in a previous answer)
def create_window():
parent = QApplication.activeWindow()
# not necesarily the best practice to declare it here, but comfortable
Global window
window = MyMainWindow(parent)
return window

pyqt: A correct way to connect multiple signals to the same function in pyqt (QSignalMapper not applicable)

I've ready many posts on how to connect multiple signals to the same event handler in python and pyqt. For example, connecting several buttons or comboboxes to the same function.
Many examples show how to do this with QSignalMapper, but it is not applicable when the signal carries a parameter, as with combobox.currentIndexChanged
Many people suggest it can be made with lambda. It is a clean and pretty solution, I agree, but nobody mentions that lambda creates a closure, which holds a reference - thus the referenced object can not be deleted. Hello memory leak!
Proof:
from PyQt4 import QtGui, QtCore
class Widget(QtGui.QWidget):
def __init__(self):
super(Widget, self).__init__()
# create and set the layout
lay_main = QtGui.QHBoxLayout()
self.setLayout(lay_main)
# create two comboboxes and connect them to a single handler with lambda
combobox = QtGui.QComboBox()
combobox.addItems('Nol Adyn Dwa Tri'.split())
combobox.currentIndexChanged.connect(lambda ind: self.on_selected('1', ind))
lay_main.addWidget(combobox)
combobox = QtGui.QComboBox()
combobox.addItems('Nol Adyn Dwa Tri'.split())
combobox.currentIndexChanged.connect(lambda ind: self.on_selected('2', ind))
lay_main.addWidget(combobox)
# let the handler show which combobox was selected with which value
def on_selected(self, cb, index):
print '! combobox ', cb, ' index ', index
def __del__(self):
print 'deleted'
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
wdg = Widget()
wdg.show()
wdg = None
sys.exit(app.exec_())
The widget is NOT deleted though we clear the reference. Remove the connection to lambda - it gets deleted properly.
So, the question is: which is the proper way to connect several signals with parameters to a single handler without leaking memory?
It is simply untrue that an object cannot be deleted because a signal connection holds a reference in a closure. Qt will automatically remove all signal connections when it deletes an object, which will in turn remove the reference to the lambda on the python side.
But this implies that you cannot always rely on Python alone to delete objects. There are two parts to every PyQt object: the Qt C++ part, and the Python wrapper part. Both parts must be deleted - and sometimes in a specific order (depending on whether Qt or Python currently has ownership of the object). In addition to that, there's also the vagaries of the Python garbage-collector to factor in (especially during the short period when the interpreter is shutting down).
Anyway, in your specific example, the easy fix is to simply do:
# wdg = None
wdg.deleteLater()
This schedules the object for deletion, so a running event-loop is required for it have any effect. In your example, this will also automatically quit the application (because the object is the last window closed).
To more clearly see what's happening, you can also try this:
#wdg = None
wdg.deleteLater()
app.exec_()
# Python part is still alive here...
print(wdg)
# but the Qt part has already gone
print(wdg.objectName())
Output:
<__main__.Widget object at 0x7fa953688510>
Traceback (most recent call last):
File "test.py", line 45, in <module>
print(wdg.objectName())
RuntimeError: wrapped C/C++ object of type Widget has been deleted
deleted
EDIT:
Here's another debugging example that hopefully makes it even clearer:
wdg = Widget()
wdg.show()
wdg.deleteLater()
print 'wdg.deleteLater called'
del wdg
print 'del widget executed'
wd2 = Widget()
wd2.show()
print 'starting event-loop'
app.exec_()
Output:
$ python2 test.py
wdg.deleteLater called
del widget executed
starting event-loop
deleted
in many cases the parameter carried by signal can be catched in another way, e.g. if an objectName is set for the sending object, so QSignalMapper can be used:
self.signalMapper = QtCore.QSignalMapper(self)
self.signalMapper.mapped[str].connect(myFunction)
self.combo.currentIndexChanged.connect(self.signalMapper.map)
self.signalMapper.setMapping(self.combo, self.combo.objectName())
def myFunction(self, identifier):
combo = self.findChild(QtGui.QComboBox,identifier)
index = combo.currentIndex()
text = combo.currentText()
data = combo.currentData()

Overriding/Reimplementing Slots in PySide

I have almost the exact same question as the one found here:
Override shouldInterruptJavaScript in QWebPage with PySide
In my case though I want to override the copy and paste slots on QLineEdit
import sys
from PySide import QtGui, QtCore
class myLineEdit(QtGui.QLineEdit):
# FIXME: This is not working, the slot is not overriden!
#QtCore.Slot()
def copy(self):
print 'overridden copy event'
App.clipboard().setText('customized text')
return False
#QtCore.Slot()
def paste(self):
print 'overridden paste event'
self.setText('customized text')
return False
if __name__ == "__main__":
App = QtGui.QApplication(sys.argv)
Widget = myLineEdit()
Widget.show()
cmenu = Widget.createStandardContextMenu()
sys.exit(App.exec_())
I'm using Python 2.7.3, with PySide 1.2.2
I don't know if these methods are even supposed to be override-able, but I can't find any documentation that says they shouldn't be.
I also found this page
http://qt-project.org/faq/answer/is_it_possible_to_reimplement_non-virtual_slots
The page explains how certain kinds of slots get pointers set to them by functions that get called when the object is initialized (or something along those lines, I'm not as familiar with the C++).
Following this logic I added the createStandardContextMenu() call above in the hopes that it would reinitialize the slots for at least the context menu, but no luck.
Am I doing something wrong? Or should I try filing a bug report?
You cannot override QLineEdit.copy or QLineEdit.paste in such a way that they will be called internally by Qt.
In general, you can only usefully override or reimplement Qt functions that are defined as being virtual. The Qt Docs will always specify whether this is the case, and for QLineEdit, there are no public slots that are defined in that way.
There is also no easy workaround. There are a lot of different ways in which copy and paste operations (or their equivalents) can be invoked, such as keyboard shortcuts, context menu, drag and drop, etc. It can be done: but it's a lot of work to get complete control over all these sorts of operations. So you need to think carefully about what you're trying to achieve before deciding how to proceed.

Categories