PySide : Children objects not destroyed on close of parent object - python

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.

Related

PyQt5 Memory Management

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()

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)

My new python gui window opened from another window exits as soon as it opens.How do I fix this

I have written python pyqt code to open a new window with a label from another window on a button click. The issue is ,new window exits as soon as it opens.How do i fix this.
The code I wrote is
import sys
from PyQt4 import QtGui,QtCore
class Window(QtGui.QWidget):
def __init__(self):
super(Window,self).__init__()
self.btn=QtGui.QPushButton('button',self)
self.btn.clicked.connect(display)
self.show()
class display(QtGui.QWidget):
def __init__(self):
super(display,self).__init__()
self.lab=QtGui.QLabel()
self.lab.setText("hi")
self.show()
def main():
App=QtGui.QApplication(sys.argv)
Gui=Window()
sys.exit(App.exec_())
main()
You need to keep a reference to the QWidget object for your second window. Currently when you click the button, the clicked signal is fired and it calls disp1. That creates the widget, but then it is immediately garbage collected.
Instead do this to keep a reference:
import sys
from PyQt4 import QtGui,QtCore
class Window(QtGui.QWidget):
def __init__(self):
super(Window,self).__init__()
self.btn=QtGui.QPushButton('button',self)
self.btn.clicked.connect(self.open_new_window)
self.show()
def open_new_window(self):
# creates the window and saves a reference to it in self.second_window
self.second_window = disp1()
class displ(QtGui.QWidget):
def __init__(self):
super(displ,self).__init__()
self.lab=QtGui.QLabel()
self.lab.setText("hello")
self.show()
def main():
App=QtGui.QApplication(sys.argv)
Gui=Window()
sys.exit(App.exec_())
main()
When passing a function as parameter, maybe it's better not to include the parentheses? Try
sys.exit(App.exec_)
Instead of
sys.exit(App.exec_())

How to accept close event of MainWindow when loading it with QUiLoader()?

How to receive close event in following code?
class Main(QMainWindow):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
self.view = QUiLoader().load("sample.ui", self)
self.view.show()
def closeEvent(self, e):
print "close event recieved"
def main():
app = QApplication(sys.argv)
a=Main()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
If I convert sample.ui to sample.py using pyside-uic and importing this into main.py then I was able to receive close event.
from sample import Ui_MainWindow
class Main(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
self.setupUi(self)
def closeEvent(self, e):
print "close event recieved"
app = QApplication(sys.argv)
a=Main()
a.show()
sys.exit(app.exec_())
The second example works because it effectively becomes a subclass of the top-level class from Qt Designer. By contrast, the first example uses composition rather than subclassing, which puts all the gui elements inside an internal namespace. The Main class is just a container that acts as the parent of the view widget, and is never actually shown (which in turn means it doesn't receive any close events).
In PyQt, the uic module has several funtions which allow you to work around these issues, but there is currently nothing like that in PySide. Instead, you have to roll your own function. See this answer for an explanation of how to do that.
Alternatively, you could change the top-level class in Qt Designer to a QWidget, and then make view the central widget of your Main class. This is a lot less flexible than the above method, though.

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