Can QFileDialog in Qt5 be changed to a lineEdit?

63 Views Asked by At

I'm wondering if the QFileDialog class in Qt5 can be modified to change the 'Look in:' dropdown to a Line Edit so that the path can be pasted directly without having to select each folder individually

import sys
from PyQt5.QtWidgets import QApplication, QFileDialog, QWidget, QVBoxLayout, QPushButton, QLabel

class FileDialogExample(QWidget):
    def __init__(self):
        super(FileDialogExample, self).__init__()

        self.initUI()

    def initUI(self):
        layout = QVBoxLayout()

        self.label = QLabel('Selected File: ')
        layout.addWidget(self.label)

        btnOpenFileDialog = QPushButton('Open File Dialog', self)
        btnOpenFileDialog.clicked.connect(self.showDialog)
        layout.addWidget(btnOpenFileDialog)

        self.setLayout(layout)

    def showDialog(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog

        fileName, _ = QFileDialog.getOpenFileName(self, "Open File", "", "All Files (*);;Text Files (*.txt)", options=options)

        if fileName:
            self.label.setText(f'Selected File: {fileName}')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = FileDialogExample()
    ex.show()
    sys.exit(app.exec_())

File dialog

I'm not sure if it can be done. I've tried searching for information, but haven't found anything yet.

2

There are 2 best solutions below

0
Alexander Trotsenko On

I think you can't change it.

But by default, Qt uses a platform-native file dialog. And at least a Windows file dialog has the behavior you want. Remove this line in your code to get back to the default file dialog:

options |= QFileDialog.DontUseNativeDialog
0
musicamante On

This cannot be directly achieved using the static functions of QFileDialog, because they internally create the dialog and no direct access to that widget is allowed.

There are two possible options though, as long as the custom Qt file dialog is used (with the DontUseNativeDialog option).

The concept is based on the fact that the non-native QFileDialog is a standard Qt widget with an explicit UI set (similarly to what we get using Designer), all of them have proper object names and are then reachable using findChild().
The "look in" combo is a standard QComboBox, which can be made editable, thus allowing text input. Since it's not natively editable, and QFileDialog internally sets its model as a flattened list (it's not a real tree), we need to do further work.

An editable QComboBox contains an inner QLineEdit, which, in turn, provides a returnPressed signal, so we can connect it to the setDirectory() function of QFileDialog.

Use a custom QFileDialog instance

This is the more logical approach, but it requires creating the dialog from scratch, either in the function that would open the dialog, or as a subclass.

The subclass approach is probably more appropriate, since it allows reusability. I will provide a very basic example, leaving the rest to the reader:

class CustomFileDialog(QFileDialog):
    def __init__(self, parent=None, caption='', directory='', filter=''):
        super().__init__(parent, caption, directory, filter)

        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        self.setOptions(options)

        lookInCombo = self.findChild(QComboBox, 'lookInCombo')
        lookInCombo.setEditable(True)
        lookInEdit = lookInCombo.lineEdit()
        lookInEdit.returnPressed.connect(lambda: 
            self.setDirectory(lookInEdit.text()))

    def getOpenFileName(self):
        self.setFileMode(self.ExistingFile)
        self.setAcceptMode(self.AcceptOpen)
        if super().exec():
            self.deleteLater()
            return self.selectedFiles()[0]
        return ''


class FileDialogExample(QWidget):
    ...
    def showDialog(self):
        fd = CustomFileDialog(
            self, "Open File", "", "All Files (*);;Text Files (*.txt)")

        fileName = fd.getOpenFileName()

        if fileName:
             self.label.setText(f'Selected File: {fileName}')

Other than having to create a dedicated QFileDialog subclass for this, the drawback is that it doesn't provide the standardized static methods, so you have to provide all of them on your own.

Use a QObject as event filter

This approach is a bit more obscure and requires some ingenuity, but it has the great benefit of allowing a standardized interface that will automatically work with all predefined QFileDialog static methods.

It works by creating a temporary "watcher" (a QObject subclass) that installs itself as an event filter onto the QApplication, waits for a new QFileDialog Show event, and then sets up everything we need:

  1. create an instance of the watcher, right before calling the QFileDialog static function (this is mandatory, and every static function call requires a separate watcher);
  2. the watcher installs itself as an event filter for the QApplication;
  3. when the watcher intercepts a Show event from a QFileDialog, it calls a function that sets up all required aspects:
    • removes itself as the event filter;
    • reparents itself to the dialog, so that it gets destroyed along with it;
    • get the "look in" combo;
    • make it editable;
    • get the line edit of the combo, and connect its returnPressed signal, similarly to the QFileDialog subclass explained above;

In the following example I also improved the connected function, so that it will check if the inserted directory is valid and eventually resets the focus to the main widgets of the file dialog (the "file name" edit, or the view showing the directory contents).

class FileDialogWatcher(QObject):
    dialog = lastFocus = None
    focusChildren = ()
    def __init__(self):
        app = QApplication.instance()
        # make the app the parent, so that we don't even need a reference
        super().__init__(app)
        app.installEventFilter(self)
        app.focusChanged.connect(self.storeFocus)        

    def eventFilter(self, obj, event):
        if isinstance(obj, QFileDialog) and event.type() == event.Show:
            self.fixDialog(obj)
        return super().eventFilter(obj, event)

    def storeFocus(self, old, new):
        if new in self.focusChildren:
            self.lastFocus = new

    def fixDialog(self, dialog):
        QApplication.instance().removeEventFilter(self)

        if not dialog.options() & QFileDialog.DontUseNativeDialog:
            self.deleteLater()
            return

        # ensure that this object will be destroyed along with the dialog
        self.setParent(dialog)

        lookInCombo = dialog.findChild(QComboBox, 'lookInCombo')
        lookInCombo.setEditable(True)
        lookInEdit = lookInCombo.lineEdit()

        fileNameEdit = dialog.findChild(QLineEdit, 'fileNameEdit')
        stack = dialog.findChild(QStackedWidget, 'stackedWidget')
        treeView = stack.findChild(QTreeView, 'treeView')
        listView = stack.findChild(QListView, 'listView')
        self.focusChildren = (fileNameEdit, treeView, listView)

        def setPath():
            path = lookInEdit.text()
            qdir = QDir(path)
            if not qdir.exists():
                lookInEdit.setText(dialog.directory().absolutePath())
                return
            dialog.setDirectory(qdir)
            if dialog.directory() == qdir and self.lastFocus:
                self.lastFocus.setFocus()

        lookInEdit.returnPressed.connect(setPath)


class FileDialogExample(QWidget):
    ...
    def showDialog(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog

        FileDialogWatcher() # no need for a reference
        fileName, _ = QFileDialog.getOpenFileName(
            self, "Open File", "", "All Files (*);;Text Files (*.txt)", 
            options=options
        )

        if fileName:
            self.label.setText('Selected File: {}'.format(fileName))