Advanced FileDialog in QML

For Windows, Mac and Gnome (i.a. Ubuntu).

Update 2014-01-10

This blog post is work in progress and is adjusted with further research and commits in the sample project.

Introduction

File dialogs to open and save files have been introduced to QML applications in Qt 5.1 and are an important step to use QML not only for mobile apps but for full featured desktop applications as well.

The FileDialog integrates perfectly into QML but misses some critical features. One of them is the ability to set a filename in a save dialog. This is especially important for all kinds of downloads where the file already has a name which the user could use instead of typing in his own.

This leads to the question, how one can use nice cross-platform file dialogs, and which features do they provide.

Interfaces

Time to introduce the contestants:

  • FileDialog the straightforward QML solution
  • QFileDialog::getSaveFileName() the quick way let a dialog pop up in a QWidget application
  • QFileDialog instance, the advanced solution in QWidget applications
  • DefaultFileDialog.qml, a fallback solution for FileDialog written entirely in QML
  • QPlatformFileDialogHelper, a hidden class used by the other top level Qt interfaces.

The challenge

Create a cross-platform file dialog that looks native on Linux, Mac and Windows, has a working modality and allows setting a filename.

Early dropout

Table

We’ll put aside DefaultFileDialog.qml first, because it does utilize the native dialogs of Windows, Mac and Linux. Apart from that it is not easy to access for end-developers, so I didn’t get the chance to test it.

QFileDialog::getSaveFileName() and QFileDialog are made and perfectly suitable for QWidget applications. But in a typical QML application, you have no QWidget available at all. I tried to wrap my QML into a QWidget application, but that’s not possible if you want to use QML ApplicationWindow:
QQuickView only supports loading of root objects that derive from QQuickItem.

Since FileDialog‘s lack of features was the core problem, we need to go with QPlatformFileDialogHelper.

QPlatformFileDialogHelper

Warning: QPlatformFileDialogHelper is a private Qt class, which means it is not properly documented and can change at any time, even in point releases. This really happens. Recently the return type and parameters changed between Qt 5.1 and Qt 5.2.

If you want to have a nice read in the Qt sources, QPlatformFileDialogHelper is defined in Qt Base: src/gui/kernel/qplatformdialoghelper.h.

Using QPlatformFileDialogHelper

This blog post is supported by a minimal Qt project on Github: qml-file-dialog-demo. There you can find the whole code. In this post I’d like to highlight the important things and explain why they work as they work.

Import private headers

First, add QT += core-private gui-private to you project’s .pro file. That is necessary to be able to include the private header files.

Add FileSaveDialog class

I’ll concentrate on creating a dialog to save files. File open works pretty much the same. You can either duplicate most of the code or create you own file dialog which can do both.

Add a C++ class FileSaveDialog that inherits from QQuickItem in filesavedialog.h and filesavedialog.cpp.

This class is responsible for dealing with the dialog.

Get a QWindow that is the dialog’s parent

Having a parent window is important for modality. In QML applications we can get a QQuickWindow, which inherits from QWindow.

QQuickItem *parent = this->parentItem();
Q_ASSERT(parent);

QQuickWindow *window = parent->window();
Q_ASSERT(window);

m_parentWindow = window;

These asserts are just for debugging since I had a hard time to get the proper QWindow object.

Create QPlatformFileDialogHelper

Just copy this ugly monster. I did steal it from the Qt sources like this.

m_dlgHelper = static_cast<QPlatformFileDialogHelper*>(QGuiApplicationPrivate::platformTheme()->createPlatformDialogHelper(QPlatformTheme::FileDialog));

Show dialog

Set options, flags and title before you show the dialog.

m_dlgHelper->setOptions(m_options);
m_dlgHelper->setFilter(); // applyOptions();

Qt::WindowFlags flags = Qt::Dialog;
if (!title().isEmpty()) flags |= Qt::WindowTitleHint;

m_visible = m_dlgHelper->show(flags, m_modality, m_parentWindow);

Hide dialog

Easy:

m_dlgHelper->hide();

Destroy

Straightforward:

if (m_dlgHelper)
    m_dlgHelper->hide();
delete m_dlgHelper;

When dialog is accepted

I.e. a reject signal emitted from QPlatformFileDialogHelper:

  1. close the dialog,
  2. emit rejected to QML.

When dialog is rejected

I.e. a accept signal emitted from QPlatformFileDialogHelper:

  1. hide the dialog,
  2. set the selected file URLs,
  3. emit accepted to QML.

Note: The return value of QPlatformFileDialogHelper‘s selectedFiles() function changed between Qt 5.1 and 5.2. It’s now a list of QUrls. Thus the sample project is only compatible with Qt 5.2+.

Dialog option

In my example I go for the following options, but there is room for customization.

m_options->setFileMode(QFileDialogOptions::AnyFile);
m_options->setAcceptMode(QFileDialogOptions::AcceptSave);
m_options->setWindowTitle(title());

Select directory and filename

I want the home directory to be the default place to save the file. This could very well be set by a QML property, if you want.

Mac: Set filename incl. directory via setInitiallySelectedFiles(). Once the dialog is created, you can not change the filename from C++ anymore.

#ifdef Q_OS_OSX
QString initialSelection = QFileInfo(QDir::homePath(), filename()).absoluteFilePath();
qDebug() << "Initial file:" << initialSelection;
m_options->setInitiallySelectedFiles(QList<QUrl>() << QUrl::fromLocalFile(initialSelection));
#endif

Windows: Set filename via setInitiallySelectedFiles() and let Windows choose the directory. Default directory: C:\\Users\XYZ\Downloads

#ifdef Q_OS_WIN
qDebug() << "Initial filename:" << filename();
m_options->setInitiallySelectedFiles(QList<QUrl>() << QUrl::fromLocalFile(filename()));
#endif

Gnome: In theory, the default filename in the save dialog can just be set using your property filename:

m_dlgHelper->selectFile(filename());

But there is a bug in Qt when setting the filename using GTK (i.e. Gnome, i.a. in Ubuntu). Thus I disable setting the filename for all Linux distributions and just set a default directory.

#ifdef Q_OS_LINUX
qDebug() << "Initial directory:" << QDir::homePath();
m_dlgHelper->setDirectory(QUrl::fromLocalFile(QDir::homePath()));
#endif

Expose FileSaveDialog to QML

In your main.cpp include the headers

#include "filesavedialog.h"

and below creating the Application or QGuiApplication register the type

qmlRegisterType<FileSaveDialog>("MyModules", 1, 0, "FileSaveDialog");

Now the dialog can be used similar in QML, similar to FileDialog:

import QtQuick 2.1
import QtQuick.Controls 1.0
import MyModules 1.0

ApplicationWindow {
    title: "Hello World"
    width: 600
    height: 300

    FileSaveDialog {
        id: saveFile
        title: "Save file"
        filename: "download.png"

        onAccepted: {
            output.text = "File selected: " + saveFile.fileUrl
        }
        onRejected: {
            output.text = "File selected: –"
        }
    }

    Column {
        Button {
            text: "Select File"
            onClicked: { saveFile.open(); }
        }

        Text { id: output }
    }
}

Credits

Thanks to my co-founder Daniel for endless source code reading and constructive discussions that finally made this solution possible.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.