QML Type Compilation: that new Qt Quick Compiler part you have not heard much about

We have been recently talking about the QML to C++ compilation, but this was mostly describing the process of compiling your JavaScript code. Along with it, however, there is another compiler coming in Qt 6.3 - the QML type compiler (or qmltc for short), available as a tool in the qtdeclarative repository. This compiler is part of the Qt Quick Compiler technology and, complementing QML script compiler (qmlsc), it aims to look at the QML language from a different angle. In this blog post you should learn about this part of QML and the process of compiling your QML types to C++, no components left aside.

qmltc: what it is and what it is not

In a nutshell, qmltc takes your QML document and creates an analogous C++ source code for it. If there are no issues, you get a C++ class that you can interact with from your application's source code. If something is wrong, qmltc loudly fails and breaks your build. This is because the qmltc produces C++ code that is considered essential for your application, similarly to how MOC generates C++ meta-object system code that is essential for the types marked with Q_OBJECT or Q_GADGET. For this reason, qmltc is an optional, opt-in feature in Qt. Additionally, mind that the type compiler is in the Technology Preview stage, so not everything is guaranteed to work. Nonetheless, we highly encourage you to try qmltc out!

Since qmltc will not help you automagically, you have to modify your current application C++ code to get any benefit from qmltc compilation. Unlike a silently working QML cache generation, qmltc provides, in some sense, user-visible output. This output - per QML file - is a class that represents the C++ form of the QML document. The purpose of qmltc (and thus the generated code) is to replace the QQmlComponent-based runtime loading and creation of QML objects with an ahead-of-time procedure. In principle, using qmltc should deprecate most of the existing object creation logic and allow the transition to the use of the generated code directly. Consequently, this would give additional optimization opportunities centered around the application startup. There are surely caveats along the way but the general idea is viable.

Having this picture in mind, the user application would have to replace the object creation code, porting away from QQmlComponent::create() to direct C++ object creation via C++ constructors. In this regard, there is a marginal difference between the two approaches from the application perspective with resulting objects looking more or less the same at the API level. At present, however, qmltc's output is not integrated well enough with higher-level abstractions such as QQmlApplicationEngine or QQuickView.

In pseudo-code, you can use both approaches side by side in the following way:


QScopedPointer<QObject> createWithQQmlComponent(QQmlEngine *e)
{
    // Use the QML document available in QRC
    QQmlComponent component(e, QUrl(QStringLiteral("qrc:/ModuleUri/HelloWorld.qml")));
    return QScopedPointer<QObject>(component.create());
}

#include "helloworld.h" // compiled HelloWorld.qml header
QScopedPointer<QObject> createWithQmltc(QQmlEngine *e)
{
    // Note that we can also return QScopedPointer
    return QScopedPointer<QObject>(new HelloWorld(e));
}

int main()
{
    // ...
    QQmlEngine e;
    auto oldStyleObject = createWithQQmlComponent(&e);
    auto newStyleObject = createWithQmltc(&e);
}

You can read more about qmltc and related matters in the Qt documentation snapshots.

Solving the build system problem

As already mentioned, you have to explicitly opt into building your application's QML files with qmltc. To achieve this, one can employ the CMake API that adds the qmltc compilation step in Qt 6.3. Note that this API assumes that you have a proper QML module and thus use our latest-and-greatest CMake command to create such a module. This is intentional: in most cases, if there is no proper QML module, the type analysis will fail and so qmltc will fail as well (not only that but qmlsc, compiling the JavaScript, would silently fall back to the old cache generation model).

Generally, writing a proper QML module and then using the qmltc compilation command should be sufficient. In certain cases, you might need to add extra import paths (e.g. when you have your own QML modules that reside outside of the default import location). Other than that, you are all set for success from the build system point of view. The other aspect is to actually use the generated output. It seems to be best described through an example.

Using qmltc

To better highlight the way qmltc works, we can create a rather trivial application. Since this is a simple showcase of the capabilities, there is no need for anything fancy. We propose the following two-component (roughly speaking) design: a) a text field, e.g. a "Hello, World" of sorts; b) a button with an onClick handler that picks a random number and shows that number in a text field. To make b) a bit more visual, we can also transform the random value into an RGB color triplet and show the resulting color on screen along with the textual representation of the original value. While not exceptionally complex, this is interesting enough.

We can lay out our items vertically one after another. Having the elements in mind, the user interface schematically could be:

qmltc_initial_blog_ui_scheme

To create such a UI, we would need multiple building blocks in Qt. Most notably, Qt Quick QML module for several basic UI classes. Unfortunately, qmltc cannot compile Qt Quick Controls 2 at the moment, so button has to be custom. Yet, the button is rather easy: it is basically a rectangle with a clickable mouse area inside (all can be taken directly from the Qt Quick), add a text field to it, other bells and whistles and here you go. We leave it as an exercise to the reader here.

To convert a random number to a color, we can create a small C++ class which would return us a QColor from the given double value. Exposing the class to QML and using the returned QColor as a value for the color property of our rectangle would make everything work. Although a bit over-complicated, adding C++ into the application would make things even more fun so it is worth it, well, at least from the perspective of this blog post!

Note that the snippets used here represent a simplified (and slightly re-structured) version of the example from the qmltc's documentation. The source code of that example is available in the qtdeclarative repository.

Project structure and CMake

For the sake of simplicity, we can put all our files into the same directory. This flat structure also guarantees correct resulting QML module structure.


.
├── CMakeLists.txt
├── colorpicker.cpp         # random-value-to-color converter (.cpp file)
├── colorpicker.h           # random-value-to-color converter (.h file)
├── myApp.qml               # application's main QML document
├── MyButton.qml            # custom button
└── main.cpp                # application's main C++ file

Now, the assumption is that you are acquainted with CMake, so we do not cover how to create an executable, link libraries to it, etc. The interesting bits are creating the QML module and enabling qmltc:


# CMakeLists.txt
set(application_qml_files # QML files in the application
    myApp.qml
    MyButton.qml
)

# Create a QML module:
qt6_add_qml_module(my_qmltc_example # my_qmltc_example is the application name
    VERSION 1.0
    URI QmltcExample
    QML_FILES ${application_qml_files}
)

# Enable qmltc (which would automatically add generated C++ to our application):
qt6_target_compile_qml_to_cpp(my_qmltc_example
    QML_FILES ${application_qml_files}
)

As part of working with qmltc, the application should also link against the private Qt libraries. This is due to most of the C++ code for the QML types being private (and thus inaccessible through Qt::Qml and Qt::Quick targets in CMake). While we strongly discourage the use of private API, this is unfortunately necessary at present when working with qmltc. There is an implicit guarantee that qmltc would ensure that the generated code works correctly with the private API. Note that this is only true (and makes sense) as long as qmltc compilation is re-done when you downgrade or upgrade the Qt libraries. In this case of working with QtQml and QtQuick, we need to link against private counterparts of Qt::Qml and Qt::Quick:


target_link_libraries(my_qmltc_example PRIVATE Qt::QmlPrivate Qt::QuickPrivate)

Application code

The most interesting part of our small application is the source code, QML and C++. Considering MyButton type as less important, let's focus on three things:

  • (since it is a C++ type) a class that creates a color from the random value
  • (since it defines the full UI structure) main QML document
  • (since it contains an entry point of our program) main.cpp

The logic behind the random-value-to-color converter (we call it a "color picker" below) is straightforward: given a double value in the [0.0; 1.0) range, scale it up to the RGB integer value range [0; 256 * 256 * 256) and then interpret the result as a QRgb value (passing it to the constructor of QColor). We can hide this logic behind a dedicated function. Thus, at the API-level our color picker might look similar to this (note that many details are omitted):


// colorpicker.h
class MyColorPicker : public QObject
{
    Q_OBJECT
    QML_ELEMENT

    // stores a value in the range [0, 1); myApp.qml type sets this with Math.random()
    Q_PROPERTY(double encodedColor READ encodedColor WRITE setEncodedColor /* ... */)

public:
    Q_INVOKABLE QColor decodeColor(); // returns a QColor for the internally stored encodedColor
};

Note the QML_ELEMENT macro there that would automatically register the C++ type in QML (in practice it is a bit more complicated but this is the foundational part), making it available once import QmltcExample 1.0 is done. In reality, this way you also make the type tooling-friendly for qmllint, qmlsc, and surely qmltc.

The main QML document would define the UI structure of our application, as well as implement the interactions between separate elements. To arrange the items within the window, anchors property, available in any Item-based QML type, can be used. We do omit the boilerplate related to this property here for brevity.


// myApp.qml
import QtQuick
import QmltcExample 1.0 // application's own QML module

Rectangle {
    id: window

    Text {
        font.pixelSize: 20
        text: "Hello, QML World!"
    }

    Text {
        id: rndText
        font.pixelSize: 25
        text: "0.00"
    }

    Rectangle {
        id: rndColorRect
        color: "black"

        MyColorPicker { // comes from C++
            id: colorPicker
            onEncodedColorChanged: rndColorRect.color = colorPicker.decodeColor()
        }
    }

    MyButton { // our custom button
        id: rndButton
        text: "PICK"
        onClicked: function() {
            var value = Math.random();
            rndText.text = value.toFixed(rndButton.text.length - 2);
            colorPicker.encodedColor = value;
        }
    }
}

Collecting it altogether, our main.cpp would just need to create the myApp object and use that as the main UI element:


// include usual Qt headers, etc.

#include "myapp.h" // generated C++ header (for myApp.qml)

int main(int argc, char *argv[]) {
    QGuiApplication app(argc, argv);

    QQmlEngine e;
QQuickWindow window;
myApp compiledQmlApp(&e); compiledQmlApp->setParentItem(window.contentItem()); window.show(); return app.exec(); }

Result

With successful compilation of the pieces above, we should now have a usable QML application. We have intentionally skipped some boilerplate parts (centering the UI within a window, spacing and padding elements, choosing color scheme for the app, #include-ing and import-ing things, etc.) as you should clearly know all that if you have been doing some UI / QML programming before. The important stuff to take with you here is the way we order the source code, what ends up being written in CMake, how our C++ class is exposed to QML and, most importantly, how the qmltc-generated class can be used. As the last bit, you can also observe (a slightly more polished) result in the end:

qmltc_initial_blog

The way forward

As stated in the beginning, the QML type compiler is currently in the Technology Preview phase. Despite this, we want to show it to the public already to collect useful feedback, to learn about the ways you interact with the compiler, what challenges you have and which improvements (and in which areas) would be most relevant. The main resource to start with would be the documentation of qmltc as well as the more general information about QML and tooling (such as qmllint).

As usual, you are welcome to participate through the various channels (with the ones advertised on The Qt Project page as the more visible to us) or submit bugs, suggestions, feature proposals through our bugtracker.

And, of course, do not hesitate to read more stuff on The Qt Company blog as we try to keep you (yes you!) up to date.


Blog Topics:

Comments