What’s new for QML Modules in 6.5

While QML modules have existed for a long time, their use had been rather sparse before Qt 6. With the introduction of qt_add_qml_module in Qt 6, they have however become much more prevalent. And with good reason: Only by placing all related QML in a module can tooling like qmllint or the Qt Quick Compilers work correctly.

However, some parts of Qt’s own API were not aware of modules so far. When interacting with QML types, e.g. via QQmlComponent, you would need to use explicit file paths so far. Starting from 6.5, there’s now an alternative solution leveraging modules, which we’ll introduce in this blog post. Moreover, we now provide a solution which obsoletes fiddling with import paths in most common cases.

New default import path

Let’s start with the new default import path that has been added: You can now put your modules under qrc:/qt/qml, and they will be automatically found there. So far, if you wanted to place modules the resource system, you would either had to call QQmlEngine::addImportPath with a path of your choosing or place it under qrc:/qt-project.org/imports. The latter is strongly discouraged, as it is only meant for Qt’s own modules and libraries that are expected to be used system wide. The former cannot be easily done by a library, and forcing every user of a library to first call addImportPath isn’t much of a solution either. To remedy this, we now provide qrc:/qt/qml as a default import path, and recommend applications and libraries to use it.


NOTE:

Applications could avoid the issue with the import path by simply loading their main entry point from a path in the resource system, and relying on the implicit import to find the module’s qmldir, assuming the main file and the qmldir are all part of the module and placed in the same folder. We’ll see below why this solution has its own shortcomings, and for libraries it was never feasible in any case.


CMake integration

Unfortunately, qt_add_qml_module has been using / as the default for the resource prefix so far, and we cannot silently change it. Fortunately, Qt 6.5 also brings support Qt CMake policies, which allow for a controlled way of evolving the defaults in our CMake API. Enabling QTP0001 sets /qt/qml as the default when no explicit value for RESOURCE_PREFIX has been provided. The easiest way to enable this policy is to call

qt_standard_project_setup(REQUIRES 6.5)

Passing REQUIRES 6.5 to qt_standard_project_setup will globally enable all new policies introduce up to Qt 6.5. That happens to only be the one about the resource prefix. For more details on controlling policies, please consult their documentation and the documentation of the qt_policy command.

But I’m using qmake!

In case you’re on a project using qmake, you can still benefit from the new default import path, by setting the prefix on the RESOURCES variable.

QML_IMPORT_NAME = MyModule
QML_IMPORT_MAJOR_VERSION = 1

SOURCES += \
    main.cpp

HEADERS += \
    filesystemmodel.h

qml_resources.files = \
    qmldir \
    Main.qml \
    MyType.qml

qml_resources.prefix = /qt/qml/MyModule

RESOURCES += qml_resources

With this setup, the MyModule module can be found by the engine without any further setup at runtime.

Module aware API

The second substantial change related to QML module is a new set of APIs to interact with QML types, both from C++ and from QML.

Loading Components: The state so far

Before we dive into the new API, let’s first look at how a simple QML application project would look before. We would have the following directory structure:

helloqml
├── CMakeLists.txt
├── main.cpp
└── main.qml

The CMakeLists.txt would look like:

cmake_minimum_required(VERSION 3.21)

project(helloqml VERSION 0.1 LANGUAGES CXX)

find_package(Qt6 6.5 COMPONENTS Quick REQUIRED)

qt_standard_project_setup(REQUIRES 6.5)

qt_add_executable(helloqmlapp
    main.cpp
)

qt_add_qml_module(helloqmlapp
    URI helloqml
    QML_FILES main.qml 
)

target_link_libraries(helloqmlapp
    PRIVATE Qt6::Quick)

Note that we’re already using the new default import path mentioned above. main.cpp contains

#include <QGuiApplication>
#include <QQmlApplicationEngine>

using namespace Qt::Literals::StringLiterals;

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

    QQmlApplicationEngine engine;
    const QUrl url(u"qrc:/qt/qml/helloqml/main.qml"_s);
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
                     &app, []() { QCoreApplication::exit(-1); },
                     Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

and finally, main.qml contains

import QtQuick

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")
}

The line of interest is const QUrl url(u"qrc:/qt/qml/helloqml/main.qml"_s);. There are multiple gotchas in here. The first is that because we are dealing with a QML module, main.qml has now been placed under a helloqml folder (remember, a QML module must be in one of the import paths within a folder corresponding to the module’s URI). The second gotcha is that while qt_add_qml_module helpfully places the QML files into the resource system, we still have to remember to load from there and not directly from the file system if we want to avoid potentially slow file system operations (and ensure that AOT compiled bindings are executed instead of being interpreted at runtime). On the other hand, if we do want to load from the normal file system during development (to iterate faster on our QML files without having to recompile), we would be out of luck as we have now hard-coded the qrc path. With Qt 6.5 we have a new option which avoids all the issues.

Loading Components: The new approach

With QML modules, we know that a given QML element can be identified via the name of its module and its type-name. So let’s use those: We rename main.qml to Main.qml (recally that only upper case files lead to exported QML types by default), and change main.cpp to use engine.loadFromModule("helloqml", "Main"); instead of engine.load(url). And we’re done. We no longer have to care where exactly the file is located, and the loading from the resource file system is handled behind the scenes. But loadFromModule can do more than just replacing existing load(QUrl) calls. It also enables a few things that were not possible before. loadFromModule does not require that the QML element we’re loading is a file. Instead it supports any element that can be created in QML. That includes

  • elements backed by files (as we’ve already seen),

  • inline components: Given

    // Outer.qml, in module MyModule
    Item {
        component Inner : Rectangle { color: "red" }
    }

    we can load Inner via engine.loadFromModule("MyModule", "Outer.Inner").

  • and types defined in C++: Given

    // part of MyModule
    struct MyFancyItem : public QQuickItem
    {
      QML_ELEMENT
      // ...
    }

    we can create an instance of MyFancyItem via engine.loadFromModule("MyModule", "MyFancyItem").

Before, the last two cases would have required creating a wrapper QML file instantiating the desired element. Additionally, the above is not limited to QQmlApplicationEngine. An equivalent API exists also in QQmlComponent in the form of QQmlComponent::loadFromModule. Lastly, there’s also a new overload of Qt.createComponent, exposing the functionality in QML:

import QtQuick
ListView {
    model: 10
    delegate: Qt.createComponent("MyModule", "MyFancyItem")
}
Singletons

The API so far was used to create new objects, and thus it’s unsuitable for QML singletons. For consistency’s sake, there is however also a new overload of QQmlEngine::singletonInstance working with singletons. That can be useful when setting up some application global data, e.g. to expose a QAbstractItemModel, or some application configuration data fetched from the network: Given a singleton defined in C++ 1

class Globals : public QObject
{
   Q_OBJECT
   QML_ELEMENT
   QML_SINGLETON
   
   Q_PROPERTY(QAbstractItemModel *model READ model WRITE setModel NOTIFY modelChanged)
   //...
}

we can then write

QQmlApplicationEngine engine;
// access the singleton
auto globals = engine.singletonInstance<Globals>("MyModule", "Globals");
// set up global state
globals.setModel(fancyModel);
// start the application after the initial setup
engine.loadFromModule("MyModule", "Main")

Note that it was possible to do this even in prior Qt versions by using qmlTypeId in combination with the the id based overload of QQmlEngine::singletonInstance. The new function has two benefits: It's slighly less to write, and it is a tiny bit faster if only one call to singletonInstance2 is needed.

Outlook

We hope that the new module related functionality will make it easier to work with them. The ability to load QML elements also brings us a step closer to a more seamless integration with the QML type compiler. Currently, usage of the type compiler requires modifying the entry point of your application. In the future, we plan to support qmltc-compiled types transparently in the various loadFromModule functions. Stay tuned for futher updates!



  1. The same function works with QML singletons, too.↩︎

  2. If you cannot cache the singleton itself, but can cache the singleton id and you need to fetch the singleton repeatedly.↩︎


Blog Topics:

Comments