In recent years, a lot has been happening to improve performance, maintainability and tooling of QML. Some of those improvements can only take full effect when your code follows modern best practices. Here are 10 things you can do in order to modernize your QML code and take full advantage of QML's capabilities.
1. Use qt_add_qml_module CMake API
Qt6 introduced a new CMake API to create QML modules. Not only is this more convenient than what previously had to be done manually, but it is also a prerequisite for being able to exploit most of the following tips.
By using the qt_add_qml_module, your QML code is automatically processed by qmlcachegen, which not only creates QML byte code ahead of time, but also converts parts of your QML code to C++ code, improving performance. How much of your code can be compiled to C++ depends on the quality of the input code. The following tips are all about improving your code in that regard.
2. Use declarative type registration
When creating custom types in C++ and registering them with qmlRegisterType and friends, they are not visible to the tooling at the compile time. qmlcachegen doesn't know which types exist and which properties they have. Hence, it cannot translate to C++ the code that's using them. Your experience with the QML Language Server will also suffer since it cannot autocomplete types and property names.
To fix this, your types should be registered declaratively using the QML_ELEMENT (and its friends, QML_NAMED_ELEMENT, QML_SINGLETON, etc) macros.
The URL and version information are inferred from the qt_add_qml_module call.
3. Declare module dependencies
Sometimes your QML module depends on other modules. This can be due to importing it in the QML code, or more subtly by using types from another module in your QML-exposed C++ code. In the latter case, the dependency needs to be declared in the qt_add_qml_module call.
For example, exposing a QAbstractItemModel subclass to QML adds a dependency to the QtCore (that's where QAbstractItemModel is registered) to your module. This does not only happen when subclassing a type but also when using it as a parameter type in properties or invokables.
Another example is creating a custom QQuickItem-derived type in C++, which adds a dependency on the Qt Quick module.
To fix this, add the DEPENDENCIES declaration to qt_add_qml_module:
4. Qualify property types fully
MOC needs types in C++ property definitions to be fully qualified, i.e. include the full namespace, even when inside that namespace. Not doing this will cause issues for the QML tooling.
5. Use types
In order for qmlcachegen to generate efficient code for your bindings, it needs to know the type for properties. Avoid using 'property var' wherever possible and use concrete types. This may be built-in types like int, double, or string, or any declaratively-defined custom type. Sometimes you want to be able to use a type as a property type in QML but don't want the type to be creatable from QML directly. For this, you can register them using the QML_UNCREATABLE macro.
6. Avoid parent and other generic properties
qmlcachegen can only work with the property types it knows at compile time. It cannot make any assumptions about which concrete subtype a property will hold at runtime. This means that, if a property is defined with type Item, it can only compile bindings using properties defined on Item, not any of its subtypes. This is particularly relevant for properties like 'parent' or 'contentItem'. For this reason, avoid using properties like these to look up items when not using properties defined on Item (properties like width, height, or visible are okay) and use look-ups via IDs instead.
7. Annotate function parameters with types
In order for qmlcachegen to compile JavaScript functions, it needs to know the function's parameter and return type. For that, you need to add type annotations to the function:
When using signal handlers with parameters, you should explicitly specify the signal parameters by supplying a JS function or an arrow expression:
Not only does this make qmlcachegen happy, it also makes your code far more readable.
8. Use qualified property lookup
QML allows you to access properties from objects several times up in the parent hierarchy without explicitly specifying which object is being referenced. This is called an unqualified property look-up and generally considered bad practice since it leads to brittle and hard to reason about code. qmlcachegen also cannot properly reason about such code. So, it cannot properly compile it. You should only use qualified property lookups
Another area that needs attention is accessing model roles in a delegate. Views like ListView inject their model data as properties into the context of the delegate where they can be accessed with expressions like 'foo', 'model.foo', or 'modelData.foo'. This way, qmlcachegen has no information about the types of the roles and cannot do its job properly. To fix this, you should use required properties to fetch the model data:
9. Use pragma ComponentBehavior: Bound
When defining components, either explicitly via Component {}
or implicitly when using delegates, it is common to want to refer to IDs outside of that component, and this generally works. However, theoretically any component can be used outside of the context it is defined in and, when doing that, IDs might refer to another object entirely. For this reason, qmlcachegen cannot properly compile such code.
To address this, we need to learn about pragma ComponentBehavior
. Pragmas are file-wide switches that influence the behavior of QML. By specifying pragma ComponentBehavior: Bound
at the top of the QML file, we can bind any components defined in this file to their surroundings. As a result, we cannot use the component in another place anymore but can now safely access IDs outside of it.
A side effect of this is that accessing model data now must happen using required properties, as described in the previous point. Learn more about ComponentBehavior here.
10. Know your tools
A lot of these pitfalls are not obvious, even to seasoned QML programmers, especially when working with existing codebases. Fortunately, qmllint helps you find most of these issues and avoids introducing them. By using the QML Language Server, you can incorporate qmllint directly into your preferred IDE/editor such as Kate or Visual Studio Code.
While qmlcachegen can help boost your QML application's performance, there are performance problems it cannot help with, such as scenes that are too complex, slow C++ code, or inefficient rendering. To investigate such problems, tools like the QML profiler, Hotspot for CPU profiling, Heaptrack for memory profiling, and GammaRay for analyzing QML scenes are very helpful.
Trusted software excellence across embedded and desktop platforms
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.
6 Comments
23 - Oct - 2024
Robert
A question for point 9: Isn't it cleaner to define the interface between the "inner" component (e.g. the delegate) and the outside world, for example by specifying the required properties that will be passed (e.g. from the model), so we get a clean separation between component and outside?
The approach you advocate sounds a bit like "use file-level global variables, and here is how".
23 - Oct - 2024
Nicolas Fella
There's tradeoffs here.
Passing in data from the model via required properties is definitely preferred if 1) The data is part of the model and 2) It's conceptually per-delegate data.
But that's not always the case. Say you have a generic model (that you can't necessarily extend) and two different UIs on top. Now in one of the UIs your delegate need to reference another UI element in the same file. Then what I'm showing you is relevant.
Another case where this is relevant is when you have explicit Component{} in your code (rather than the implicit component from delegate) and you want to access IDs from outside that component.
Whether you should have this cross-component coupling is a valid question, and you definitely shouldn't overdo it, but it's a pattern that appears in real-world code.
25 - Oct - 2024
Lorenzo
Can I use 'qt_add_qml_module' when I'm developing a library? For example, when the library contains some classes that should expose enums to QML using QML_ELEMENT with QML_UNCREATABLE()?
25 - Oct - 2024
Nicolas Fella
Yes!
You can use qt_add_qml_module on the existing library target. That way it will process the type registration from the library and create a QML module for it.
If you cannot or don't want to modify the existing library to add the type registration macro you can also use qt_add_qml_module to create a new QML module target and use QML_FOREIGN to register your library types.
What qt_add_qml_module doesn't do for you is installing the created QML module as part of the CMake install. That's something that needs doing manually until https://bugreports.qt.io/browse/QTBUG-110784 is addressed.
28 - Oct - 2024
Lorenzo
Thanks! The bad part is when I've to use the library in a project. Setting the 'QML_IMPORT_PATH' in CMake does not work for me. I have to call 'QQmlEngine::addImportPath'
11 - Jan - 2025
Peter
Thanks for this, quite useful and ranks well on SEO.