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

Removing a QGraphicsProxyWidget from QGraphicsScene crashes the app, but only when using a button on a toolbar



  • Hello, I have a medium sized app which had some random crashing problems. The crashes don't print any error messages. After a long investigation, getting rid of a lot of code, now I have a minimal example.

    The app has a QGraphicsView, and I add and remove QWidgets dynamically on the graphics view. The widgets have a toolbar, and the toolbar had a close button on it, which removes and deletes the widget from QGraphicsView. The crashes would randomly happen when I used this close button.

    Here is a complete example (it is a PySide2 program, I haven't tried it with C++):

    import sys
    
    from PySide2.QtCore import Signal
    from PySide2.QtGui import QKeySequence
    from PySide2.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QWidget, QVBoxLayout, QAction, \
        QToolBar
    
    
    class MyWidget(QWidget):
        destroy_myself = Signal()
    
        def __init__(self):
            super().__init__()
    
            layout = QVBoxLayout()
            self.setLayout(layout)
    
            toolbar = QToolBar()
            layout.addWidget(toolbar)
            self._remove_widget_action = QAction(f"Remove Widget", self)
            self._remove_widget_action.setShortcut(QKeySequence("d"))
            self._remove_widget_action.triggered.connect(self._remove_widget_action_cb)
            toolbar.addAction(self._remove_widget_action)
    
        def _remove_widget_action_cb(self):
            self.destroy_myself.emit()
    
    
    class MyGraphicsView(QGraphicsView):
        def __init__(self):
            super().__init__()
    
            self._widget = None
    
            self._scene = QGraphicsScene()
            self.setScene(self._scene)
    
            remove_widget_action = QAction(f"Remove Widget", self)
            remove_widget_action.setShortcut(QKeySequence("a"))
            remove_widget_action.triggered.connect(self._create_widget)
            self.addAction(remove_widget_action)
    
        def _widget_wants_to_be_destroyed(self):
            if self._widget is not None:
                widget = self._widget
                self._scene.removeItem(widget.graphicsProxyWidget())
                self._widget = None
    
        def _create_widget(self):
            if self._widget is None:
                self._widget = MyWidget()
                self._scene.addWidget(self._widget)
                self._widget.destroy_myself.connect(self._widget_wants_to_be_destroyed)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        gv = MyGraphicsView()
        gv.show()
        app.exec_()
    

    OS: Windows 10
    Python: 3.8.6 64-bit
    PySide2: 5.15.2

    You can add a new widget to QGraphicsView using the "a" shortcut button on your keyboard. Then you can remove that widget by clicking on the toolbar button. Do this a few times, and you will eventually crash the program (without any error message).

    Note that you need to remove the widget by using mouse click in order to crash. Weirdly, if you use the "d" shortcut button on your keyboard to remove the widget, the program will not crash.

    My questions are:

    1) Why does it crash when I trigger the action using mouse click on the button?
    2) Why does it NOT crash, when I use the keyboard shortcut for the action?

    Here is another behavior I don't understand: if I implement the closing button, not using a toolbar, but using a regular QPushButton, it does not crash if I press on it via mouse. Here is a complete example, this time implemented using a QPushButton:

    import sys
    
    from PySide2.QtCore import Signal
    from PySide2.QtGui import QKeySequence
    from PySide2.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QWidget, QVBoxLayout, QAction, QPushButton
    
    
    class MyWidget(QWidget):
        destroy_myself = Signal()
    
        def __init__(self):
            super().__init__()
    
            layout = QVBoxLayout()
            self.setLayout(layout)
    
            self._button = QPushButton("Click to Destroy")
            layout.addWidget(self._button)
            self._button.clicked.connect(self._remove_widget_action_cb)
    
        def _remove_widget_action_cb(self):
            self.destroy_myself.emit()
    
    
    class MyGraphicsView(QGraphicsView):
        def __init__(self):
            super().__init__()
    
            self._widget = None
    
            self._scene = QGraphicsScene()
            self.setScene(self._scene)
    
            remove_widget_action = QAction(f"Remove Widget", self)
            remove_widget_action.setShortcut(QKeySequence("a"))
            remove_widget_action.triggered.connect(self._create_widget)
            self.addAction(remove_widget_action)
    
        def _widget_wants_to_be_destroyed(self):
            if self._widget is not None:
                widget = self._widget
                self._scene.removeItem(widget.graphicsProxyWidget())
                self._widget = None
    
        def _create_widget(self):
            if self._widget is None:
                self._widget = MyWidget()
                self._scene.addWidget(self._widget)
                self._widget.destroy_myself.connect(self._widget_wants_to_be_destroyed)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        gv = MyGraphicsView()
        gv.show()
        app.exec_()
    

    The example above uses a QPushButton, instead of a QToolBar+QAction, and it does not crash. So another question:

    3) Why does it not crash when I use QPushButton instead of QToolBar+QAction?

    I found a workaround, which avoids crashes even when using QToolBar+QAction: I call the removeItem() method using a QTimer.singleShot() method. Here is a complete example:

    import sys
    
    from PySide2.QtCore import Signal, QTimer
    from PySide2.QtGui import QKeySequence
    from PySide2.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QWidget, QVBoxLayout, QAction, QToolBar
    
    
    class MyWidget(QWidget):
        destroy_myself = Signal()
    
        def __init__(self):
            super().__init__()
    
            layout = QVBoxLayout()
            self.setLayout(layout)
    
            toolbar = QToolBar()
            layout.addWidget(toolbar)
            self._remove_widget_action = QAction(f"Remove Widget", self)
            self._remove_widget_action.setShortcut(QKeySequence("d"))
            self._remove_widget_action.triggered.connect(self._remove_widget_action_cb)
            toolbar.addAction(self._remove_widget_action)
    
        def _remove_widget_action_cb(self):
            self.destroy_myself.emit()
    
    
    class MyGraphicsView(QGraphicsView):
        def __init__(self):
            super().__init__()
    
            self._widget = None
    
            self._scene = QGraphicsScene()
            self.setScene(self._scene)
    
            remove_widget_action = QAction(f"Remove Widget", self)
            remove_widget_action.setShortcut(QKeySequence("a"))
            remove_widget_action.triggered.connect(self._create_widget)
            self.addAction(remove_widget_action)
    
        def _widget_wants_to_be_destroyed(self):
            if self._widget is not None:
                widget = self._widget
                QTimer.singleShot(0, lambda: self._scene.removeItem(widget.graphicsProxyWidget()))
                self._widget = None
    
        def _create_widget(self):
            if self._widget is None:
                self._widget = MyWidget()
                self._scene.addWidget(self._widget)
                self._widget.destroy_myself.connect(self._widget_wants_to_be_destroyed)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        gv = MyGraphicsView()
        gv.show()
        app.exec_()
    

    This example won't crash. I guess waiting for the control to return to event loop before removing the item helps. So my final question is:

    4) Why does using QTimer.singleShot() helps here?


  • Lifetime Qt Champion

    Hi,

    What if you call widget.deleteLater() rather than setting it as None ?



  • @SGaist said in Removing a QGraphicsProxyWidget from QGraphicsScene crashes the app, but only when using a button on a toolbar:

    Hi,

    What if you call widget.deleteLater() rather than setting it as None ?

    If I replace removeItem() line with deleteLater() it still crashes. If I replace the None assignment line with deleteLater() but keep the removeItem(), it gives an error saying the object is already deleted.

    By the way, does moving the topic to Qt for Python means that this error does not happen in C++?


  • Lifetime Qt Champion

    Did you try to run your application using the debugger to see exactly what happens when the removal fails ?

    @canol said in Removing a QGraphicsProxyWidget from QGraphicsScene crashes the app, but only when using a button on a toolbar:

    By the way, does moving the topic to Qt for Python means that this error does not happen in C++?

    No it just means that you are developing with PySide2 hence it makes more sense to be in the Qt for Python sub-forum. You'll have more eyes from people using Qt with Python here.
    Whether it's related to the C++ implementation needs to be confirmed and even so, it does not hurt if in this sub-forum as it could also be a binding issue and not a problem with the underlying Qt libraries.



  • @SGaist I am using PyCharm and run the program in debug mode but nothing is printed when the program crashes, or it does not throw an exception either. Is there a way to get more info when the program fails?


  • Lifetime Qt Champion

    Since you can reliably trigger it, I would put a Pdb set_trace there and go step by step until if fails. You should then be able to get a stack trace.



  • @SGaist I am very unfamiliar with pdb but I tried it. I put pdb set_trace to that method, and then typed "s"+Enter until it failed. But when it fails, it kills the Python process, so I cannot tell it to print the trace anymore, but it prints this output before killing the process:

    "Process finished with exit code -1073741819 (0xC0000005)"


  • Lifetime Qt Champion

    In that case, you will have to go to the next level and use gdb.

    The Python Wiki shows how pretty nicely.



  • @canol said in Removing a QGraphicsProxyWidget from QGraphicsScene crashes the app, but only when using a button on a toolbar:

    Hello, I have a medium sized app which had some random crashing problems. The crashes don't print any error messages. After a long investigation, getting rid of a lot of code, now I have a minimal example.

    For problems that don't appear to produce any usable output, turning on all QLoggingCategory categories can help. QT_LOGGING_RULES="*.*=true" python test.py should flood the console with information about what Qt is doing prior to the crash.

    In addition, installing a QObject::eventFilter() on the widget or QApplication can provide insight into how far input processing proceeded.

    class Filter(QObject):
        def __init__(self, target, *args, **kwargs):
            super().__init__(*args, **kwargs)
            target.installEventFilter(self)
    
        def eventFilter(self, target, event):
            print("Attempting to deliver {} to {}".format(event.type(), target))
            return super().eventFilter(target, event)
    
    app = QtWidgets.QApplication([])
    filter = Filter(app)
    

    The app has a QGraphicsView, and I add and remove QWidgets dynamically on the graphics view. The widgets have a toolbar, and the toolbar had a close button on it, which removes and deletes the widget from QGraphicsView. The crashes would randomly happen when I used this close button.

    Here is a complete example (it is a PySide2 program, I haven't tried it with C++):

    Porting the example to PyQt5 had crashes for the same input, on every attempt. Maybe PyQt is more aggressive in object reclamation than PySide.

    My questions are:

    **1) Why does it crash when I trigger the action using mouse click on the button?

    The issue appears to be a deletion of the recipient object while there are events for it remaining in the queue. As mentioned in the documentation for QObject::~QObject(), this is a problem.

    In C++, QObject::deleteLater() provides a convenient solution. Unfortunately, PySide and PyQt interfere by deleting the object when the last python reference is removed.

    1. Why does it NOT crash, when I use the keyboard shortcut for the action?**

    Different triggering mechanisms lead to a different sequence of events. If the last event triggers the action, deleting the object is fine. If there's a mouse release or a paint event after, deletion in the slot connected to the action is unsafe.

    The example above uses a QPushButton, instead of a QToolBar+QAction, and it does not crash. So another question:

    3) Why does it not crash when I use QPushButton instead of QToolBar+QAction?

    As with #2, the events delivered are probably different.

    This example won't crash. I guess waiting for the control to return to event loop before removing the item helps. So my final question is:

    4) Why does using QTimer.singleShot() helps here?

    Using QTimer.singleShot() schedules the slot for the next iteration of the event loop, after the currently pending events have been processed. The same thing can be accomplished using a queued connection for the action's slot instead of a 0-timer.