Crash in simple PyQt5 app
-
I have a small experience with python but enough with C++/Qt. For self study i decided to make a simple app and got troubles during developing. I have prepared sandbox which reproduces the crash. Most strange thing for me is how objects are released(sometimes C++ obj is released first then Py wrapper, or vice versa). but i do not know how to set right order for removing.
from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5 import * import sys def ListWidgetCompleterDestroyed(obj:QObject): print("<<>> ListWidgetCompleter C++ obj removed" + str(obj)) class ListWidgetCompleter(QObject): def __init__(self, parent: QLineEdit): QObject.__init__(self, parent) assert parent != None self.destroyed.connect(ListWidgetCompleterDestroyed) self.parent().installEventFilter(self) self.__popup = None self.__maxVisibaleItems = 0 def __del__(self): print("<<>> ListWidgetCompleter py layer deleted") def setPopup(self, listWidget: QListWidget): assert listWidget != None if self.__popup != listWidget and self.__popup != None: self.__popup.deleteLater() self.__popup = listWidget self.__popup.setParent(None) self.__popup.setWindowFlag(Qt.Popup) self.__popup.installEventFilter(self) self.__popup.setFocusProxy(self.parent()) self.__popup.setFocusPolicy(self.parent().focusPolicy()) def setMaxVisibleItems(self, maxItems: int): self.__maxVisibaleItems = maxItems def popup(self): return self.__popup def showPopup(self): lineEdit = self.parent() pos = lineEdit.mapToGlobal(QPoint(0, 0)) print("X " + str(pos.x()) + " Y " + str(pos.y()) + " le.w " + str(lineEdit.width())) self.__popup.setGeometry(pos.x(), pos.y() + lineEdit.height(), lineEdit.width(), 80) if not self.__popup.isVisible(): self.__popup.show() self.__popup.setFocus() def eventFilter(self, o: QObject, e: QEvent): lineEdit: QLineEdit = self.parent() popup_ = self.popup() if lineEdit == o: pass else: if e.type() == QEvent.KeyPress: print("keypress") lineEdit.setFocus() lineEdit.event(e) return False return QObject.eventFilter(self, o, e) def MyLineEditDestroyed(obj:QObject): print("<<>> MyLineEdit C++ obj removed" + str(obj)) class MyLineEdit(QLineEdit): def __init__(self, parent = None): QLineEdit.__init__(self, parent) self.destroyed.connect(MyLineEditDestroyed) def __del__(self): print("<<>> MyLineEdit py layer deleted") def DicViewDestroyed(obj:QObject): print("<<>> DicView C++ obj removed" + str(obj)) class DicView(QWidget): def __init__(self, parent = None): QWidget.__init__(self, parent) self.destroyed.connect(DicViewDestroyed) vbox = QVBoxLayout(self) self.lineEidt = MyLineEdit() vbox.addWidget(self.lineEidt) self.inputCompleter = ListWidgetCompleter(self.lineEidt) self.inputCompleter.setPopup(QWidget()) def __del__(self): print("<<>> DicView py layer deleted") def showEvent(self, event): print("showEvent " + type(self).__name__) QWidget.showEvent(self, event) QTimer.singleShot(1, self.inputCompleter.showPopup) class MyWindow(QWidget): def __init__(self, parent = None): QWidget.__init__(self, parent) self.vbox = QVBoxLayout(self) btn = QPushButton(self) self.vbox.addWidget(btn) self.dicView = DicView(self) self.vbox.addWidget(self.dicView) btn.setText("remove Me") btn.clicked.connect(self.onClick) def onClick(self): self.vbox.removeWidget(self.dicView) self.dicView.deleteLater() self.dicView = None if __name__ == "__main__": app = QApplication(sys.argv) win = MyWindow() win.show() sys.exit(app.exec_()) print('APP exited')
traces:
Python 3.9.1
python main.py
showEvent DicView
X 660 Y 329 le.w 160
<<>> DicView py layer deleted
<<>> DicView C++ obj removed<PyQt5.QtWidgets.QWidget object at 0x0000024C99083CA0>
<<>> MyLineEdit C++ obj removed<PyQt5.QtWidgets.QWidget object at 0x0000024C99083CA0>
<<>> ListWidgetCompleter py layer deleted
Traceback (most recent call last):
File "C:\Users\o.kryvoruchko\Andeksan\System\Python\gui\qt\completer\main.py", line 57, in eventFilter
lineEdit: QLineEdit = self.parent()
RuntimeError: wrapped C/C++ object of type ListWidgetCompleter has been deletedsome ideas how to set a right order or fix crash?
-
@Andeksan
I'm not sure but I observe the following.self.dicView = DicView(self) self.vbox.addWidget(self.dicView) def onClick(self): self.dicView.deleteLater() self.dicView = None
You
deleteLater()
/set toNone
theDicView
widget, but you do not explicitly remove it from thevbox
layout it is on. I wouldself.vbox.removeWidget(self.dicView)
before deleting it. Else I think the layout now still has a deleted widget on it.I don't know if that is the cause of your problem, but worth a try.
Otherwise I suggest you reduce your code to something considerably smaller.
I will say that in a whole PyQt5 application I worked on I only have a single
deleteLater()
. You mostly just don't have to do that, because Python reference counting deals with it.As a hint, you can use
QtWidgets.QApplication.allWidgets()
to discover what widgets are still around, if you want to know what has/has not been destroyed. I wrote the following utility function to tell me this:def debugWidgetsInUse(): # Function to allow to see what widgets are currently existing/in use from Qt # This can be used to help to detect if there are any "leaks" allWidgets = QtWidgets.QApplication.allWidgets() errfunctions.uiLogger.debug("Existing widgets: {}".format(len(allWidgets))) allDialogs = [w for w in allWidgets if isinstance(w, QtWidgets.QDialog)] for dlg in allDialogs: errfunctions.uiLogger.debug("Existing dialog: {} ({}) ({})".format( str(type(dlg)), dlg.windowTitle(), "visible" if dlg.isVisible() else "invisible")) allOrphans = [ w for w in allWidgets if w.parent() is None and not isinstance(w, QtWidgets.QDesktopWidget) and not isinstance(w, QtWidgets.QMainWindow) ] for w in allOrphans: text = w.objectName() if text == "": if hasattr(w, "text"): text = w.text() descendants = [ descendant for descendant in allWidgets if descendant != w and w.isAncestorOf(descendant) ] errfunctions.uiLogger.debug("Orphan widget: {} ({}) ({} descendant(s))".format( str(type(w)), text, len(descendants)))
-
@JonB actually i forgot that my main project has this call self.vbox.removeWidget(self.dicView) before deleteLater. But even with it, sandbox prj still has problem. I have updated my first post. to be more precise.
I will think how to simplify this simple project. for present moment i have not to much ideas.(
I understand that Python should take care about object life. and if remove deleteLater i will get crashes during closing app(case for the main project). As for me better to see immediately what view causes crash.
Also for me interesting how qt python projects deal with that fact that destroying object can be as on python and C++ side in different order. -
ok, at first glance i fixed the crash, now app destroys objects correctly
class ListWidgetCompleter(QObject): def __init__(self, popup: QWidget, widget: QWidget): QObject.__init__(self, None) assert popup != None assert widget != None self.destroyed.connect(ListWidgetCompleterDestroed) # assert isinstance(parent, QLineEdit) self.__maxVisibaleItems = 0 self.__attachPoupToWidget(popup, widget) def __del__(self): print("<<>> ListWidgetCompleter py layer deleted") def __attachPoupToWidget(self, popup: QWidget, widget: QWidget): self.__popup = popup self.__widget = widget self.__popup.setParent(None) self.__popup.setWindowFlag(Qt.Popup) self.__popup.installEventFilter(self) self.__widget.installEventFilter(self) self.__popup.setFocusProxy(self.__widget) self.__popup.setFocusPolicy(self.__widget.focusPolicy()) def setMaxVisibleItems(self, maxItems: int): self.__maxVisibaleItems = maxItems def popup(self): return self.__popup def widget(self): return self.__widget def showPopup(self, rect): pos = self.__widget.mapToGlobal(QPoint(0, 0)) print("X " + str(pos.x()) + " Y " + str(pos.y()) + " le.w " + str(self.__widget.width())) self.__popup.setGeometry(pos.x(), pos.y() + self.__widget.height(), self.__widget.width(), 80) if not self.__popup.isVisible(): self.__popup.show() self.__popup.setFocus() def eventFilter(self, o: QObject, e: QEvent): if self.__widget == o: pass else: if e.type() == QEvent.KeyPress: print("keypress") self.__widget.setFocus() self.__widget.event(e) return False return QObject.eventFilter(self, o, e)
<<>> DicView py layer deleted
<<>> ListWidgetCompleter py layer deleted
<<>> ListWidgetCompleter C++ obj removed<PyQt5.QtCore.QObject object at 0x000002345A1C88B0>
<<>> SuggestionPopup py layer deleted
<<>> SuggestionPopup C++ obj removed<PyQt5.QtWidgets.QWidget object at 0x000002345A1C88B0>
<<>> DicView C++ obj removed<PyQt5.QtWidgets.QWidget object at 0x000002345A194EE0>