Introduction to the QML CMake API

When Qt 6 migrated to CMake, we also wanted to provide a nicer experience for setting up QML projects. With the initial Qt 6.0 release, we did however only provide some tech preview API, which did not do much more than what was available in qmake since Qt 5.15.

Now, with the imminent release of Qt 6.2, we’ve finalized the CMake API. In this blog post, we’ll motivate why we needed to add build system support for QML, and show how two common use cases can be achieved.

Why do we even need CMake support?

Before we talk about the new way of creating QML applications and libraries, we should first look at how they were done so far:

  • Only custom C++ were put into an actual QML namespace; either via declarative type registration since 5.15, or still with manual qml*Register calls.
  • QML files were commonly simply put into a single folder. If they referenced each other, people generally relied on the implicit import (QML documents can use types in other QML documents, as long as they are in the same folder). Most ignored qmldir files, unless they had a strong reason to use them (most likely because they needed a QML singleton). Applications would then simply load a “main” QML file via QQmlApplicationEngine or QQuickWindow.
  • Additional resources like images, shaders or fonts were often put into the resource system. Then, the QML files would reference them via qrc paths. Consequently, you could no longer use the QML files with qml(scene) or the QML designer in QtCreator.
  • To use qmlcachegen, it was necessary to create a QRC file with all the QML files, and to then enable qmlcachegen by specifying QT +=qtquickcompiler.prf This would only take effect if you then loaded the respective files from the resource file system, though. When loading the files from the physical file system they would still be compiled at run time.

The above already lists a few issues caused by the rather ad-hoc approach that had been used so far. Generally, our tooling had a hard time reasoning about QML projects. Enter CMake and qt6_add_qml_module. With this newly introduced function, the build system knows about all parts of your QML project, and can pass that to our tooling as needed. Let’s look at a few examples!

A basic QML application…

We start with a small “Hello, World!” example. We have in the same directory a single QML file, main.qml

import QtQuick

Window {
    id: root
    visible: true
    Text {
        text: "Hello, world!"
        anchors.centerIn: parent
    }
}

and a main.cpp file which instantiates it:

#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();
}

Mostly straightforward, except for the “hello” in the URL. We come back to this in a second. But first, let’s take a look at the CMakeLists.txt file needed to build the project: First we do some generic setup:

cmake_minimum_required(VERSION 3.18)

project(hello 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(myapp
    main.cpp
)

target_link_libraries(myapp PRIVATE Qt6::Gui Qt6::Quick)

We’ve added the Quick module, and told CMake to create a target for our executable The interesting part happens next:

qt_add_qml_module(myapp
    URI hello
    VERSION 1.0
    QML_FILES main.qml)

Here we finally see qt_add_qml_module in action. We pass it the target of the executable, a URI, module version and a list of QML files. In turn, it ensures that myapp becomes a QML module. Among other things, this places the QML files into qrc:/${URI} in the resource file system. This might seem weird now, and will actually still seem weird at the end of this blog post: You’ll have to wait for the upcoming blog post on QML modules, where we explain in detail why this is needed. At this point, you’ll probably wonder whether this weirdness is actually worth it. Placing files into the resource system was after all already possible in Qt 5 with cmake with less ceremony!

However, rest assured, we already gained two things: First, qt_add_qml_module ensures that qmlcachegen runs. Second, it creates a myapp_qmllint target, which runs qmllint on the files in QML_FILES. Read our blog post on tooling to learn why you want to run qmllint.

…becomes more advanced

Let’s extend the example to make it more interesting. We add a new QML file, FramedImage.qml, and an image in an img subdirectory:

import QtQuick

Rectangle {
    border.width: 2
    border.color: "black"
    Image {
        source: Qt.resolvedUrl("img/world.png")
        anchors.centerIn: parent
        sourceSize.width: parent.width
        sourceSize.height: parent.height
    }
}

and use it in main.qml:

import QtQuick

Window {
    id: root
    visible: true
    Text {
        text: "Hello, world!"
        anchors.centerIn: parent
        color: "white"
        z: 2
    }
    FramedImage { anchors.fill: parent }
}

We then update the CMake list to reflect our changes:

qt_add_qml_module(myapp
    URI hello
    VERSION 1.0
    QML_FILES
        main.qml
        FramedImage.qml
    RESOURCES
        img/world.png
)

You probably already expected that QML_FILES now also lists FramedImage.qml. The more interesting change is the RESOURCES entry. By adding the referenced resources there, they get automatically added to the application under the same root path as the QML files – also in the resource file system. No need to repeat the path in qt_add_resources. And by keeping the path in the resource system consistent with the one in the source and build directory, we ensure that that the image is always found: As it is resolved relative to FramedImage.qml, it refers to the image in the resource file system if we load main from there, or to the one in the actual file system if we check it with the qml tool.

Creating a QML library

Now, let’s move on to our second example. We create a library which exposes a C++ type to QML. Our directory structure for this examples looks like

├── CMakeLists.txt
└── example
    └── mylib
        ├── CMakeLists.txt
        ├── mytype.cpp
        └── mytype.h

mytype.h declares a class and uses the declarative registration macros introduced in Qt 5.15 to expose it to the engine:

#include <QObject>
#include <QtQml/qqmlregistration.h>

class MyType : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(int answer READ answer CONSTANT)
 public:
    int answer() const;
};

The toplevel CMakeLists.txt file does some basic setup, and then uses add_subdirectory to include the one in mylib. We use the subdirectory structure which corresponds to the QML module’s URI, but with the dots replaced by slashes - that’s the same logic the engine uses when it searches for a module in the import paths. By following that convention, we help tooling.

In the subdirectory’s CMakeLists.txt we again call qt6_add_qml_module. However, this time the invocation is slightly different:

qt6_add_qml_module(mylib
    URI example.mylib
    VERSION 1.0
    SOURCES
        mytype.h mytype.cpp
)

First of all, this time we’re adding C++ types. To do that, we specify them with the SOURCES parameter. Moreover, this time we haven’t created any target for mylib so far. If the target parameter of qt6_add_qml_module does not exist already, it will automatically create a library target, which is what we want in this case.

If you build the project, you will notice that we do not only build the library, but also a QML plugin – even though we did not write a custom QQmlEngineExtensionPlugin. So what does the plugin do? By itself, not very much. The mylib library itself already contains the code to register the types with the engine. However, that is only useful in cases where we can link against the library. To make the module usable in a QML file loaded by the qml tool, we need some plugin that can be loaded. The plugin is then responsible for actually linking against the library, and ensuring that the types get registered. It should be noted that the automatic plugin generation is only possible if the module does not do anything besides registering the types. If it needs to do something more advanced, like registering an image provider in initializeEngine, you still need to manually write the plugin. qt6_add_qml_module has support for this with NO_GENERATE_PLUGIN_SOURCE. We will look at this in more detail in a later blog post.

Also, do you remember that we mentioned that following the directory layout convention helps tooling? That layout is mirrored in our build directory, with the plugin in place. That means that you can pass the path to your build directory to the qml tool (via the -I flag), and it will find the plugin. In turn, this can help you with testing changes on a pure C++ library module, as you do not need to do any further install steps.

Before we conclude, let’s add a QML file to the module: In the lib subfolder, we add a Mistake.qml file:

import example.mylib

MyType{
    answer: 43
}

and adjust the qt6_add_qml_module call:

qt6_add_qml_module(mylib
    URI example.mylib
    VERSION 1.0
    SOURCES
        mytype.h mytype.cpp
    QML_FILES
        Mistake.qml
)

As the name indicates, we made a mistake here: answer is actually a read-only property. Why would we do that? To show off the promised qmllint integration, of course: CMake will have create a qmllint target for us, and once we run it, qmllint warns us about the issue:1

$> cmake --build . --target mylib_qmllint
...
Warning: Mistake.qml:4:13: Cannot assign to read-only property answer
    answer: 43
            ^^

Outlook

This was the first blog post in a series about the new CMake API. We introduced qt6_add_qml_module, and showed how it handles some basic use cases. We’ve seen that the CMake helps you by taking care of repetitive tasks, like manualy writing a QML plugin, and how it helps with our tooling story.

In the next installments of this blog series, we will take a deep dive into QML modules, and look at some more advanced use cases: Applications which contain multiple QML libraries and installing QML module libraries.

Also check out the reference documentation of qt6_add_qml_module if you want to learn more about all the options it provides.


  1. Only if you use a recent qmllint from the dev branch.↩︎


Blog Topics:

Comments