Implicit Imports vs. QML Modules in Qt 6

Several versions of Qt have been released since my last treatise on QML modules. Most of it is in fact still very valid advice, but I feel I have to stress a few things that people often misunderstand.

Turning Pickles into Chocolate with QML

As you may know, each QML document has an implicit import. Other QML documents in the same directory can be used without importing anything. This is a very useful feature that greatly reduces the amount of boiler plate you need to write. You generally don't have to import the module a QML file belongs to itself.

This feature is distinctively less useful if the implicit import does not give you the module your file belongs to. Now you will ask me "How can that happen?" and I would like to reply "It doesn't!" However, since I can't go back in time to restrict our CMake API, I have to admit it can happen. Clearly, when writing about QML modules, I did not think of this case. None of the examples exercise it.

Yet, people more imaginative than me knew how to use the CMake API for QML modules in ways it was not intended for. For educational purposes, let's explore what happens then. You mustn't do this without your parents.

So, here is how you make your QML module different from the implicit import of its files:

myProject
    | - CMakeLists.txt
    | - main.cpp
    | - some_qml_type.h
    | - some_qml_type.cpp
    | - qml
        | - main.qml
        | - Pickles.qml

With the above project structure, I assume you have a call to qt_add_qml_module in the CMakeLists.txt, and the QML module defined this way contains main.qml and Pickles.qml. The implicit import of main.qml and Pickles.qml is the "qml" directory. Their module, however, is defined in the "myProject" directory. Now, if you want main.qml or Pickles.qml to use anything declared in some_qml_type.h, you have to explicitly import myProject. Congratulations!

Why is that? Quite simple: main.qml and Pickles.qml will not see a qmldir file in their directory because the qmldir file is one directory up, in myProject. If there is no qmldir file in the same directory, the implicit import is constructed using only the file names of the QML files found there. There is no space for C++-defined types in this case.

And you can have even more fun by adding the following to your CMakeLists.txt:

set_source_files_properties(qml/Pickles.qml PROPERTIES
    QT_QML_SOURCE_TYPENAME Chocolate
)

Now, in main.qml you can use a component called "Pickles" as defined by Pickles.qml next to it, just like before. If you import myProject, though, you do not get a component called "Pickles". Instead you get a component called "Chocolate" with the same contents. main.qml, which doesn't import myProject, does not get to have your Chocolate. Isn't it beautiful?

But we're still not done. Did you know you can have a type name that refers to a singleton when imported via a named module, but to a regular type when accessed via the implicit import inside the same module? I will leave the exact way to do this as an exercise to the reader (under parental observation). Just a hint: It's not as trivial as you think. I will send some pickles to the first one who can post a correct solution in the comments.

All of these effects are features, of course. Enjoy. And don't you dare pestering me with bug reports. So much for the fun. Let's get back to the serious parts.

How did we get here?

The sad truth is that many of the official examples show exactly the structure presented above, with an extra directory called "qml" between the application root and the QML files. Back in Qt5, when it was hard to create proper named QML modules, this was a good way to structure your code and separate your QML files from your C++ code. When porting to CMake, the problem with this structure went undetected for a while. Now, having crept into everybody's projects, it is enshrined in legacy.

People also create such subdirectories on purpose, though. Having an internal structure to your QML module also helps break a large module into smaller parts, improving readability. Those parts should, by definition, be separate modules. However, adding a subdirectory has a lower up front cost than splitting a module. You don't have to write an extra CMakeLists.txt after all, and you don't have to think about the URIs, whether the modules should be dynamic or static, how they should be linked etc. If you never run into the situations described above, splitting a large module might never pay off.

How to fix it?

There are multiple ways to change your project structure so that your QML files' implicit import becomes the same as the module they belong to:

  1. Move the QML files one level up and dissolve the "qml" directory. This is the simplest way to deal with it. However, it also dissolves the internal separation of QML from C++ code that you have probably created for a reason.

  2. Define more QML modules. If the top level directory defines a QML module called "myProject", the "qml" subdirectory can define a QML module called "myProject.qml". It's a matter of adding another CMakeLists.txt in the "qml" directory and an "add_subdirectory(qml)" in the "myProject" directory. If you want to use types from one of those modules in the other one, you have to explicitly import. Arguably, the explicit imports are better than the ambiguities described above (unless you've actually enjoyed, that is). You can have multiple QML modules in the same binary.

  3. Always explicitly import your own module in each file. Mind that this requires that the module can be found in the import path. You should definitely not use NO_RESOURCE_TARGET_PATH in this (or any other) case. And you have to make sure the module is either available from a well-known import path (e.g. ":/qt/qml" or your application directory), or an explicitly defined import path.

  4. Wait for Ulf to come up with a solution. Since these "qml" subdirectories are so widespread, it is time to fix this in a central way, probably via some extra argument to qt_add_qml_module() or via a policy you can enable using qt_standard_project_setup. There are a few ideas floating around, but there is no definite solution, yet. See QTBUG-111763 for the progress on this.

Epilogue

While writing this post I realized that if:

  • your module doesn't have a plugin
  • you are using one of its components as entry point with the URL-based QQmlComponent constructor
  • the QML file you are loading this way is in the module's base directory

then the module's C++-defined types will still not be loaded from the implicit import. Clearly, this is a bug. https://codereview.qt-project.org/c/qt/qtdeclarative/+/541951 fixes it.


Blog Topics:

Comments