Invisto
Invisto · your interface partner
Mailinfo@invisto.beTelefoon+32 477 42 11 14KantoorHangar K, Kortrijknlen
Alle inzichten
inzichten/QML

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

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:

QML
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:

CMakeCMakeLists.txt
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:

QMLLogo.qml
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:

C++main.cpp
#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:

CMakeCMakeLists.txt
find_package(Qt6 6.5 REQUIRED COMPONENTS Quick)qt_standard_project_setup(REQUIRES 6.5)
C++main.cpp
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:

C++qmllibs/demolib/custom_element.h
#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:

CMakeqmllibs/demolib/CMakeLists.txt
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:

QMLmain.qml
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.

CMakeCMakeLists.txt
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++:

CMakeCMakeLists.txt
target_link_libraries(app PRIVATE demolibplugin)
C++main.cpp
#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, en all_qmllint voor het hele project. Het draait niet vanzelf bij elke build, dus zet het in je CI-pipeline. Met de gegenereerde .qmltypes controleert 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:

CMakeCMakeLists.txt
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.

Meer over het commando lees je in de documentatie van qt_add_qml_module.

Loop je vast in de structuur van je Qt-project? Laten we eens sparren.

Plan een gesprek