PySide application crashes when setting a new widget to QScrollArea

Question:

This is a simplification of an application I wrote.

The application’s main window has a button and a checkbox.
The checkbox resides inside a QScrollArea (via a widget).
The checkbox has a number stating how many times that checkbox was created.
Clicking on the button will open a dialog with a refresh button.

Clicking on the refresh button will set a new widget to the scroll area with a new checkbox.

The checkbox has a context menu that opens the same dialog.
However, when the widget is created using the dialog triggered by the context menu of the checkbox the application crashes with the following error:

2016-08-03 09:22:00.036 Python[17690:408202] modalSession has been
exited prematurely – check for a reentrant call to endModalSession:

Python(17690,0x7fff76dcb300) malloc: * error for object
0x7fff5fbfe2c0: pointer being freed was not allocated
*
set a breakpoint in malloc_error_break to debug

The crash doesn’t happen when clicking on the button to open the dialog and clicking refresh from the dialog.

The crash happens on both Mac and Windows.
I am using Python 2.7.10 with PySide 1.2.4

Context menu of the checkboxMain Window

#!/usr/bin/env python
import sys
from itertools import count
from PySide import QtCore, QtGui

class MainWindow(QtGui.QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.centralwidget = QtGui.QWidget(parent=self)

        self.open_diag_btn = QtGui.QPushButton('Open dialog', parent=self)
        self.open_diag_btn.clicked.connect(self.open_dialog)
        self.scroll_widget = QtGui.QScrollArea(parent=self)

        layout = QtGui.QGridLayout(self.centralwidget)
        layout.addWidget(self.scroll_widget)
        layout.addWidget(self.open_diag_btn)
        self.setCentralWidget(self.centralwidget)
        self.set_scroll_widget()

    def open_dialog(self):
        dialog = Dialog(parent=self)
        dialog.refresh.connect(self.set_scroll_widget)  # Connecting the signal from the dialog to set a new widget to the scroll area
        dialog.exec_()
        # Even if I call the function here, after the dialog was closed instead of using the signal above the application crashes, but only via the checkbox
        # self.set_scroll_widget()

    def set_scroll_widget(self):
        """Replacing the widget of the scroll area with a new one.
           The checkbox in the layout of the widget has an class instance counter so you can see how many times the checkbox was created."""
        widget = QtGui.QWidget()
        layout = QtGui.QVBoxLayout(widget)
        widget.setLayout(layout)
        open_diag_check = RefreshCheckbox(parent=self)
        open_diag_check.do_open_dialog.connect(self.open_dialog)  # Connecting the signal to open the dialog window

        layout.addWidget(open_diag_check)
        self.scroll_widget.setWidget(widget)


class RefreshCheckbox(QtGui.QCheckBox):
    """A checkbox class that has a context menu item which emits a signal that eventually opens a dialog window"""
    do_open_dialog = QtCore.Signal()
    _instance_counter = count(1)

    def __init__(self, *args, **kwargs):
        super(RefreshCheckbox, self).__init__(unicode(self._instance_counter.next()), *args, **kwargs)
        self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
        action = QtGui.QAction(self)
        action.setText("Open dialog")
        action.triggered.connect(self.emit_open_dialog)
        self.addAction(action)

    def emit_open_dialog(self):
        self.do_open_dialog.emit()


class Dialog(QtGui.QDialog):
    """A dialog window with a button that emits a refresh signal when clicked.
       This signal is used to call MainWindow.set_scroll_widget()"""
    refresh = QtCore.Signal()

    def __init__(self, *args, **kwargs):
        super(Dialog, self).__init__(*args, **kwargs)
        self.refresh_btn = QtGui.QPushButton('Refresh')
        self.refresh_btn.clicked.connect(self.do_refresh)
        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.refresh_btn)

    def do_refresh(self):
        self.refresh.emit()


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    mySW = MainWindow()
    mySW.show()
    mySW.raise_()
    app.exec_()
Asked By: Nir

||

Answers:

It looks like Python is trying to delete an object (or one of its child objects) which is no longer there – but quite what causes that to happen is not completely clear to me. The problematic object is the widget set on the scroll-area. If you excplicitly keep a reference to it:

    def set_scroll_widget(self):
        self._old_widget = self.scroll_widget.takeWidget()
        ...

your example will no longer dump core.

I think running the dialog with exec may somehow be the proximal cause of the problem, since this means it will run with its own event-loop (which might have an affect on the order of deletion-related events). I was able to find a better fix for your example by running the dialog with show:

def open_dialog(self):
    dialog = Dialog(parent=self)
    dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose)
    dialog.refresh.connect(self.set_scroll_widget)
    dialog.setModal(True)
    dialog.show()

Doing things this way means it’s no longer necessary to keep an explicit reference to the previous scroll-area widget.

Answered By: ekhumoro