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");
Adding another QML module
qt6_add_qml_module(style_module
STATIC
URI com.basyskom.style
QML_FILES
Style.qml
IMPORTS
QtQuick
)
target_link_libraries(myhmi PRIVATE style_moduleplugin)
#include <QQmlExtensionPlugin>
Q_IMPORT_QML_PLUGIN(com_basyskom_stylePlugin)
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml)
set(QML_IMPORT_PATH ${QT_QML_OUTPUT_DIRECTORY} CACHE PATH "Extra QML import paths to make Qt Creator happy")
Benefits
$ 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:
3 Responses
`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.
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!