Qt Quick Controls - Filesystem Explorer#

This example demonstrates how to create a modern-looking filesystem explorer with a dark-themed user interface that has a uniform look across all operating systems. Custom Qt Quick Controls have been implemented to provide a clean and intuitive UI for opening and navigating text-files from the filesystem.

Frameless Window#

To maximize the available space, we use a frameless window. The basic functionality, such as minimizing, maximizing, and closing the window, has been moved to a customized MenuBar called MyMenuBar. Users can drag the window thanks to the WindowDragHandler added to the Sidebar and MenuBar.

Customization#

Combining customized animations and colors with QtQuick Controls allows us to easily create custom user interfaces. This example showcases the potential of QtQuick Controls for creating aesthetically pleasing UIs.

With the knowledge gained from this example, developers can apply similar techniques to create their own customized UIs using PySide’s QtQuick Controls.

QtQuickControls Filesystem Explorer Screenshot

Download this example

# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

"""
This example shows how to customize Qt Quick Controls by implementing a simple filesystem explorer.
"""

# Compile both resource files app.qrc and icons.qrc and include them here if you wish
# to load them from the resource system. Currently, all resources are loaded locally
# import FileSystemModule.rc_icons
# import FileSystemModule.rc_app

from PySide6.QtWidgets import QFileSystemModel
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import (QQmlApplicationEngine, QmlElement, QmlSingleton)
from PySide6.QtCore import (Slot, QFile, QTextStream, QMimeDatabase, QFileInfo, QStandardPaths)

import sys


QML_IMPORT_NAME = "FileSystemModule"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
@QmlSingleton
class FileSystemModel(QFileSystemModel):
    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.setRootPath(QStandardPaths.writableLocation(QStandardPaths.HomeLocation))
        self.db = QMimeDatabase()

    # we only need one column in this example
    def columnCount(self, parent):
        return 1

    # check for the correct mime type and then read the file.
    # returns the text file's content or an error message on failure
    @Slot(str, result=str)
    def readFile(self, path):
        if path == "":
            return ""

        file = QFile(path)

        mime = self.db.mimeTypeForFile(QFileInfo(file))
        if 'text' in mime.comment().lower() or any('text' in s.lower() for s in mime.parentMimeTypes()):
            if file.open(QFile.ReadOnly | QFile.Text):
                stream = QTextStream(file).readAll()
                return stream
            else:
                return self.tr("Error opening the file!")
        return self.tr("File type not supported!")


if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    app.setOrganizationName("QtProject")
    app.setApplicationName("File System Explorer")
    engine = QQmlApplicationEngine()
    # Include the path of this file to search for the 'qmldir' module
    engine.addImportPath(sys.path[0])

    engine.loadFromModule("FileSystemModule", "Main")

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec())
module FileSystemModule
Main 1.0 Main.qml
Icon 1.0 qml/Icon.qml
About 1.0 qml/About.qml
MyMenu 1.0 qml/MyMenu.qml
Sidebar 1.0 qml/Sidebar.qml
MyMenuBar 1.0 qml/MyMenuBar.qml
singleton Colors 1.0 qml/Colors.qml
ResizeButton 1.0 qml/ResizeButton.qml
FileSystemView 1.0 qml/FileSystemView.qml
WindowDragHandler 1.0 qml/WindowDragHandler.qml
<RCC>
    <qresource prefix="/qt/qml/FileSystemModule">
        <file>qmldir</file>
        <file>Main.qml</file>
        <file>qml/About.qml</file>
        <file>qml/Colors.qml</file>
        <file>qml/FileSystemView.qml</file>
        <file>qml/Icon.qml</file>
        <file>qml/MyMenu.qml</file>
        <file>qml/MyMenuBar.qml</file>
        <file>qml/ResizeButton.qml</file>
        <file>qml/Sidebar.qml</file>
        <file>qml/WindowDragHandler.qml</file>
    </qresource>
</RCC>
module FileSystemModule
Main 1.0 Main.qml
Icon 1.0 qml/Icon.qml
About 1.0 qml/About.qml
MyMenu 1.0 qml/MyMenu.qml
Sidebar 1.0 qml/Sidebar.qml
MyMenuBar 1.0 qml/MyMenuBar.qml
singleton Colors 1.0 qml/Colors.qml
ResizeButton 1.0 qml/ResizeButton.qml
FileSystemView 1.0 qml/FileSystemView.qml
WindowDragHandler 1.0 qml/WindowDragHandler.qml
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls.Basic
import QtQuick.Layouts
import FileSystemModule

ApplicationWindow {
    id: root
    width: 1100
    height: 600
    visible: true
    flags: Qt.Window | Qt.FramelessWindowHint
    title: qsTr("Qt Quick Controls - File System Explorer")

    property string currentFilePath: ""
    property bool expandPath: false

    menuBar: MyMenuBar {
        rootWindow: root

        infoText: currentFilePath
            ? (expandPath ? currentFilePath
            : currentFilePath.substring(currentFilePath.lastIndexOf("/") + 1, currentFilePath.length))
            : "File System Explorer"

        MyMenu {
            title: qsTr("File")

            Action {
                text: qsTr("Increase Font")
                shortcut: "Ctrl++"
                onTriggered: textArea.font.pixelSize += 1
            }
            Action {
                text: qsTr("Decrease Font")
                shortcut: "Ctrl+-"
                onTriggered: textArea.font.pixelSize -= 1
            }
            Action {
                text: expandPath ? qsTr("Toggle Short Path") : qsTr("Toggle Expand Path")
                enabled: currentFilePath
                onTriggered: expandPath = !expandPath
            }
            Action {
                text: qsTr("Exit")
                onTriggered: Qt.exit(0)
            }
        }

        MyMenu {
            title: qsTr("Edit")

            Action {
                text: qsTr("Cut")
                shortcut: StandardKey.Cut
                enabled: textArea.selectedText.length > 0
                onTriggered: textArea.cut()
            }
            Action {
                text: qsTr("Copy")
                shortcut: StandardKey.Copy
                enabled: textArea.selectedText.length > 0
                onTriggered: textArea.copy()
            }
            Action {
                text: qsTr("Paste")
                shortcut: StandardKey.Paste
                enabled: textArea.canPaste
                onTriggered: textArea.paste()
            }
            Action {
                text: qsTr("Select All")
                shortcut: StandardKey.SelectAll
                enabled: textArea.length > 0
                onTriggered: textArea.selectAll()
            }
            Action {
                text: qsTr("Undo")
                shortcut: StandardKey.Undo
                enabled: textArea.canUndo
                onTriggered: textArea.undo()
            }
        }
    }

    Rectangle {
        anchors.fill: parent
        color: Colors.background

        RowLayout {
            anchors.fill: parent
            spacing: 0

            // Stores the buttons that navigate the application.
            Sidebar {
                id: sidebar
                rootWindow: root

                Layout.preferredWidth: 60
                Layout.fillHeight: true
            }

            // Allows resizing parts of the UI.
            SplitView {
                Layout.fillWidth: true
                Layout.fillHeight: true

                handle: Rectangle {
                    implicitWidth: 10
                    color: SplitHandle.pressed ? Colors.color2 : Colors.background
                    border.color: Colors.color2
                    opacity: SplitHandle.hovered || SplitHandle.pressed ? 1.0 : 0.0

                    Behavior on opacity {
                        OpacityAnimator {
                            duration: 900
                        }
                    }
                }

                // We use an inline component to make a reusable TextArea component.
                // This is convenient when the component is only used in one file.
                component MyTextArea: TextArea {
                    antialiasing: true
                    color: Colors.textFile
                    selectedTextColor: Colors.textFile
                    selectionColor: Colors.selection
                    renderType: Text.QtRendering
                    textFormat: TextEdit.PlainText

                    background: null
                }

                Rectangle {
                    color: Colors.surface1

                    SplitView.preferredWidth: 250
                    SplitView.fillHeight: true

                    StackLayout {
                        currentIndex: sidebar.currentTabIndex

                        anchors.fill: parent

                        // Shows the help text.
                        MyTextArea {
                            readOnly: true
                            text: qsTr("This example shows how to use and visualize the file system.\n\n"
                                + "Customized Qt Quick Components have been used to achieve this look.\n\n"
                                + "You can edit the files but they won't be changed on the file system.\n\n"
                                + "Click on the folder icon to the left to get started.")
                            wrapMode: TextArea.Wrap
                        }

                        // Shows the files on the file system.
                        FileSystemView {
                            id: fileSystemView
                            color: Colors.surface1

                            onFileClicked: (path) => root.currentFilePath = path
                        }
                    }
                }

                // The ScrollView that contains the TextArea which shows the file's content.
                ScrollView {
                    leftPadding: 20
                    topPadding: 20
                    bottomPadding: 20
                    clip: true

                    SplitView.fillWidth: true
                    SplitView.fillHeight: true

                    property alias textArea: textArea

                    MyTextArea {
                        id: textArea
                        text: FileSystemModel.readFile(root.currentFilePath)
                    }
                }
            }
        }
        ResizeButton {}
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls.Basic
import FileSystemModule

ApplicationWindow {
    id: root
    width: 500
    height: 360
    flags: Qt.Window | Qt.FramelessWindowHint
    color: Colors.surface1

    menuBar: MyMenuBar {
        id: menuBar
        implicitHeight: 20
        rootWindow: root
        infoText: "About Qt"
    }

    Image {
        id: logo
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.top: parent.top
        anchors.margins: 20
        source: "../icons/qt_logo.svg"
        sourceSize: Qt.size(80, 80)
        fillMode: Image.PreserveAspectFit
        smooth: true
        antialiasing: true
        asynchronous: true
    }

    TextArea {
        anchors.top: logo.bottom
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        anchors.margins: 20
        antialiasing: true
        wrapMode: Text.WrapAnywhere
        color: Colors.textFile
        horizontalAlignment: Text.AlignHCenter
        readOnly: true
        selectionColor: Colors.selection
        text: qsTr("Qt Group (Nasdaq Helsinki: QTCOM) is a global software company with a strong \
presence in more than 70 industries and is the leading independent technology behind 1+ billion \
devices and applications. Qt is used by major global companies and developers worldwide, and the \
technology enables its customers to deliver exceptional user experiences and advance their digital \
transformation initiatives. Qt achieves this through its cross-platform software framework for the \
development of apps and devices, under both commercial and open-source licenses.")
        background: Rectangle {
            color: "transparent"
        }
    }
    ResizeButton {}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

pragma Singleton
import QtQuick

QtObject {
    readonly property color background: "#23272E"
    readonly property color surface1: "#1E2227"
    readonly property color surface2: "#090A0C"
    readonly property color text: "#ABB2BF"
    readonly property color textFile: "#C5CAD3"
    readonly property color disabledText: "#454D5F"
    readonly property color selection: "#2C313A"
    readonly property color active: "#23272E"
    readonly property color inactive: "#3E4452"
    readonly property color folder: "#3D4451"
    readonly property color icon: "#3D4451"
    readonly property color iconIndicator: "#E5C07B"
    readonly property color color1: "#E06B74"
    readonly property color color2: "#62AEEF"
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic
import FileSystemModule

// This is the file system view which gets populated by the C++ model.
Rectangle {
    id: root

    signal fileClicked(string filePath)

    TreeView {
        id: fileSystemTreeView
        anchors.fill: parent
        model: FileSystemModel
        boundsBehavior: Flickable.StopAtBounds
        boundsMovement: Flickable.StopAtBounds
        clip: true

        property int lastIndex: -1

        Component.onCompleted: fileSystemTreeView.toggleExpanded(0)

        // The delegate represents a single entry in the filesystem.
        delegate: TreeViewDelegate {
            id: treeDelegate
            indentation: 8
            implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250
            implicitHeight: 25

            required property int index
            required property url filePath

            indicator: null

            contentItem: Item {
                anchors.fill: parent

                Icon {
                    id: directoryIcon
                    x: leftMargin + (depth * indentation)
                    anchors.verticalCenter: parent.verticalCenter
                    path: treeDelegate.hasChildren
                        ? (treeDelegate.expanded ? "../icons/folder_open.svg" : "../icons/folder_closed.svg")
                        : "../icons/generic_file.svg"
                    iconColor: (treeDelegate.expanded && treeDelegate.hasChildren) ? Colors.color2 : Colors.folder
                }
                Text {
                    anchors.left: directoryIcon.right
                    anchors.verticalCenter: parent.verticalCenter
                    width: parent.width
                    text: model.fileName
                    color: Colors.text
                }
            }

            background: Rectangle {
                color: treeDelegate.index === fileSystemTreeView.lastIndex
                    ? Colors.selection
                    : (hoverHandler.hovered ? Colors.active : "transparent")
            }

            TapHandler {
                onSingleTapped: {
                    fileSystemTreeView.toggleExpanded(row)
                    fileSystemTreeView.lastIndex = index
                    // If this model item doesn't have children, it means it's representing a file.
                    if (!treeDelegate.hasChildren)
                        root.fileClicked(filePath)
                }
            }
            HoverHandler {
                id: hoverHandler
            }
        }

        // Provide our own custom ScrollIndicator for the TreeView.
        ScrollIndicator.vertical: ScrollIndicator {
            active: true
            implicitWidth: 15

            contentItem: Rectangle {
                implicitWidth: 6
                implicitHeight: 6
                color: Colors.color1
                opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0

                Behavior on opacity {
                    OpacityAnimator {
                        duration: 500
                    }
                }
            }
        }
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Effects

// Custom Component for displaying Icons
Item {
    id: root

    required property url path
    property real padding: 5
    property real size: 30
    property alias iconColor: overlay.colorizationColor
    property alias hovered: mouse.hovered

    width: size
    height: size

    Image {
        id: icon
        anchors.fill: root
        anchors.margins: padding
        source: path
        sourceSize: Qt.size(size, size)
        fillMode: Image.PreserveAspectFit
        smooth: true
        antialiasing: true
        asynchronous: true
    }

    MultiEffect {
        id: overlay
        anchors.fill: icon
        source: icon
        colorization: 1.0
        brightness: 1.0
    }

    HoverHandler {
        id: mouse
        acceptedDevices: PointerDevice.Mouse
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls.Basic
import FileSystemModule

Menu {
    id: root

    background: Rectangle {
        implicitWidth: 200
        implicitHeight: 40
        color: Colors.surface2
    }

    delegate: MenuItem {
        id: menuItem
        implicitWidth: 200
        implicitHeight: 40
        contentItem: Item {
            Text {
                anchors.verticalCenter: parent.verticalCenter
                anchors.left: parent.left
                anchors.leftMargin: 5
                text: menuItem.text
                color: enabled ? Colors.text : Colors.disabledText
            }
            Rectangle {
                anchors.verticalCenter: parent.verticalCenter
                anchors.right: parent.right
                width: 6
                height: parent.height
                visible: menuItem.highlighted
                color: Colors.color2
            }
        }
        background: Rectangle {
            color: menuItem.highlighted ? Colors.active : "transparent"
        }
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic
import FileSystemModule

// The MenuBar also serves as a controller for our Window as we don't use any decorations.
MenuBar {
    id: root

    required property ApplicationWindow rootWindow
    property alias infoText: windowInfo.text

    implicitHeight: 25

    // The top level menus on the left side
    delegate: MenuBarItem {
        id: menuBarItem
        implicitHeight: 25

        contentItem: Text {
            horizontalAlignment: Text.AlignLeft
            verticalAlignment: Text.AlignVCenter
            color: menuBarItem.highlighted ? Colors.textFile : Colors.text
            opacity: enabled ? 1.0 : 0.3
            text: menuBarItem.text
            elide: Text.ElideRight
            font: menuBarItem.font
        }

        background: Rectangle {
            color: menuBarItem.highlighted ? Colors.selection : "transparent"
            Rectangle {
                id: indicator
                width: 0; height: 3
                anchors.horizontalCenter: parent.horizontalCenter
                anchors.bottom: parent.bottom
                color: Colors.color1

                states: State {
                    name: "active"; when: menuBarItem.highlighted
                    PropertyChanges { target: indicator; width: parent.width }
                }

                transitions: Transition {
                    NumberAnimation {
                        properties: "width"
                        duration: 300
                    }
                }

            }
        }
    }

    // The background property contains an information text in the middle as well as the
    // Minimize, Maximize and Close Buttons.
    background: Rectangle {
        color: Colors.surface2
        // Make the empty space drag the specified root window.
        WindowDragHandler { dragWindow: rootWindow }

        Text {
            id: windowInfo
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.verticalCenter: parent.verticalCenter
            color: Colors.text
        }

        component InteractionButton: Rectangle {
            signal action;
            property alias hovered: hoverHandler.hovered

            width: root.height
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            color: hovered ? Colors.background : "transparent"

            HoverHandler { id: hoverHandler }
            TapHandler { onTapped: action() }
        }

        InteractionButton {
            id: minimize

            anchors.right: maximize.left
            onAction: rootWindow.showMinimized()
            Rectangle {
                width: parent.height - 10; height: 2
                anchors.centerIn: parent
                color: parent.hovered ? Colors.iconIndicator : Colors.icon
            }
        }

        InteractionButton {
            id: maximize

            anchors.right: close.left
            onAction: rootWindow.showMaximized()
            Rectangle {
                anchors.fill: parent
                anchors.margins: 5
                border.width: 2
                color: "transparent"
                border.color: parent.hovered ? Colors.iconIndicator : Colors.icon
            }
        }

        InteractionButton {
            id: close

            color: hovered ? "#ec4143" : "transparent"
            anchors.right: parent.right
            onAction: rootWindow.close()
            Rectangle {
                width: parent.height - 8; height: 2
                anchors.centerIn: parent
                color: parent.hovered ? Colors.iconIndicator : Colors.icon
                rotation: 45
                transformOrigin: Item.Center
                antialiasing: true
                Rectangle {
                    width: parent.height
                    height: parent.width
                    anchors.centerIn: parent
                    color: parent.color
                    antialiasing: true
                }
            }
        }
    }

}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick.Controls
import FileSystemModule

Button {
    icon.width: 20; icon.height: 20
    anchors.right: parent.right
    anchors.bottom: parent.bottom
    rightPadding: 3
    bottomPadding: 3

    icon.source: "../icons/resize.svg"
    icon.color: down || checked ? Colors.iconIndicator : Colors.icon

    checkable: false
    display: AbstractButton.IconOnly
    background: null
    onPressed: {
        root.startSystemResize(Qt.BottomEdge | Qt.RightEdge)
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic
import FileSystemModule

Rectangle {
    id: root
    color: Colors.surface2

    required property ApplicationWindow rootWindow
    property alias currentTabIndex: tabBar.currentIndex

    ColumnLayout {
        anchors.fill: root
        anchors.topMargin: 10
        anchors.bottomMargin: 10
        spacing: 10

        // TabBar is designed to be horizontal, whereas we need a vertical bar.
        // We can easily achieve that by using a Container.
        Container {
            id: tabBar

            Layout.fillWidth: true

            // ButtonGroup ensures that only one button can be checked at a time.
            ButtonGroup {
                buttons: tabBar.contentItem.children
                // We have to manage the currentIndex ourselves, which we do by setting it to the
                // index of the currently checked button.
                // We use setCurrentIndex instead of setting the currentIndex property to avoid breaking bindings.
                // See "Managing the Current Index" in Container's documentation for more information.
                onCheckedButtonChanged: tabBar.setCurrentIndex(Math.max(0, buttons.indexOf(checkedButton)))
            }

            contentItem: ColumnLayout {
                spacing: tabBar.spacing

                Repeater {
                    model: tabBar.contentModel
                }
            }

            component SidebarEntry: Button {
                id: sidebarButton
                icon.color: down || checked ? Colors.iconIndicator : Colors.icon
                icon.width: 35
                icon.height: 35
                leftPadding: 8 + indicator.width

                background: null

                Rectangle {
                    id: indicator
                    x: 4
                    anchors.verticalCenter: parent.verticalCenter
                    width: 4
                    height: sidebarButton.icon.width
                    color: Colors.color1
                    visible: sidebarButton.checked
                }
            }

            // Shows help text when clicked.
            SidebarEntry {
                icon.source: "../icons/light_bulb.svg"
                checkable: true
                checked: true

                Layout.alignment: Qt.AlignHCenter
            }

            // Shows the file system when clicked.
            SidebarEntry {
                icon.source: "../icons/read.svg"
                checkable: true

                Layout.alignment: Qt.AlignHCenter
            }
        }

        // This item acts as a spacer to expand between the checkable and non-checkable buttons.
        Item {
            Layout.fillHeight: true
            Layout.fillWidth: true

            // Make the empty space drag our main window.
            WindowDragHandler { dragWindow: rootWindow }
        }

        // Opens the Qt website in the system's web browser.
        SidebarEntry {
            id: qtWebsiteButton
            icon.source: "../icons/globe.svg"
            checkable: false

            onClicked: Qt.openUrlExternally("https://www.qt.io/")
        }

        // Opens the About Qt Window.
        SidebarEntry {
            id: aboutQtButton
            icon.source: "../icons/info_sign.svg"
            checkable: false

            onClicked: aboutQtWindow.visible = !aboutQtWindow.visible
        }
    }

    About {
        id: aboutQtWindow
        visible: false
    }
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import QtQuick
import QtQuick.Controls

// Allows dragging the window when placed on an unused section of the UI.
DragHandler {

    required property ApplicationWindow dragWindow

    target: null
    onActiveChanged: {
        if (active) dragWindow.startSystemMove()
    }
}