Qt Forum

    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    • Unsolved

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

    Qt for Python
    qgraphicsview qgraphicsproxyw crash qtoolbar
    3
    9
    443
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • C
      canol last edited by

      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?

      jeremy_k 1 Reply Last reply Reply Quote 0
      • SGaist
        SGaist Lifetime Qt Champion last edited by

        Hi,

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

        Interested in AI ? www.idiap.ch
        Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

        C 1 Reply Last reply Reply Quote 0
        • C
          canol @SGaist last edited by

          @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++?

          1 Reply Last reply Reply Quote 0
          • SGaist
            SGaist Lifetime Qt Champion last edited by

            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.

            Interested in AI ? www.idiap.ch
            Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

            C 1 Reply Last reply Reply Quote 0
            • C
              canol @SGaist last edited by

              @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?

              1 Reply Last reply Reply Quote 0
              • SGaist
                SGaist Lifetime Qt Champion last edited by

                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.

                Interested in AI ? www.idiap.ch
                Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

                C 1 Reply Last reply Reply Quote 0
                • C
                  canol @SGaist last edited by

                  @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)"

                  1 Reply Last reply Reply Quote 0
                  • SGaist
                    SGaist Lifetime Qt Champion last edited by

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

                    The Python Wiki shows how pretty nicely.

                    Interested in AI ? www.idiap.ch
                    Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

                    1 Reply Last reply Reply Quote 1
                    • jeremy_k
                      jeremy_k @canol last edited by

                      @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.

                      Asking a question about code? http://eel.is/iso-c++/testcase/

                      1 Reply Last reply Reply Quote 1
                      • First post
                        Last post