Creating a QML Plugin: Here's How!

Photo by ConvertKit on Unsplash

Creating a QML Plugin: Here's How!

2/3 Building and Publishing Qt-Framework Plugins with GitHub-Actions step by step

ยท

8 min read

QML plugins are a great way to package QML files and C++ code into a single library and then use it in other applications. This allows us to modularize our applications and create reusable code. In this article, we will focus on how to create a QML plugin that provides an API in another application.

On the part of the QT framework, there is unfortunately currently no adequate example for the use of this great tool, which is why this article will potentially help some developers.

The QQmlExtensionPlugin Class

To create a QML plugin, we need a class that is derived from QQmlExtensionPlugin called e. g. ClockPlugin. As we derive from this class we also have to use the Q_OBJECT macro. So we can take advantage of the Qt's meta-object system I won't go deeper in this article.

#include <QQmlExtensionPlugin>

class ClockPlugin : public QQmlExtensionPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)

public:
    void registerTypes(const char* uri) {}
};

This class provides us with two functions that we need to implement: registerTypes() and initializeEngine().

registerTypes() is called by the Qt framework when the plugin is loaded. It is responsible for registering the custom QML types provided by the plugin. In this case we just leave this function empty as we don't have any custom C++ types.

๐Ÿ’ก
The qmlRegisterType() method allows you to make your C++ classes available as QML types, so they can be instantiated and used directly in QML code.

initializeEngine() is called by the Qt framework after the QML engine has been created but before any QML components are loaded. So, it provides an opportunity to perform any necessary initialization steps specific to your plugin or QML environment. You could e. g. register singletons, or perform other tasks required for your plugin to work correctly.

By using Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid), you are telling the Qt plugin system that your plugin implements the QQmlExtensionInterface, which is required for QML extension plugins. This allows the Qt framework to load and manage your plugin correctly.

Creating a qmldir File

In addition to the QQmlExtensionPlugin class, we also need a qmldir file. This file describes the QML files that are included in our plugin and is used by Qt to load the plugin. So basically it informs Qt which functionality the plugin has and which components it is using.

module ClockPlugin
ClockPlugin 1.0 Clock.qml
plugin clockplugin

module ClockPlugin states the module name. This name is used to group the components of the plugin and to avoid conflicts with other modules. ClockPlugin 1.0 Clock.qml defines a component of the plugin with the name "ClockPlugin" and version "1.0". "Clock.qml" is the file in which the plugin is implemented.

In summary, the directory structure for the part that is necessary for the plugin itself looks like this:

clock_plugin
|__ qml
  ...
  |__ Clock
    qmldir
    Clock.qml
    GenericClockHand.qml
    ...
|__ src
clock_plugin.pro

It is important, that the qmldir file is located in the directory of the plugin, (here: where Clock.qml is located) as the qml engine will only search in this directory.

๐Ÿ’ก
The qmldir file is not visible inside of the editor by default. If you want to see it, you have to manually add it.

Explanation of the .pro File

To compile our plugin, we also need a .pro file. This file describes which files need to be compiled and how they need to be compiled. In this section, we will go through the .pro file line by line and explain which settings are necessary to compile the plugin correctly.

๐Ÿ’ก
I use QMake to structure the project instead of CMake as in newer versions.
QT += qml # 1
TEMPLATE = lib # 2
CONFIG += qt plugin qmltypes # 3

QML_IMPORT_NAME = ClockPlugin # 4
QML_IMPORT_MAJOR_VERSION = 1 # 5

DESTDIR = imports/$$QML_IMPORT_NAME # 6
TARGET  = clockplugin # 7

RESOURCES += qml/plugin_qml.qrc # 8

# 9
SOURCES += \
    src/ClockPlugin.cpp

# 10
HEADERS += \
    src/ClockPlugin.h

# copying dependecies of the plugin in the output-folder
PLUGIN_DEPENDENCY_DIRECTORY = $$PWD/qml/Clock # 11
# 12
PluginDependencies.commands = $(COPY_DIR) $$shell_path($$PLUGIN_DEPENDENCY_DIRECTORY) $$shell_path($$DESTDIR)

first.depends = $(first) PluginDependencies # 13
export(first.depends) # 14
export(PluginDependencies) # 15
QMAKE_EXTRA_TARGETS += first PluginDependencies # 16
  1. #1 to #3 sets some basic configuration parameters like that we want to use QML (#1) or that this project should be treated as a library (#2).

  2. #4 and #5 is important, as we have to set, how the library should be imported when we want to use it.

  3. #6 says where the target file should be located, whereas #7 specifies the name of the target file. (default: name of the .pro-file).

  4. #8 adds the qrc (Qt Resource Collection) file to the project. This file contains the compiled QML resources.

  5. #9 and #10 add the source files of the project.

  6. #11 defines the directory path for the plugin dependencies in its own variable for better readability. $$PWD defines the full path to the directory of the current file. I used this predefined variable so that this approach can work for everyone.

  7. #12 defines the command to copy the plugin dependencies from the specified directory to the output directory.

  8. #13 specifies that the target first depends on both the first target itself and the PluginDependencies target.

  9. #14 and #15 export both dependencies to be used by other parts of the build system.

  10. #16 adds both targets to the build system.

๐Ÿ’ก
Generally, every predefined QMake variable is documented.

Deployment: Ensuring Your Plugin Loads Correctly

If we want to use our plugin in other applications, we need to ensure that all dependencies of the plugin are also present.

When we compile the plugin project, we automatically create a shadow build directory. (typical name: build-clock_plugin-Desktop_Qt_6_2_4_MinGW_64_bit-Debug) In this directory all build files according to the plugin can be found. So also the directory imports, in which the defined plugin with all dependencies is present inside of a subdirectory. The name of the subdirectory is determined by QML_IMPORT_NAME. (in this case: "ClockPlugin")

Now we can copy the folder of the plugin into the debug build directory ./{shadow-build}/debug/ of another project. To use the plugin inside of the project we just have to import it in QML.

...
import ClockPlugin 1.0

Window
{
    width: 640
    height: 480
    visible: true
    title: qsTr("TestingApplication")

    ClockPlugin
    {
        id: clock
        anchors.centerIn: parent
    }
}
๐Ÿ’ก
Notice, that we have to use the QML_IMPORT_NAME and QML_IMPORT_MAJOR_VERSION to import the module.

If we now start the test project in debug mode, the plugin is loaded and can be used. At this point, you should see the created plugin inside of your test project.

Using QMake Parameters: Optimizing the Make Behavior

In this section, we will focus on using QMake parameters to change the making behaviour. I will explain the advantages of this method and how it can be used to compile a plugin that can be used both as a standalone application and as a plugin in other applications.

First, we have to enhance the .pro file.

isEqual(BUILD_PLUGIN, "1") {
    {CODE_FOR_BUILDING_ONLY_PLUGIN}
} else {
    QT += quick

    RESOURCES += qml/qml.qrc

    SOURCES += \
        src/main.cpp
}

We use a conditional to check if the self-defined Qmake variable BUILD_PLUGIN is defined. You can define the variable in QtCreator like this:

  1. Navigate to "Projects".

  2. Select the correct kit.

  3. Navigate to "Build Steps".

  4. Click on "qmake".

  5. Click on "Details".

  6. Type in the text field "Extra Arguments" BUILD_PLUGIN=1.

If the variable is defined, we proceed as explained in the previous chapter.

If the variable is not defined, then we start the plugin as a standalone application. For this scenario, we don't need to configure the plugin. But we have to create and load another .qrc file. A .qrc file in Qt is a resource file that allows you to bundle and manage external resources, such as images, icons, and translation files, within your application. It provides a convenient way to organize and access these resources in a cross-platform manner, making it easier to distribute and deploy your Qt application.

So, we do need the QML files of the plugin, but don't need the qmldir file which describes the plugin. As a result, I create a new .qrc file in ./qml/ called qml.qrc.

๐Ÿ’ก
The plugin_qml.qrc file is the same just with an extra file entry for the qmldir.
<RCC>
    <qresource prefix="/">
        <file>main.qml</file>
        <file>Clock/Clock.qml</file>
        <file>Clock/ClockBackground.qml</file>
        <file>Clock/GenericClockHand.qml</file>
        <file>Clock/HourClockHand.qml</file>
        <file>Clock/MinuteClockHand.qml</file>
        <file>Clock/SecondClockHand.qml</file>
        <!-- <file>Clock/qmldir</file> when using the plugin -->
    </qresource>
</RCC>

As a source file, we just need main.cpp as we don't have to use any plugin source files. The advantage of this approach is that we can develop the plugin with the non-plugin (BUILD_PLUGIN=0) build and deploy it with the plugin build (BUILD_PLUGIN=1).

Summary and Next Steps

Now that we have a working QML plugin, we are ready to take the project to the next level. In the next and thus last article in this series, we will look at how to create a CICD pipeline for the automated compilation of our project.

You are also welcome to expand the project. Take a look at the ReadMe of the project on GitHub.

Thank you for reading this article. Hopefully, you gained some knowledge. If you have any suggestions for improving my articles, please feel free to write them in the comments. If you have any further questions or need assistance, do not hesitate to contact me. I am happy to help! โ™ฅ๏ธ

Did you find this article valuable?

Support My journey as a developer ๐Ÿ’ป by becoming a sponsor. Any amount is appreciated!

ย