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.
# 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: StandardKey.ZoomIn
onTriggered: textArea.font.pixelSize += 1
}
Action {
text: qsTr("Decrease Font")
shortcut: StandardKey.ZoomOut
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)
shortcut: "Ctrl+Q"
}
}
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()
}
}