12

Is it possible to show a QFileDialog where the user can select a file or a directory, either one?

QFileDialog::getOpenFileName() accepts only files, while QFileDialog::getExistingDirectory() is directories-only, but I need to show a file dialog that can accept both a file or a directory (it makes sense for my program). QFileDialog::​Options didn't have anything promising.

sashoalm
  • 63,456
  • 96
  • 348
  • 677
  • Quick search gives me: http://www.qtcentre.org/threads/34226-QFileDialog-select-multiple-directories?p=220108#post220108 – vahancho Dec 17 '14 at 07:34
  • @vahancho Thanks, I actually need a single file or a single directory, sorry if my wording was a bit ambiguous. – sashoalm Dec 17 '14 at 07:37
  • @vahancho: that seems to be for multiple selection. – lpapp Dec 17 '14 at 07:40
  • @sashoalm: http://stackoverflow.com/questions/6484793/multiple-files-and-folder-selection-in-a-qfiledialog Not sure if it helps though.. – lpapp Dec 17 '14 at 07:44
  • @JoachimPileborg `SetFileMode` doesn't allow combining, it's still only files or only directories, but not both. `dialog.setFileMode(QFileDialog::ExistingFile | QFileDialog::Directory);` won't work. – sashoalm Dec 17 '14 at 07:45
  • @JoachimPileborg: how would that help? There is no option for file and directory, and the enum values are not flags. – lpapp Dec 17 '14 at 07:48
  • @lpapp I'm starting to suspect there's no supported way to do it, it's either Win32 or hacking Qt. – sashoalm Dec 17 '14 at 07:50
  • @sashoalm: I am trying out the python code now. – lpapp Dec 17 '14 at 07:51
  • @sashoalm, maybe it worth to reconsider your application's architecture? Why it needs to select either a directory or a file from the same dialog? – vahancho Dec 17 '14 at 07:51
  • @vahancho If it's a single file, it performs the action on that file. If it's a directory, it performs it on all files in that directory and it's subdirectories. – sashoalm Dec 17 '14 at 07:53
  • @lpapp Of course! Please post it. – sashoalm Dec 17 '14 at 08:03
  • @sashoalm: https://paste.kde.org/pbxrkpolt but it is not working yet for directories when getting the model index. – lpapp Dec 17 '14 at 08:24
  • I think it is not possible very easily. :( Well, good question, upvoted. – lpapp Dec 17 '14 at 08:38
  • @lpapp Why not post the code as an answer? I'm working on other parts in the program but I'll get back to the file dialog and try your code. – sashoalm Dec 17 '14 at 08:41
  • @sashoalm: because it does not work conceptually. :( – lpapp Dec 17 '14 at 08:42
  • @lpapp Well, then an answer saying it's not possible should be valid, at least it might save someone time telling him to not bother trying. Obviously Qt just doesn't support this. Maybe Win32 does, I'm on Windows. – sashoalm Dec 17 '14 at 08:43

4 Answers4

7

QFileDialog currently does not support this. I think the main problem for you here is that the FileMode is not a Q_FLAGS and the values are not power of 2, either, and so, you cannot write this to solve this issue.

setFileMode(QFileDialog::Directory|QFileDialog::ExistingFiles)

To solve this, you would need quite a bit of fiddling, e.g.:

  • Override the open button click operation.

  • Get the "treeview" indices properly for both files and directories.

My attempt below demonstrates the former, but I did not really go as far as solving the second because that seems to involve some more fiddling with the selection model.

main.cpp

#include <QFileDialog>
#include <QApplication>
#include <QWidget>
#include <QTreeWidget>
#include <QPushButton>
#include <QStringList>
#include <QModelIndex>
#include <QDir>
#include <QDebug>

class FileDialog : public QFileDialog
{
    Q_OBJECT
    public:
        explicit FileDialog(QWidget *parent = Q_NULLPTR)
            : QFileDialog(parent)
        {
            setOption(QFileDialog::DontUseNativeDialog);
            setFileMode(QFileDialog::Directory);
            // setFileMode(QFileDialog::ExistingFiles);
            for (auto *pushButton : findChildren<QPushButton*>()) {
                qDebug() << pushButton->text();
                if (pushButton->text() == "&Open" || pushButton->text() == "&Choose") {
                    openButton = pushButton;
                    break;
                }
            }
            disconnect(openButton, SIGNAL(clicked(bool)));
            connect(openButton, &QPushButton::clicked, this, &FileDialog::openClicked);
            treeView = findChild<QTreeView*>();
        }

        QStringList selected() const
        {
            return selectedFilePaths;
        }

    public slots:
        void openClicked()
        {
            selectedFilePaths.clear();
            qDebug() << treeView->selectionModel()->selection();
            for (const auto& modelIndex : treeView->selectionModel()->selectedIndexes()) {
                qDebug() << modelIndex.column();
                if (modelIndex.column() == 0)
                    selectedFilePaths.append(directory().absolutePath() + modelIndex.data().toString());
            }
            emit filesSelected(selectedFilePaths);
            hide();
            qDebug() << selectedFilePaths;
       }

    private:
        QTreeView *treeView;
        QPushButton *openButton;
        QStringList selectedFilePaths;
};

#include "main.moc"

int main(int argc, char **argv)
{
    QApplication application(argc, argv);
    FileDialog fileDialog;
    fileDialog.show();
    return application.exec();
}

main.pro

TEMPLATE = app
TARGET = main
QT += widgets
CONFIG += c++11
SOURCES += main.cpp

Build and Run

qmake && make && ./main
sashoalm
  • 63,456
  • 96
  • 348
  • 677
lpapp
  • 47,035
  • 38
  • 95
  • 127
2

What worked for me was to use:

file_dialog.setProxyModel(nullptr);

as suggested here, or

class FileFilterProxyModel : public QSortFilterProxyModel
{
protected:
    virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
    {
        QFileSystemModel* fileModel = qobject_cast<QFileSystemModel*>(sourceModel());
        return (fileModel!=NULL && fileModel->isDir(sourceModel()->index(sourceRow, 0, sourceParent))) || QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
    }
};
...
FileFilterProxyModel* proxyModel = new FileFilterProxyModel;
file_dialog.setProxyModel(proxyModel);

as suggested here, or

class FileDialog : public QFileDialog
{
    Q_OBJECT
public:
    explicit FileDialog(QWidget *parent = Q_NULLPTR)
        : QFileDialog(parent)
    {
        m_btnOpen = NULL;
        m_listView = NULL;
        m_treeView = NULL;
        m_selectedFiles.clear();

        this->setOption(QFileDialog::DontUseNativeDialog, true);
        this->setFileMode(QFileDialog::Directory);
        QList<QPushButton*> btns = this->findChildren<QPushButton*>();
        for (int i = 0; i < btns.size(); ++i) {
            QString text = btns[i]->text();
            if (text.toLower().contains("open") || text.toLower().contains("choose"))
            {
                m_btnOpen = btns[i];
                break;
            }
        }

        if (!m_btnOpen) return;

        m_btnOpen->installEventFilter(this);
        //connect(m_btnOpen, SIGNAL(changed()), this, SLOT(btnChanged()))
        m_btnOpen->disconnect(SIGNAL(clicked()));
        connect(m_btnOpen, SIGNAL(clicked()), this, SLOT(chooseClicked()));


        m_listView = findChild<QListView*>("listView");
        if (m_listView)
            m_listView->setSelectionMode(QAbstractItemView::ExtendedSelection);

        m_treeView = findChild<QTreeView*>();
        if (m_treeView)
            m_treeView->setSelectionMode(QAbstractItemView::ExtendedSelection);
    }

    QStringList selectedFiles()
    {
        return m_selectedFiles;
    }

    bool eventFilter( QObject* watched, QEvent* event )
    {
        QPushButton *btn = qobject_cast<QPushButton*>(watched);
        if (btn)
        {
            if(event->type()==QEvent::EnabledChange) {
                if (!btn->isEnabled())
                    btn->setEnabled(true);
            }
        }

        return QWidget::eventFilter(watched, event);
    }

public slots:
    void chooseClicked()
    {
        QModelIndexList indexList = m_listView->selectionModel()->selectedIndexes();
        foreach (QModelIndex index, indexList)
        {
            if (index.column()== 0)
                m_selectedFiles.append(this->directory().absolutePath() + "/" + index.data().toString());
        }
        QDialog::accept();
    }

private:
    QListView *m_listView;
    QTreeView *m_treeView;
    QPushButton *m_btnOpen;
    QStringList m_selectedFiles;
};

as suggested here. Credits for the original authors and me.

Adriel Jr
  • 1,357
  • 12
  • 20
2

Quite old question but I think I have a simpler solution than most for the lazy ones. You can connect the signal currentChanged of QFileDialog with a function that dynamically changes the fileMode.

//header
class my_main_win:public QMainWindow  
{
private:
    QFileDialog file_dialog;    
}

//constructor 

my_main_win(QWidget * parent):QMainWindow(parent)
{
    connect(&file_dialog,QFileDialog::currentChanged,this,[&](const QString & str)
        {
        QFileInfo info(str);
        if(info.isFile())
            file_dialog.setFileMode(QFileDialog::ExistingFile);
        else if(info.isDir())
            file_dialog.setFileMode(QFileDialog::Directory);
    });
}

And then simply call file_dialog as you would.

if(file_dialog.exec()){
    QStringList dir_names=file_dialog.selectedFiles():
}
1

Connect to the currentChanged signal and then set the file mode to the currently selected item (directory or file). This is a Python3 implementation:

class GroupFileObjectDialog(QFileDialog):

    def __init__(self, parent):
        super().__init__(parent)
        self.setOption(QFileDialog.DontUseNativeDialog)
        self.setFileMode(QFileDialog.Directory)
        self.currentChanged.connect(self._selected)

    def _selected(self,name):
        if os.path.isdir(name):
            self.setFileMode(QFileDialog.Directory)
        else:
            self.setFileMode(QFileDialog.ExistingFile)

Tested on PyQt 5.14 running on linux / Ubuntu18.04.

gerardw
  • 4,437
  • 33
  • 33