Search code examples
pyside6qfiledialog

PySide6 Recommended Way to Use QFileDialog


The PySide6 QDialog.exec() docs state to avoid using exec():

Avoid using this function; instead, use open(). Unlike , open() is asynchronous, and does not spin an additional event loop. This prevents a series of dangerous bugs from happening (e.g. deleting the dialog’s parent while the dialog is open via ). When using open() you can connect to the finished() signal of QDialog to be notified when the dialog is closed.

open() is a virtual function, but I don't believe it is pure virtual since I can call it directly on any subclass to immediately open the dialog.

However, QFileDialog.open(receiver, member) is a bit of a mystery. It connects either the filesSelected() or fileSelected() signal (depending on the fileMode) to

a slot specified by receiver and member

and

The signal will be disconnected from the slot when the dialog is closed.

Considering the above, is the correct (i.e. recommended) way to use QFileDialog like so:

from qtpy import QtCore, QtWidgets


class MyWindow(QtWidgets.QMainWindow):

    def __init__(self) -> None:
        QtWidgets.QMainWindow.__init__(self)

        self.dialog = QtWidgets.QFileDialog(self)
        
        self.dialog.setFileMode(QtWidgets.QFileDialog.Directory)
        self.dialog.setWindowTitle('Open folder...')
        
        self.dialog.finished.connect(self.on_finished)


    @QtCore.Slot(QtWidgets.QDialog.DialogCode)
    def on_finished(
        self,
        result: QtWidgets.QDialog.DialogCode,
    ) -> None:
        if result == QtWidgets.QDialog.Accepted:
            print('Accepted')
        else:  # QtWidgets.QDialog.Rejected
            print('Rejected')

if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    
    window = MyWindow()
    window.show()

    window.dialog.open()

    app.exec()

or is QFileDialog.open(receiver, member) supposed to be used? If so, how does one use receiver and member?

NOTE: I'm aware the slot decorator isn't strictly necessary in PySide6, but I add it since it allows me to see at a glance which of my methods are slots vs. just methods.


Solution

  • TL;DR: exec(), open(), and static functions can all be used. Which one you choose depends on your use case, which primarily has to do with whether the dialog is application or window modal. exec() is application modal and open() window modal, the former being subject to bugs if the dialog is able to delete its parent while the dialog is open. To use receiver and member requires old-style signal/slot syntax, which is demonstrated below.

    "Correct" way to use QFileDialog?

    Per @musicamante's comment, there is no "correct" way and using either exec() or the static functions are acceptable. For example, the PySide6 QFileDialog docs state

    The easiest way to create a QFileDialog is to use the static functions.

    and then again, the PySide6 docs also state

    The most common way to display a modal dialog is to call its exec() function.

    The docs include examples that use exec(), and in fact, if you review the QFileDialog C++ source code, you will see that most of these static methods ultimately call exec().

    Hence, how QFileDialog is used depends on one's needs:

    1. On Windows and MacOS, static functions return a native file dialog, which keeps the look and feel consistent across OS's, but limits you to native functionality unless you pass DontUseNativeDialog, in which case a QFileDialog is returned:

    By default, a platform-native file dialog will be used if the platform has one. In that case, the widgets which would otherwise be used to construct the dialog will not be instantiated, so related accessors such as layout() and itemDelegate() will return null. Also, not all platforms show file dialogs with a title bar, so be aware that the caption text might not be visible to the user. You can set the DontUseNativeDialog option to ensure that the widget-based implementation will be used instead of the native dialog.

    1. You must decide whether or not you want the dialog to be modal (application or window) or non-modal and if you need to do things like delete the parent widget during execution of the dialog. On Windows, the static functions spin a blocking modal event loop that does not dispatch QTimers. Quite literally, open() behaves in a window modal fashion, and the docs corroborate this.

    Thus, I argue the most important functional characteristic to consider when choosing whether to use exec() or open() is the modality of the dialog and whether or not widgets can be deleted/closed when it is open.

    open(): How Does One Use receiver and member?

    As mentioned in the question, the wording of the QFileDialog.open docs is confusing:

    The specific signal depends is filesSelected() if fileMode is ExistingFiles and fileSelected() if fileMode is anything else.

    This is saying receiver is passed either a list of files or a single file (depending on the dialog file mode), but the receiver/member nomenclature may feel odd to those of us who do not recall the old-style signal/slot syntax, which more closely mirrors how signals and slots are connected in C++ (see C++ GUI Programming with Qt4, Ch. 2, section "Signals and Slots in Depth"):

    connect(sender, SIGNAL(signal), receiver, SLOT(slot));
    

    Indeed, all QFileDialog.open() does is

    • create a signal containing the file or files to send,
    • connect the signal to the slot specified by receiver and member,
    • set the signal and slot to be disconnected when the dialog is closed, and
    • call QDialog.open()

    Hence, using QFileDialog.open() with receiver and member requires the old-style slot/signal syntax with the SLOT macro and @Slot decorator. Without the macro, Qt will issue the warning:

    qt.core.qobject.connect: QObject::connect: Use the SLOT or SIGNAL macro to connect MyWindow::on_finished()
    

    Without the decorator, Qt will complain:

    qt.core.qobject.connect: QObject::connect: No such slot MyWindow::on_finished()
    

    Example:

    from __future__ import annotations
    
    from qtpy import QtCore, QtWidgets
    
    
    class MyWindow(QtWidgets.QMainWindow):
        def __init__(self) -> None:
            QtWidgets.QMainWindow.__init__(self)
    
            self.dialog = QtWidgets.QFileDialog(self)
    
            self.dialog.setFileMode(QtWidgets.QFileDialog.Directory)
            self.dialog.setWindowTitle('Open folder...')
    
        @QtCore.Slot()
        def on_finished(self) -> None:
            for path in self.dialog.selectedFiles():
                print(path)
    
    
    if __name__ == '__main__':
        app = QtWidgets.QApplication([])
    
        window = MyWindow()
        window.show()
    
        window.dialog.open(window, QtCore.SLOT('on_finished()'))
    
        app.exec()
    
    

    Since QFileDialog.open(receiver, member) calls QDialog.open() under the hood, you can use that instead. The benefit is this does not require the old-style syntax, with the caveat that you are responsible for connecting signals and properly disconnecting them when the dialog closes.