Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

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 deleted

    some 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 to None the DicView widget, but you do not explicitly remove it from the vbox layout it is on. I would self.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.



  • @JonB If deleteLater() is used then it is not necessary to use removeWidget since the layout will be notified if any of the child widgets are removed and then it will remove them first.



  • 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>


Log in to reply