QML-modules in Qt 6, zo bouw je ze goed op

In een Qt 5-project was een QML-module vooral handwerk. Je schreef zelf een qmldir, hield een .qrc bij, registreerde C++-types met qmlRegisterType en hoopte dat de import paths op het toestel klopten. Sinds Qt 6.2 doet qt_add_qml_module het meeste daarvan voor je. Dat scheelt boilerplate, maar het belangrijkste voordeel is dat de hele Qt-tooling je code begrijpt: de compiler, qmllint, de language server en Qt Creator weten welke types er bestaan en waar ze vandaan komen.
We lopen hieronder door twee kleine voorbeeldprojecten die we op GitHub hebben gezet: een applicatie als module en een library met een C++-type. Ze zijn bewust klein, zodat je ziet wat CMake genereert en wat je zelf nog moet regelen.
Wat een QML-module eigenlijk is
Een QML-module is een verzameling types onder één naam, de URI. Die types kunnen QML-bestanden zijn, C++-klassen of een mix van beide. Wie de module importeert, krijgt ze allemaal in één keer:
import QtQuickimport be.invisto.controlsWindow { width: 640 height: 480 visible: true IvProgressBar { value: 50 }}Een versienummer in de import is in Qt 6 optioneel. Laat je het weg, dan krijg je de nieuwste versie van de module die gevonden wordt. In de praktijk laten we het meestal weg en leggen we de versie vast in het buildsysteem.
Achter zo’n import zit altijd een qmldir-bestand dat de types opsomt, en voor C++-types ook een .qmltypes-bestand met hun properties, signalen en methodes. Dat tweede bestand is wat tooling nodig heeft om je code te controleren. Vroeger ontbrak het vaak, en dan bleef code completion in Qt Creator leeg of gaf qmllint een waarschuwing bij elke eigen component.
Een applicatie als module
Ook je applicatie zelf kun je als module definiëren. Dat is het eerste demoproject. Het toont het Invisto-logo in het midden van een venster:
cmake_minimum_required(VERSION 3.16)project(QMLModuleApplicationDemo VERSION 1.0 LANGUAGES CXX)set(CMAKE_AUTOMOC ON)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)find_package(Qt6 6.2 COMPONENTS Quick Gui REQUIRED)qt_add_executable(demo_app main.cpp)qt_add_qml_module(demo_app URI hello VERSION 1.0 QML_FILES main.qml Logo.qml RESOURCES images/Invisto-Logo.svg)target_link_libraries(demo_app PRIVATE Qt6::Gui Qt6::Quick)Omdat demo_app al een executable is, maakt qt_add_qml_module geen aparte library aan. Het hangt de QML-bestanden en de SVG als resources aan het programma, genereert de qmldir en zorgt dat de bestanden bij het bouwen door qmlcachegen gaan. Je hoeft geen .qrc meer te onderhouden.
Logo.qml verwijst naar de afbeelding met een relatief pad:
import QtQuickImage { source: Qt.resolvedUrl("images/Invisto-Logo.svg")}Dat werkt omdat de SVG in dezelfde module zit, onder hetzelfde resourcepad als de QML-bestanden. Daarom zie je in goed opgebouwde projecten nergens hardgecodeerde qrc:/-paden in de QML zelf.
In main.cpp staat er wel nog één:
#include <QGuiApplication>#include <QQmlApplicationEngine>int main(int argc, char *argv[]){ QGuiApplication app(argc, argv); QQmlApplicationEngine engine; const QUrl url(u"qrc:/hello/main.qml"_qs); engine.load(url); return app.exec();}Het pad qrc:/hello/ volgt uit de URI hello. Tot en met Qt 6.4 zet qt_add_qml_module de bestanden onder qrc:/<URI>/. Vanaf Qt 6.5 verandert die standaard naar qrc:/qt/qml/<URI>/, via de CMake-policy QTP0001. Dat pad staat ook standaard in de import paths van de engine, zodat modules in resources gevonden worden zonder dat je zelf addImportPath moet aanroepen.
Werk je met Qt 6.5 of nieuwer, dan schrijven we het vandaag zo:
find_package(Qt6 6.5 REQUIRED COMPONENTS Quick)qt_standard_project_setup(REQUIRES 6.5)QQmlApplicationEngine engine;engine.loadFromModule("hello", "Main");qt_standard_project_setup zet onder meer AUTOMOC en de nieuwe policies aan. Met loadFromModule laad je het type Main uit de module hello, zonder te weten waar het bestand in de resources staat. Het bestand heet dan Main.qml, met een hoofdletter, want de bestandsnaam wordt de naam van het type. En de _qs-literal uit het voorbeeld is sinds Qt 6.4 vervangen door _s, uit Qt::StringLiterals.
Een C++-type in een eigen module
Het tweede demoproject zet een C++-klasse in een aparte module. De klasse krijgt de macro QML_ELEMENT:
#pragma once#include <QObject>#include <QtQml/qqmlregistration.h>class CustomElement : public QObject{ Q_OBJECT QML_ELEMENT Q_PROPERTY(int counter READ counter WRITE setCounter NOTIFY counterChanged)public: CustomElement(); int counter() const; void setCounter(int value);signals: void counterChanged();private: int m_counter;};Let op de include. QtQml/qqmlregistration.h bevat alleen de registratiemacro’s. Wie <QtQml> include, haalt de volledige module binnen, en dat merk je in de compileertijd van een groot project.
De module zelf is een paar regels CMake:
qt6_add_qml_module(demolib URI qmllibs.demolib VERSION 1.0 SOURCES custom_element.h custom_element.cpp)Omdat demolib nog niet bestaat, maakt het commando hier zelf een library aan, samen met een plugin-target demolibplugin. Tijdens de build draait qmltyperegistrar over de headers. Die vindt QML_ELEMENT, schrijft de registratiecode, en genereert de qmldir en demolib.qmltypes. Het qmlRegisterType-werk uit Qt 5 verdwijnt volledig.
In QML gebruik je het type daarna zoals elk ander element:
import QtQuickimport qmllibs.demolibWindow { width: 640 height: 480 visible: true CustomElement { counter: 10 }}Hoe Qt je module terugvindt
Als je het hoofdproject van de library-demo bekijkt, valt iets op: de applicatie linkt demolib nergens.
add_subdirectory(qmllibs/demolib)qt_add_executable(app main.cpp)qt_add_qml_module(app URI hello VERSION 1.0 QML_FILES main.qml)target_link_libraries(app PRIVATE Qt6::Gui Qt6::Quick)Toch werkt import qmllibs.demolib. Dat komt door de mappenstructuur. De module staat in qmllibs/demolib, en dat is precies de URI qmllibs.demolib met schuine strepen in plaats van punten. CMake bouwt de module in dezelfde map onder de builddirectory, met de qmldir en de plugin erbij. De QML-engine zoekt standaard ook in de map van het programma, vindt daar qmllibs/demolib/qmldir en laadt de plugin wanneer de import voor het eerst gebruikt wordt.
Dat is handig om te beginnen, maar het verklaart ook een van de meest gestelde vragen: waarom een module op de ontwikkelmachine wel gevonden wordt en op het toestel niet. Wijkt de map af van de URI, of staat de module na installatie ergens anders, dan vindt de engine ze niet meer. Houd de map daarom altijd gelijk aan de URI, en controleer bij het deployen waar de qmldir terechtkomt.
Bouw je statisch, wat op embedded targets vaak gebeurt, dan wordt er niets meer at runtime geladen. Je linkt de plugin dan expliciet en importeert hem in C++:
target_link_libraries(app PRIVATE demolibplugin)#include <QtQml/qqmlextensionplugin.h>Q_IMPORT_QML_PLUGIN(qmllibs_demolibPlugin)De naam van de pluginklasse volgt uit de URI: de punten worden underscores, met Plugin erachter. In projecten die zowel shared als statisch gebouwd worden, linken we de module daarom altijd expliciet. Dan werkt het in beide gevallen, en ziet CMake ook de afhankelijkheid tussen de targets.
Wat je er gratis bij krijgt
Een correct opgebouwde module levert meer op dan minder bestanden.
- qmlcachegen compileert je QML tijdens de build naar bytecode, en waar het kan naar C++. De applicatie start sneller en je vangt fouten eerder.
- qmllint krijgt per module een eigen target,
<naam>_qmllint, enall_qmllintvoor het hele project. Het draait niet vanzelf bij elke build, dus zet het in je CI-pipeline. Met de gegenereerde.qmltypescontroleert het ook je eigen C++-types. - qmlls en Qt Creator gebruiken dezelfde informatie voor code completion, navigatie en waarschuwingen in de editor.
- Typeveiligheid tussen QML en C++. Hernoem je een property in C++ en vergeet je de QML, dan meldt de tooling dat, in plaats van dat je het pas op het toestel merkt.
Tips uit onze projecten
Een paar dingen die we in HMI-projecten telkens opnieuw toepassen.
Eén module per verantwoordelijkheid. Een module voor de eigen controls, een voor stijl en thema, een voor de C++-backend en de applicatie zelf. Zo kan een ander product de controls hergebruiken zonder de rest mee te nemen, en blijven de afhankelijkheden zichtbaar in CMake.
Een URI die van jou is. Gebruik een omgekeerde domeinnaam, zoals be.invisto.controls. Een algemene naam als controls botst vroeg of laat met een andere module op het import path.
Singletons via CMake. Een thema of een centrale configuratie maak je een singleton door pragma Singleton bovenaan het QML-bestand te zetten en het bestand te markeren:
set_source_files_properties(Theme.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)Zet die regel vóór qt_add_qml_module, anders wordt het bestand als gewoon type geregistreerd.
Afhankelijkheden benoemen. Importeert je module zelf andere modules, vermeld die dan met IMPORTS of DEPENDENCIES. Dat helpt qmllint en qmlcachegen, en voorkomt vage fouten bij een statische build.
Migreren vanuit Qt 5. Begin met de C++-types. Vervang qmlRegisterType door QML_ELEMENT of QML_NAMED_ELEMENT, en schrap de handgeschreven qmldir en .qrc pas als de module via CMake gebouwd wordt. Zo kun je stap voor stap overschakelen, met een werkende build tussendoor.
De voorbeeldcode
Beide projecten staan op GitHub. Ze zijn geschreven voor Qt 6.2 en werken ook op nieuwere versies, met de verschillen die hierboven beschreven staan.
- Application-QML-Module-Demo: een applicatie als QML-module, met QML-bestanden en een afbeelding als resource.
- Library-QML-Module-Demo: een aparte module met een C++-type dat via
QML_ELEMENTin QML beschikbaar wordt.
Meer over het commando lees je in de documentatie van qt_add_qml_module.
