basysKom AnwendungsEntwicklung

How To Use Modern QML Tooling in Practice
Essential Summary
Qt 5.15 introduced “Automatic Type Registration”. With it, a C++ class can be marked as “QML_ELEMENT” to be automatically registered to the QML engine. Qt 6 takes this to the next level and builds all of its tooling around the so-called QML Modules. Let’s talk about what this new infrastructure means to your application in practice and how to benefit from it in an existing project.
Professionelle Entwicklung Ihrer HMI

Sie benötigen ein Geräte-HMI, Ihnen fehlt es aber an Zeit oder speziellem HMI-Know-How?​

Warum Dienstleistungen von basysKom?

Von der Konzeption über die Implementierung bis zum Testen unterstützen wir Sie in der Entwicklung Ihrer individuellen HMI. Unsere Services umfassen zudem die Technische Beratung, individuelle Trainings, Coaching, Verstärkung Ihrer Entwicklungsteams bis hin zur vollständigen Auftragsabwicklung im gesamten Lebenszyklus Ihres Produktes.

Creating the main application QML module

Your typical embedded device HMI consists of its main application UI, a bunch of shared UI components, as well as back-end glue code to talk to some hardware. While shared QML components are usually implemented as proper QML plug-ins already, the main UI is often bundled in the application binary loaded through a qrc:/qml/main.qml URL on startup. With QML modules, however, Qt wants to move away from loading QML files in an ad-hoc fashion like this. Every QML file that is used should be explicitly declared. This means that even the main application UI should become its own QML module:

qt6_add_executable(myhmi main.cpp ...)
qt6_add_qml_module(myhmi
    URI com.basyskom.app
    QML_FILES
        Main.qml
        MainPage.qml
        ...
    IMPORTS
        QtQuick
) 

The Main.qml file will be added to the resource system automatically in qrc:/qt/qml/com/basyskom/app/Main.qml but you will rather be loading it by its module and type name:

QQmlApplicationEngine engine;
engine.loadFromModule("com.basyskom.app", "Main"); 

Likewise from QML:

Qt.createComponent("com.basyskom.app", "Main"); 
Now it doesn’t matter whether it’s an actual QML file somewhere on your hard drive, in your application resources, or has been compiled to proper C++. If the CMake target already exists, the QML module is added to it, otherwise a library target is created automatically.

Adding another QML module

Unfortunately, every CMake target can only be assigned a single QML module. If you want to keep using separate QML imports within the same binary, you’ll need to create a static library for each module and link it to the main application:
qt6_add_qml_module(style_module
    STATIC
    URI com.basyskom.style
    QML_FILES
        Style.qml
    IMPORTS
        QtQuick
)
target_link_libraries(myhmi PRIVATE style_moduleplugin) 
We then also want to link the “plugin” rather than its backing library. Since the compiler is free to eliminate unreferenced symbols, it needs to be explicitly mentioned in the main application. The plugin class name is based on the URI with “Plugin” appended to it:
#include <QQmlExtensionPlugin>
Q_IMPORT_QML_PLUGIN(com_basyskom_stylePlugin) 
There’s a catch: by default, for executable targets, the path components based on its URI (e.g. com/basyskom/app) are automatically appended to the QML module’s binary location whereas for library targets it’s not! The rationale being that an application can control its import paths and search for QML imports in its binary directory whereas a library has no control over its environment. It’s your responsibility to deploy it correctly and set the QML engine’s import paths accordingly. That’s why in the above example the CMakeLists.txt has to be placed in a com/basyskom/style subfolder. This ensures that the resulting binary folder structure matches the structure the QML engine expects by default.
Realistically, for existing projects, matching the source code directory structure to the QML import path structure is not feasible. Luckily, you can force QML module code generation for all targets to happen in a specific directory:
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml) 
Once an OUTPUT_DIRECTORY is set, the default rule no longer applies and the URI path components are always appended. Confusing, I know. Sadly, just because the application runs, it doesn’t mean that the QML tooling finds its imports. The application is linked against the plug-in and its resource files and can access it just fine at runtime whereas QML tooling needs to look for “physical” files describing the modules without execution. Qt Creator for instance checks the QML_IMPORT_PATH from CMakeCache.txt:
set(QML_IMPORT_PATH ${QT_QML_OUTPUT_DIRECTORY} CACHE PATH "Extra QML import paths to make Qt Creator happy") 
You might have to reset the QML code model for Qt Creator to find newly added QML components.

Benefits

Once you have added the relevant QML files to your new application module, the application will run pretty much as before. The good news is that the two worlds can co-exist. You can keep your manual calls to qmlRegisterType and gradually migrate as you add new features or refactor existing code. Thus, the big question right now is: what are the benefits of this new approach?
First of all, the qmldir file that describes your QML module is automatically generated. Usually, qmldir was only maintained when absolutely necessary, such as when implementing a singleton type, with applications instead relying on the implicit import that makes types in the same directory available automatically. Likewise for the qmltypes type description file that an IDE like Qt Creator uses to figure out which C++ types are accessible in the module and which properties they expose.
There’s also no need to tinker with resource files as QML files and their assets get automatically bundled. That alone makes it worth it as there is now a single source of truth about where to add a new QML file. Having assets, such as icons, addressable relative to the QML files that use them also makes them more easily viewable standalone, such as from the qml tool (think qmlscene) or Qt Creator’s QML Designer. Sadly, bundling many assets alongside QML files also has an impact on compilation times. A couple of seconds penalty whenever changing a single line of QML because of assets being recompiled can be quite irritating.
The most game-changing part of QML modules, however, is the tooling it unlocks: The QML Linter (qmllint) knows exactly what types and properties are available and can warn of unqualified property access, vague type descriptions, unused imports. The types of bugs that only show at runtime and are therefore hard to spot in advance. The QML Language Server (qmlls) then supplies an LSP-capable editor with information collected by qmllint for a richer development experience even outside a fully-featured IDE as well as invoke the QML Formatter (qmlformat) to format your code. Finally, the QML compilers (qmlcachegen or qmlsc, the QML script compiler) can produce more efficient code if they know in advance what types are being used in a property binding or function call, since JavaScript is a very dynamic language. Of course, thanks to QML files being processed at build time, we also get build failures for syntax errors in them.
The qt_add_qml_module CMake function automatically generates qmllint targets for your libraries and executables. Just build the relevant myapp_qmllint or convenience all_qmllint targets and look at the results.
$ cmake --build . --target all_qmllint
...
Warning: app/qml/Main.qml:52:42: Member "title" not found on type "QQuickItem" [missing-property]
                text: stack.currentItem.title
                                        ^^^^^

Info: app/qml/pages/MainPage.qml:7:1: Unused import [unused-imports]
import QtQuick

Warning: app/com/basyskom/style/Style.qml:24:31: Member "colorScheme" not found on type "QObject" [missing-property]
            if (Qt.styleHints.colorScheme === Qt.ColorScheme.Dark) {
                              ^^^^^^^^^^^

ninja: build stopped: subcommand failed. 

The linter target “failed” because there were some warnings, making it an ideal candidate for inclusion in a CI system. Naturally, all the linter sees is the “currentPage” property of StackView which is declared “QQuickItem”. It doesn’t know that in our application we will only ever push an implementation of AbstractPage. We can fix that by using a type assertion:

text: (stack.currentItem as Controls.AbstractPage).title 

Think of a type assertion (the “as” cast) as a “qobject_cast”: if the type is not (an implementation of) an AbstractPage, it will return null. Unfortunately, there’s a couple of APIs in Qt itself where the return type is generic, which we cannot fix on our side. Qt.styleHints returns a generic QObject, not the StyleHints type. Therefore, we should use the “styleHints” property of the Application singleton instead.

Conclusion

We have seen how to transform our existing main application UI into proper QML modules to take full advantage of modern QML tooling. While it’s nice for the build system to be more knowledgeable about our code base now, as a developer you want this information available at your fingertips while actually writing code. Stay tuned for our next issue where we are going to have a look at the QML Language Server and how to set it up, and what you can do to help the QML Compiler generate more efficient code.

Further reading:

Picture of Kai Uwe Broulik

Kai Uwe Broulik

Kai Uwe Broulik is Software Architect at basysKom where he designs embedded HMI applications based on Qt with C++ and QML. He also trains customers on how to use Qt efficiently. With his more than ten years of experience in Qt he has successfully deployed Qt applications to a variety of platforms, such as mobile phones, desktop environments, as well as automotive and other embedded devices.

3 Antworten

  1. `text: (stack.currentItem as Controls.AbstractPage).title `
    is straight up bad advice and goes against the dynamic nature of QML for this kind of property late-binding. The linter is wrong here and if it can’t infer the dynamic type correctly it should not create these kind of warnings at all, as late-binding is a proper use-case in QML/Javascript. So fix the linter instead of making worse QML.

    Also, the `as` keyword should instead only be used to add a named scope to imports:
    `import … as MyScopedName`.

    But what’s really wrong here with the … may I say abuse of the `as` keywoard? … is that a type is enforced for `stack.currentItem`, in a distance from the actual stack component. What if the stack deals with elements of different types, or a new type is added because the stacks’ use case evolved? Better be explicit about these kind of assumptions and turn them into a property. This will make QML refactors easier as well. Remember, properties in QML often express invariants; they can be your best friends!

    So what I would do instead is to turn this assumption about the stack element’s type into a read-only property of that type:
    `readonly property Controls.AbstractPage currentItem: stack.currentItem // use of concrete type instead of alias property`.

    The above has been possible for a long time, is idiomatic QML and it will fail at that single instance of that type-cast (with a clear type warning/error!) whereas the proposed solution will fail every time that type-cast is exercised in the component, or worse, leave you with empty strings in the user interface.

    The failed cast with `Qt.styleHints` should have been a strong hint to you that you are using the `as` keyword in unintended ways.

    • > The linter is wrong here and if it can’t infer the dynamic type correctly it should not create these kind of warnings at all.

      All it sees is a property of type `QQuickItem`. It’s a genuine warning since QML tooling will be unable to optimize this call. Either way, feel free to file a Qt bug report and start a discussion about the linter’s capabilities over there.

      > late-binding is a proper use-case in QML/Javascript

      Maybe, but it’s also a source of trouble and something that should be discouraged: it defeats any chance of build time optimization or early error detection. There’s a reason Qt went through all classes to mark most `Q_PROPERTY` as `FINAL`, so it’s clear beforehand where a property belongs.

      > So what I would do instead is turn this assumption about the stack element’s type into a read-only property of that type

      Perhaps that’s the more expressive way indeed. Nevertheless, it was a good way to demonstrate what a type assertion actually does. If we didn’t use optional chaining (`?.` operator), we’d also get a warning when the type isn’t what we expected. The official documentation recommends doing a type assertion on something like `parent as Rectangle` by the way which is definitely something I would not recommend (the “parent” part, that is).

      > The failed cast with ‘Qt.styleHints’ should have been a strong hint that you are using the ‘as’ keyword in unintended ways.

      It’s not. It’s an indication that Qt didn’t expose a suitable type for this property. It’s declared as `QObject` and probably wasn’t fixed for compatibility reasons. There’s a new `Application` singleton that declares `styleHints` with a proper type.

  2. This explains much more concisely everything that you need to know to use the new cmake-qt build integrations. I’ve tried the official documentation on this and it even link to a tutorial that builds but doesn’t run. The documentation on this subject is so scattered and often missing crucial details. Your article not only fills those details, it also explains from a high level how everything is meant to work. Thankyou for a tutorial that is better than the official documentation and works!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Weitere Blogartikel

basysKom Newsletter

We collect only the data you enter in this form (no IP address or information that can be derived from it). The collected data is only used in order to send you our regular newsletters, from which you can unsubscribe at any point using the link at the bottom of each newsletter. We will retain this information until you ask us to delete it permanently. For more information about our privacy policy, read Privacy Policy