Пользовательский интерфейс в Qt

QML + C++ singleton: как сделать глобальный объект доступным во всех QML-файлах

Введение

Некоторые объекты — менеджер сетевых запросов, глобальные настройки приложения, текущий авторизованный пользователь — логически существуют в единственном экземпре и должны быть доступны из любого QML-файла без необходимости явно передавать их через свойства компонентов. Qt предоставляет механизм регистрации C++ singleton непосредственно как QML-тип через qmlRegisterSingletonType (или, в современном CMake-based подходе, декларативно через QML_SINGLETON). В статье покажем оба способа.

Концепция

В отличие от обычного qmlRegisterType, который делает класс доступным для создания экземпляров через ИмяТипа { ... } в QML, регистрация singleton делает один заранее существующий (или создаваемый «по требованию» QML-движком) экземпляр доступным через простое обращение к имени типа как к объекту, без блока создания — SomeSingleton.someProperty вместо SomeType { }.someProperty.

Современный подход (Qt 6, с использованием CMake-функции qt_add_qml_module) позволяет объявить singleton декларативно прямо в заголовочном файле через макрос QML_SINGLETON рядом с QML_ELEMENT, без отдельного явного вызова регистрации в main.cpp — система сборки сама генерирует нужный код регистрации.

Пример кода: QML + C++ singleton

// appsession.h
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QString>

class AppSession : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    QML_SINGLETON

    Q_PROPERTY(QString currentUserName READ currentUserName WRITE setCurrentUserName NOTIFY currentUserNameChanged)
    Q_PROPERTY(bool isAuthenticated READ isAuthenticated NOTIFY currentUserNameChanged)

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

    QString currentUserName() const { return m_currentUserName; }
    void setCurrentUserName(const QString &name)
    {
        if (m_currentUserName == name) return;
        m_currentUserName = name;
        emit currentUserNameChanged();
    }

    bool isAuthenticated() const { return !m_currentUserName.isEmpty(); }

    Q_INVOKABLE void logout()
    {
        setCurrentUserName(QString());
    }

signals:
    void currentUserNameChanged();

private:
    QString m_currentUserName;
};
# CMakeLists.txt — современный подход с автоматической генерацией регистрации
qt_add_qml_module(MyApp
    URI App.Session
    VERSION 1.0
    QML_FILES main.qml
    SOURCES appsession.h
)
// main.qml — использование без явного создания экземпляра
import QtQuick
import QtQuick.Controls
import App.Session 1.0

ApplicationWindow {
    visible: true
    width: 300
    height: 200

    Column {
        anchors.centerIn: parent
        spacing: 10

        Text {
            text: AppSession.isAuthenticated
                  ? "Привет, " + AppSession.currentUserName
                  : "Вы не авторизованы"
        }

        Button {
            text: AppSession.isAuthenticated ? "Выйти" : "Войти как Гость"
            onClicked: {
                if (AppSession.isAuthenticated) {
                    AppSession.logout();
                } else {
                    AppSession.currentUserName = "Гость";
                }
            }
        }
    }
}

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

Макросы QML_ELEMENT и QML_SINGLETON, размещённые прямо в классе AppSession, сообщают системе сборки Qt (через инструмент qmltyperegistrar, запускаемый автоматически при qt_add_qml_module), что этот класс должен быть зарегистрирован как singleton-тип в указанном модуле — никакого явного кода регистрации в main.cpp писать не нужно. Это современная замена более старому явному вызову qmlRegisterSingletonType() с лямбдой-фабрикой, создающей экземпляр.

В main.qml обращение к AppSession.currentUserName работает без создания экземпляра типа — движок QML автоматически предоставляет единственный экземпляр при первом обращении к имени типа AppSession, и это же обращение работает идентично из любого другого QML-файла, импортирующего модуль App.Session.

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

  • Владение и время жизни singleton-объекта, созданного QML-движком автоматически. Если конструктор по умолчанию недостаточен (например, объекту нужны зависимости, переданные явно из C++ при старте приложения), декларативный QML_SINGLETON без дополнительной настройки не подходит — в таких случаях нужен явный qmlRegisterSingletonInstance(), передающий уже существующий, заранее сконфигурированный из C++ экземпляр.
  • Singleton, доступный из любого QML-файла, создаёт скрытую глобальную зависимость, аналогично проблеме обычного C++ singleton — компоненты, обращающиеся к AppSession напрямую внутри своей реализации, сложнее тестировать в изоляции и переиспользовать в другом контексте, где такого глобального объекта может не быть.
  • Несколько singleton-инстансов при ошибках конфигурации модуля. Неверная настройка qt_add_qml_module или конфликтующие версии модуля, импортированные с разными номерами версий в разных частях приложения, теоретически могут привести к путанице в том, какой именно экземпляр используется — для надёжности URI и версия модуля должны быть согласованы по всему проекту.
  • Прямое изменение свойств singleton из разных QML-файлов без централизованной логики валидации. Поскольку любой QML-файл может напрямую писать в свойства singleton (как в примере, AppSession.currentUserName = "Гость"), без явного API уровня «действие» (а не прямого присваивания свойства) легко получить несогласованное изменение состояния из разных мест кода без единой точки контроля корректности.