2

I have a parent class which handles opening projects. Projects can be opened from a child window which calls the parent function to handle opening the project. However, when a file-dialog is cancelled from the child window, the entire application exits.

from PyQt5.QtCore import Qt, QDateTime
from PyQt5.QtWidgets import *
from PyQt5 import QtGui

class ParentWindow(QDialog):
    def __init__(self):
        super(ParentWindow, self).__init__()
        self.cw = None

        self.setFixedSize(300, 100)
        self.button = QPushButton('Open')
        self.button.clicked.connect(self.open)

        layout = QHBoxLayout()
        layout.addWidget(self.button)
        self.setLayout(layout)

        self.show()

    def open(self):
        fileDialog = QFileDialog(self, 'Projects')
        fileDialog.setFileMode(QFileDialog.DirectoryOnly)

        if fileDialog.exec():
            self.hide()
            name = fileDialog.selectedFiles()[0]
            if self.cw:
                self.cw.close()
            self.cw = ChildWindow(self, name)

class ChildWindow(QDialog):
    def __init__(self, parent, name):
        super(ChildWindow, self).__init__(parent)
        self.setFixedSize(500, 100)
        self.setWindowTitle(name)

        self.openButton = QPushButton('Open')
        self.openButton.clicked.connect(self.parent().open)

        layout = QHBoxLayout()
        layout.addWidget(self.openButton)
        self.setLayout(layout)

        self.show()

I can't figure out why the program won't return to the child window when cancel is pressed in the file-dialg. Is there a way to keep the parent responsible for opening projects and fix this issue?

ekhumoro
  • 115,249
  • 20
  • 229
  • 336
Evan Brittain
  • 547
  • 5
  • 15
  • I didn't understand your question, could you explain me better – eyllanesc Sep 27 '19 at 05:34
  • A child window creates a QFileDialog instance when a button is pressed. If cancel is selected from the QFileDialog the entire application exits. Pressing cancel from the QFileDialog should return back to the previous screen which is the child window. – Evan Brittain Sep 27 '19 at 05:37

2 Answers2

2

The problem probably resides on the different event timings of both hide and show events: I suppose that, until the open function returns, Qt has not yet "registered" the child as a window that will check against the QApplication.quitOnLastWindowClosed() option, meaning that even if the child window is shown for a fraction of time, it still "thinks" that there is only one window (the parent).

According to your requirements there are two possibilities:

  • use setQuitOnLastWindowClosed(False) on the application instance, remembering to call quit in the CloseEvent of the parent window (or any other window for which you want to quit on close);
  • use a QTimer.singleShot(1, self.hide), which should delay the hiding enough to avoid this problem;

The first solution is usually better and I strongly suggest you to use it.
I'm not even sure that using a one-ms delay would be actually enough to allow the "a new window exists" notification to the application: it might be necessary a higher amount, and that value could also be arbitrary, depending on various conditions (including platform implementation).
According to the source code, as soon as a top level widget is closed it checks against all QApplication.topLevelWidgets(), but according to my tests the list is not immediately updated: the ChildWindow usually "appears" some time after show(), but sometimes (usually <2 ms after) it does not appear in the list at all.

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Don't you mean `setQuitOnLastWindowClosed(False)`? – Heike Sep 27 '19 at 11:40
  • Another option could be to make `ChildWindow` a top level widget instead of a child of `ParentWindow` and store the parent widget as an instance variable. – Heike Sep 27 '19 at 11:44
  • The reason why the child window closes after the file-dialog is cancelled seems to be (1) because its parent is not visible and (2) because it's not treated as a top-level window. As Heike says, the simplest way to solve this is therefore to *not give the child window a parent*. Using `setQuitOnLastWindowClosed(False)` creates its own issues, because there will be no way to quit the application after the child window is closed (since its parent remains hidden), – ekhumoro Sep 27 '19 at 13:45
  • However, none of this really explains why cancelling the file-dialog causes the child window to close. They're completely independent windows, so why should closing one window also close the other? This spooky behaviour seems very counter-intuitive to me, and looks like a bug in Qt (or at least a misfeature). The issue is discussed in [this QT Forum thread](https://forum.qt.io/topic/79266/closing-child-object-causes-parent-to-be-closed), but it doesn't reach any firm conclusions. – ekhumoro Sep 27 '19 at 13:46
  • @ekhumoro In the example code, I commented out the `self.hide()` and tried different combinations of two QTimer.singleShot: one actually hiding the parent window, the other returning showing the result of what the [QWidgetPrivate.close_helper](https://code.woboq.org/qt5/qtbase/src/widgets/kernel/qwidget.cpp.html#_ZN14QWidgetPrivate12close_helperENS_9CloseModeE) does. I suppose that's because of the underlying asynchronous platform events, which would partially explain why the child window sometimes doesn't even show in the `topLevelWidgets` list as I wrote. – musicamante Sep 27 '19 at 17:09
  • @musicamante Have you tried running the OPs code in the way they describe in their comment? All the relevant events happen long after the first call of `open` has returned, so there are no timing issues of any kind. To see the weird behaviour, you need to open the file-dialog again *from the child window itself*, and then cancel it. When `open` is called *for the second time*, the child window is most definitely already in `topLevelWidgets`; and the second call of `self.hide()` is obviously a NOOP, because the parent window is already hidden at that point. – ekhumoro Sep 27 '19 at 18:05
  • @musicamante I now understand exactly what is happening and how to fix it (see my answer). I think this is a perhaps more of a misfeature than a bug. Possibly Qt could treat child windows with hidden parents differently if they happen to be the only remaining window. However, it might be tricky to work out when and if such a window should be kept open, so I suppose explicit is better than implicit. – ekhumoro Sep 27 '19 at 19:28
  • `setQuitOnLastWindowClosed(False)` prevented closing entire application. Thanks – Rahul A Ranger Jan 28 '22 at 07:58
2

Here is a very simple fix:

def open(self):
    fileDialog = QFileDialog(self, 'Projects')
    fileDialog.setAttribute(Qt.WA_QuitOnClose, False)

or even simpler:

def open(self):
    fileDialog = QFileDialog(self.sender(), 'Projects')

The issue here is that whenever a window is closed, Qt checks to see if any other windows should also be closed. In most cases, it will automatically close a window if these two conditions are met:

  1. the WA_QuitOnClose attribute is set, and
  2. there is no parent, or the parent is hidden

Unfortunately, in your example, this is true for both the file-dialog and the child window, which results in both windows being closed. In addition, since quitOnLastWindowClosed is true by default, the application will also automatically quit.

The first fix above works by ensuring at least one window does not have the quit-on-close attribute set, and the second works by ensuring the parent of the file-dialog is always a visible window.

ekhumoro
  • 115,249
  • 20
  • 229
  • 336