how to close a modal dialog in showEvent?

Viewed 557

I can't believe I have to ask this, but I can't get a PyQt5 modal dialog box to close from the showEvent.

Here is a miniature example program demonstrating the problem. The intended behavior of this test program is since somethingWentTerriblyWrong is set, as soon as btnShowDialog is pressed, the modal dialog should perhaps appear for an instant, if even that long, and then close automatically, but as you can see from the screenshot below this does not happen.

# test.py

from PyQt5.QtWidgets import QApplication, QDialog, QWidget, QLabel, QGridLayout, QPushButton
from PyQt5.Qt import Qt

def main():
    app = QApplication([])
    mainForm = MainForm()
    mainForm.show()
    app.exec()
# end main

class MainForm(QWidget):

    def __init__(self):
        super().__init__()

        self.initUi()

        self.myDialog = MyDialog(self)
        # set that something went wrong in the dialog so it should close immediately in the showEvent
        self.myDialog.somethingWentTerriblyWrong = True
    # end function

    def initUi(self):
        # set default form size and location
        self.setGeometry(400, 400, 400, 275)

        # declare a button
        self.btnShowDialog = QPushButton('show dialog')
        self.btnShowDialog.clicked.connect(self.btnShowDialogClicked)

        # increase the font size
        setFontSize(self.btnShowDialog, 16)

        # declare a layout and add the label to the layout
        self.gridLayout = QGridLayout()
        self.gridLayout.addWidget(self.btnShowDialog)

        # add the layout to the form
        self.setLayout(self.gridLayout)
    # end function

    def btnShowDialogClicked(self):
        retVal = self.myDialog.exec()
        print('retVal = ' + str(retVal))
    # end function

# end class

class MyDialog(QDialog):

    def __init__(self, parent):
        super().__init__(parent)

        self.initUi()

        self.somethingWentTerriblyWrong = False
    # end function

    def initUi(self):
        self.setGeometry(250, 250, 250, 175)

        self.lblDialog = QLabel('label on Dialog')

        # center the label and increase the font size
        self.lblDialog.setAlignment(Qt.AlignCenter)
        setFontSize(self.lblDialog, 15)

        self.gridLayout = QGridLayout()
        self.gridLayout.addWidget(self.lblDialog)

        self.setLayout(self.gridLayout)
    # end function

    def showEvent(self, event):
        super(MyDialog, self).showEvent(event)

        # if something went terribly wrong, close this dialog form
        if self.somethingWentTerriblyWrong:
            print('in if self.somethingWentTerriblyWrong:')
            # self.reject()
            self.close()
        # end if

    # end function

# end class

def setFontSize(widget, fontSize):
    font = widget.font()
    font.setPointSize(fontSize)
    widget.setFont(font)
# end function

if __name__ == '__main__':
    main()

In MyDialog, I would have expected either the self.reject() or self.close() lines to get the dialog to close, however on Ubuntu 18.04 at least the modal dialog turns dark and hangs:

enter image description here

I'm certain execution is getting in the if statement in MyDialog's show event because the terminal output is:

$ python3 test.py 
in if self.somethingWentTerriblyWrong:

What am I doing wrong?

2 Answers

You are prematurely closing the widget, in the showEvent() method the widget is visible but the painting on the device has not yet been done so when calling the close() method you stop the painting, and it is not hidden because the internal flag was not updated generating undefined behavior.

The solution is to invoke the close()method ( or reject() method) an instant after showEvent() after an instant (leaving the time for the internal flags to update correctly) using QTimer or QMetaObject:

from PyQt5.QtCore import Qt, QTimer, QMetaObject

# ...


class MyDialog(QDialog):
    # ...
    def showEvent(self, event):
        super(MyDialog, self).showEvent(event)

        # if something went terribly wrong, close this dialog form
        if self.somethingWentTerriblyWrong:
            print("in if self.somethingWentTerriblyWrong:")
            QTimer.singleShot(0, self.close)
            # or
            # - QMetaObject.invokeMethod(self, "close", Qt.QueuedConnection)
            # - QTimer.singleShot(0, self.reject)
            # - QMetaObject.invokeMethod(self, "reject", Qt.QueuedConnection)

Complete working example based on eyllanesc's answer:

# CloseDialogInShowEvent.py

from PyQt5.QtWidgets import QApplication, QDialog, QWidget, QLabel, QGridLayout, QPushButton
from PyQt5.Qt import Qt
from PyQt5.QtCore import QTimer

def main():
    app = QApplication([])
    mainForm = MainForm()
    mainForm.show()
    app.exec()
# end main

class MainForm(QWidget):

    def __init__(self):
        super().__init__()

        self.initUi()

        self.myDialog = MyDialog(self)
        # set that something went wrong in the dialog so it should close immediately in the showEvent
        self.myDialog.somethingWentTerriblyWrong = True
    # end function

    def initUi(self):
        # set default form size and location
        self.setGeometry(400, 400, 400, 275)

        # declare a button
        self.btnShowDialog = QPushButton('show dialog')
        self.btnShowDialog.clicked.connect(self.btnShowDialogClicked)

        # increase the font size
        setFontSize(self.btnShowDialog, 16)

        # declare a layout and add the label to the layout
        self.gridLayout = QGridLayout()
        self.gridLayout.addWidget(self.btnShowDialog)

        # add the layout to the form
        self.setLayout(self.gridLayout)
    # end function

    def btnShowDialogClicked(self):
        retVal = self.myDialog.exec()
        print('retVal = ' + str(retVal))
    # end function

# end class

class MyDialog(QDialog):

    def __init__(self, parent):
        super().__init__(parent)

        self.initUi()

        self.somethingWentTerriblyWrong = False

        self.tmrClose = QTimer(self)
        self.tmrClose.setInterval(200)
        self.tmrClose.timeout.connect(self.tmrCloseTimeout)
    # end function

    def initUi(self):
        self.setGeometry(250, 250, 250, 175)

        self.lblDialog = QLabel('label on Dialog')

        # center the label and increase the font size
        self.lblDialog.setAlignment(Qt.AlignCenter)
        setFontSize(self.lblDialog, 15)

        self.gridLayout = QGridLayout()
        self.gridLayout.addWidget(self.lblDialog)

        self.setLayout(self.gridLayout)
    # end function

    def showEvent(self, event):
        super(MyDialog, self).showEvent(event)

        # if something went terribly wrong, close this dialog form
        if self.somethingWentTerriblyWrong:
            print('in if self.somethingWentTerriblyWrong:')
            self.tmrClose.start()
        # end if
    # end function

    def tmrCloseTimeout(self):
        self.reject()
    # end function

# end class

def setFontSize(widget, fontSize):
    font = widget.font()
    font.setPointSize(fontSize)
    widget.setFont(font)
# end function

if __name__ == '__main__':
    main()
Related