Modular QML Architecture in Qt 6: Component Organization, Resource Management, and Singleton Patterns
Module Definition and Scoping
In modern Qt 6 development, a CMake project inherently functions as a QML module. The recommended approach replaces legacy qmldir management with qt_add_qml_module, which centralizes configuration and automatically generates module metadata during the build process.
Module paths are organized hierarchically but remain logically parallel at runtime. Componants declared within the same module share the same import namespace.
Intra-Module Component Resolution
When referencing components within a single module, naming conventions require that all QML file names begin with an uppercase letter. Resolution follows three primary patterns:
Implicit Directory Access
If target components reside in the exact same directory as the consumer, no import declaration is necessary. They are resolved directly by filename.
project/
├─ CMakeLists.txt
├─ Dashboard.qml
└─ StatCard.qml
// Inside Dashboard.qml
Column {
spacing: 10
StatCard {
anchors.horizontalCenter: parent.horizontalCenter
title: "Active Users"
}
}
Relative Path Imports
For components located in subdirectories, a relative import statement aggregates all valid QML files within that folder.
project/
├─ CMakeLists.txt
├─ AppWindow.qml
└─ navigation/
├─ TopRail.qml
└─ SideDrawer.qml
import QtQuick
import "./navigation"
ApplicationWindow {
id: mainShell
width: 640
height: 480
visible: true
SideDrawer { anchors.left: parent.left }
}
Module Namespace Imports
When a qmldir descriptor is present, the directory transforms into an isolated module namespace. Consumers must explicitly import the defined module URI.
// navigation/qmldir
module App.Navigation
TopRail 1.0 TopRail.qml
SideDrawer 1.0 SideDrawer.qml
import QtQuick
import App.Navigation 1.0
ApplicationWindow {
width: 640
height: 480
visible: true
TopRail { anchors.top: parent.top }
}
The same outcome is achieved via CMake configuration, which eliminates manual qmldir maintenance:
qt_add_qml_module(Application
URI App.Navigation
VERSION 1.2
QML_FILES
navigation/TopRail.qml
navigation/SideDrawer.qml
)
Inter-Module Dependencies
Larger applications separate concerns into distinct modules. Cross-module communication relies entirely on unique URIs and versioned imports.
project/
├─ CMakeLists.txt
├─ AppWindow.qml
├─ widgets/
│ ├─ ProgressBar.qml
│ └─ ToggleSwitch.qml
└─ dialogs/
└─ ConfirmationSheet.qml
Each subdirectory declares its own module descriptor or is registered via separate CMake calls:
qt_add_qml_module(Application
UI Widgets 1.0
QML_FILES widgets/ProgressBar.qml widgets/ToggleSwitch.qml
)
qt_add_qml_module(Application
UI Dialogs 1.0
QML_FILES dialogs/ConfirmationSheet.qml
)
import QtQuick
import UI.Widgets 1.0
import UI.Dialogs 1.0
ApplicationWindow {
width: 640
height: 480
visible: true
ProgressBar { anchors.bottom: parent.bottom }
ConfirmationSheet { anchors.centerIn: parent }
}
Note: Even within the same parent module, singletons or namespaced components require an explicit import statement matching their registered URI to resolve correctly.
Resource Deployment Strategies
Qt provides two primary mechanisms for bundling QML and associated assets, each serving different development and deployment phases.
Filesystem-Driven Loading
During development, QML files remain on the host filesystem. The engine watches for changes, enabling immediate hot-reloading.
qt_add_qml_module(CoreApp
URI App.Core
VERSION 1.0
QML_FILES main_view.qml config_panel.qml
)
C++ initialization uses module URI rather than direct file paths:
QQmlApplicationEngine engine;
engine.loadFromModule("App.Core", "MainView");
Assets referenced in QML use relative or absolute disk paths:
Image { source: "assets/logo_dark.png" }
Binary Resource Compilation
For production releases, assets are compiled into the executable using the Qt Resource Compiler (qrc). Qt treats the resource tree as a virtual filesystem, preserving relative path resolution.
<RCC>
<qresource prefix="/app_bundle">
<file>screens/SetupScreen.qml</file>
<file>images/background.jpg</file>
</qresource>
</RCC>
qt_add_qml_module(CoreApp
URI App.Core
VERSION 1.0
QML_FILES screens/SetupScreen.qml
RESOURCES bundle.qrc
)
Consumers access compiled assets using the qrc: scheme:
import QtQuick
import App.Core 1.0
ApplicationWindow {
id: launcher
Rectangle {
anchors.fill: parent
color: "transparent"
Image {
source: "qrc:/app_bundle/images/background.jpg"
anchors.fill: parent
}
}
}
Implementing QML Singletons
Global state or utility objects in QML are typically implemented as singletons. Registration can occur from C++ or be handled natively within the QML build system.
C++ Registration
The engine exposes a QML file as a singleton type via the core registration API:
qmlRegisterSingletonType(QUrl("qrc:/config/GlobalPrefs.qml"),
"System.Settings", 1, 0, "GlobalPrefs");
Native QML Singleton
Using pragma Singleton combined with a qmldir entry allows pure QML definitions without C++ boilerplate.
project/
├─ CMakeLists.txt
├─ AppWindow.qml
└─ prefs/
└─ GlobalPrefs.qml
pragma Singleton
import QtQuick
QtObject {
property bool darkThemeEnabled: true
property int primaryFontSize: 14
function resetToDefaults() {
darkThemeEnabled = true
primaryFontSize = 14
}
}
Descriptor entry:
module System.Settings
singleton GlobalPrefs 1.0 GlobalPrefs.qml
import QtQuick
import System.Settings 1.0
ApplicationWindow {
visible: true
Text {
anchors.centerIn: parent
font.pointSize: GlobalPrefs.primaryFontSize
text: GlobalPrefs.darkThemeEnabled ? "Night Mode" : "Day Mode"
}
}
The CMake equivalent requires setting a file property before module declaration:
set_source_files_properties(prefs/GlobalPrefs.qml
PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
qt_add_qml_module(CoreApp
URI System.Settings
VERSION 1.0
QML_FILES prefs/GlobalPrefs.qml
)
Runtimme Component Instantiation
Dynamic UI generation avoids loading all components at startup. Qt provides two idiomatic approaches.
The Loader Element
Best for swapping entire views or heavy components on demand. The element manages memory automatically when the source changes or clears.
ApplicationWindow {
id: workspace
Loader {
id: contentHost
anchors.fill: parent
}
Button {
text: "Open Analytics"
onClicked: contentHost.source = "AnalyticsView.qml"
}
}
Component Factory Method
For creating multiple instances or placing objects at precise coordinates, instantiate a Component definition and call createObject. Manual memory management or parent assignment is required.
ApplicationWindow {
id: canvas
Component {
id: dataNodeFactory
NodeIndicator {
radius: 20
border.width: 2
}
}
Button {
text: "Add Node"
onClicked: {
const newNode = dataNodeFactory.createObject(canvas, {
"x": 50 + Math.random() * 200,
"y": 50 + Math.random() * 200,
"color": "steelblue"
})
}
}
}