QML

Плагины QML: создание собственных расширений через QQmlExtensionPlugin

Введение

Помимо плагинов общего назначения, рассмотренных в предыдущих статьях, у Qt Quick есть собственный специализированный механизм плагинов — QQmlExtensionPlugin, позволяющий упаковать набор QML-типов (C++ классы, зарегистрированные для использования в QML) в отдельный, динамически подключаемый модуль, импортируемый в QML через обычный import. В статье разберём, как создать такой плагин и почему этот механизм отличается от общего QPluginLoader.

Концепция

QQmlExtensionPlugin — специализированный базовый класс именно для QML-модулей: вместо ручной загрузки через QPluginLoader и явного qobject_cast, движок QML сам обнаруживает и загружает такие плагины при обработке директивы import в QML-файле, если модуль с соответствующим именем установлен в одном из путей поиска QML-модулей (QML_IMPORT_PATH или стандартные системные пути). Современный подход (Qt 6, с CMake-функцией qt_add_qml_module) автоматизирует значительную часть ручной работы по сравнению с более старым, полностью ручным API QQmlExtensionPlugin.

Пример кода

// temperaturesensor.h — C++ тип, который будет доступен в QML после импорта модуля
#pragma once
#include <QObject>
#include <QQmlEngine>

class TemperatureSensor : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(double celsius READ celsius NOTIFY celsiusChanged)

public:
    explicit TemperatureSensor(QObject *parent = nullptr) : QObject(parent) {}

    double celsius() const { return m_celsius; }

    Q_INVOKABLE void simulateReading(double value)
    {
        m_celsius = value;
        emit celsiusChanged();
    }

signals:
    void celsiusChanged();

private:
    double m_celsius = 0.0;
};
# CMakeLists.txt — плагин QML-модуля через современный API Qt 6
find_package(Qt6 REQUIRED COMPONENTS Qml)
qt_standard_project_setup()

qt_add_qml_module(SensorsPlugin
    URI Hardware.Sensors
    VERSION 1.0
    SHARED                          # компилируется как динамический плагин
    SOURCES
        temperaturesensor.h
    QML_FILES
        SensorPanel.qml             # можно также добавлять обычные QML-компоненты в модуль
)
// SensorPanel.qml — QML-компонент, включённый в тот же модуль плагина
import QtQuick

Rectangle {
    id: root
    width: 200; height: 80
    property alias sensor: temperatureSensor

    TemperatureSensor { id: temperatureSensor }

    Text {
        anchors.centerIn: parent
        text: temperatureSensor.celsius.toFixed(1) + " °C"
        font.pixelSize: 24
    }
}
// Использование модуля из любого QML-приложения, установившего этот плагин
import QtQuick
import Hardware.Sensors 1.0

Window {
    visible: true
    width: 300; height: 200

    SensorPanel {
        id: panel
        anchors.centerIn: parent
    }

    Timer {
        interval: 1000; running: true; repeat: true
        onTriggered: panel.sensor.simulateReading(20 + Math.random() * 5)
    }
}

Пояснения к коду

qt_add_qml_module(... SHARED ...) — современный, декларативный способ описать QML-модуль как динамический плагин: CMake автоматически генерирует весь необходимый boilerplate-код регистрации (включая реализацию QQmlExtensionPlugin «под капотом», без необходимости писать её вручную), на основе указанных SOURCES (C++ классы с QML_ELEMENT) и QML_FILES (обычные QML-компоненты, тоже включаемые в состав модуля как часть единого пакета).

SensorPanel.qml демонстрирует, что QML-модуль не обязан состоять исключительно из C++ типов — он может включать готовые, написанные на QML компоненты, использующие эти C++ типы внутри себя, что даёт конечным потребителям модуля более высокоуровневый, готовый к использованию виджет, а не только «сырые» примитивы данных. Применение модуля в конечном приложении выглядит как обычный import Hardware.Sensors 1.0, технически не отличимый для пишущего QML-код разработчика от использования стандартных модулей самого Qt Quick.

Подводные камни

  • Путь установки скомпилированного QML-плагина должен точно соответствовать структуре, которую ожидает движок QML для разрешения import (директория, названная в соответствии с URI модуля, содержащая сам бинарный плагин и файл qmldir, описывающий типы модуля) — несоответствие структуры путей приводит к ошибке «module not found» при попытке импорта, даже если сам плагин технически скомпилирован успешно.
  • Версия модуля в import Hardware.Sensors 1.0 должна совпадать с версией, объявленной в VERSION 1.0 при сборке плагина — попытка импортировать несуществующую или несовместимую версию модуля завершится ошибкой на этапе загрузки QML, аналогично версионированию обычных Qt-модулей.
  • Смешивание QML-плагинов (через QQmlExtensionPlugin/qt_add_qml_module) с общими C++ плагинами (через QPluginLoader) для разных целей в одном приложении требует понимания, что это два разных, не пересекающихся механизма с разными жизненными циклами загрузки — QML-плагины загружаются движком QML при разборе import, тогда как обычные плагины (статьи 180–186) загружаются явно вызываемым кодом приложения; путаница между этими двумя подходами — частый источник недопонимания у разработчиков, впервые работающих с расширяемыми Qt Quick приложениями.
  • Статическая линковка QML-плагина (STATIC вместо SHARED в qt_add_qml_module) меняет модель его использования аналогично статическим плагинам общего назначения (статья 182) — типы становятся частью основного бинарника без отдельного файла на диске, что подходит для встраиваемых сценариев или приложений с фиксированным набором модулей, но не позволяет конечному пользователю устанавливать новые QML-модули независимо от пересборки приложения.