QMLの基礎 - シングルトン

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

オブジェクト指向のプログラム言語のクラスのデザインパターンの1種である。
デザインパターンとは、オブジェクト指向の言語で使用される設計のパターンのことであり、設計時に直面する問題とその解決策を整理してまとめたものを指す。

シングルトンは、クラスの設計方法の1つであり、クラスのインスタンスを2つ以上生成できないようにすることで、常に同一のインスタンスが参照されるデザインパターンとなる。
例えば、インスタンスの状態を常に持つ場合やクラス端で共通のデータとしてアクセスする必要がある場合に使用される。


前提条件

QQmlEngineヘッダファイルをインクルードする必要がある。
また、qmakeを使用する場合は、変数QTにqmlを追加する必要がある。

 // cppファイル
 
 #include <QQmlEngine>


# qmakeファイル

QT += qml



QML_SINGLETONマクロ

QML_SINGLETONマクロとは

QML_SINGLETONマクロは、QML環境において、C++クラスをQMLシングルトンとして露出させるために使用する。
QML_SINGLETONマクロを使用することにより、QMLコードからグローバルに利用可能な単一のインスタンスを生成することができる。

主な目的は、アプリケーション全体で共有される状態やサービスを提供することである。
例えば、設定管理、ログ記録、ネットワーク接続管理等のグローバルサービスを実装する場合に非常に有効である。

QMLにおいて、内包する型がシングルトンであることを宣言するには、QObjectクラスを継承したクラスを記述して、QML_SINGLETONマクロを使用する。
これは、そのクラスがQ_OBJECTマクロが記述されており、QMLで利用可能である場合(QML_ELEMENTマクロまたはQML_NAMED_ELEMENT()マクロを持つ場合)にのみ有効である。

  • Qt 5の場合
    QQmlEngineクラスは、その型に最初にアクセスした時、その型のデフォルトコンストラクタを使用してシングルトンのインスタンスを生成する。
    もし、デフォルトコンストラクタが存在しない場合は、シングルトンは初期状態ではアクセス不能になるため、以下に示す記述を行う。
    • qmlRegisterSingletonType関数に特定のファクトリー関数を指定する。
    • qmlRegisterSingletonInstance関数に同じクラスで同じ型の名前空間とバージョンの特定のインスタンスを指定して呼び出すことにより、オーバーライドする。


  • Qt 6の場合
    以下に示すタイミングで、シングルトンのインスタンスが生成される。
    • QQmlEngineクラスは、その型に最初にアクセスした時、その型のデフォルトコンストラクタが使用される。
    • デフォルトコンストラクタが存在しない場合は、静的ファクトリー関数(T *create(QQmlEngine*, QJSEngine*))が使用される。

    もし、両方が存在して両方アクセス可能な場合は、デフォルトコンストラクタが優先される。
    ただし、デフォルトコンストラクタも静的ファクトリ関数も存在しない場合は、シングルトンはアクセス不能になることに注意する。


QMLファイル内でシングルトンを使用する場合は、通常のQMLタイプと同様にインポートする。
インスタンス化する必要はなく、直接そのプロパティやメソッドにアクセスすることができる。

※注意
QML_SINGLETONマクロを使用する場合は、クラスがQ_OBJECTマクロも使用している必要がある。
また、シングルトンクラスは少なくとも1つのQ_INVOKABLEメソッド、または、NOTIFIABLEプロパティを持っている必要がある。

デフォルトコンストラクタが存在する場合

デフォルトコンストラクタを持つクラスをシングルトンとして宣言する場合は、QML_SINGLETONマクロを記述する。

 class MySingleton : public QObject
 {
    Q_OBJECT
    QML_ELEMENT
    QML_SINGLETON
 
    // Q_PROPERTY( ... )
 
 public:
    // メンバ変数, メンバ関数, Q_INVOKABLEメソッド等
 };


静的ファクトリー関数が存在する場合

シングルトンクラスがデフォルトで構築不可であり修正可能な場合は、アクセス可能にするため、静的ファクトリー関数を追加する。

 class MySingleton : public QObject
 {
    Q_OBJECT
    QML_ELEMENT
    QML_SINGLETON
 
    // Q_PROPERTY( ... )
 
 public:
    static MySingleton *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine)
    {
       MySingleton *result = nullptr;
       // 何らかのカスタムコンストラクタやファクトリを用いてオブジェクトを生成する
       // QMLエンジンは、最終的にこのオブジェクトの所有権を取得および削除する
       return result;
    }
 
    // メンバ変数 / メンバ関数, Q_INVOKABLEメソッド等
 };


両方存在しない場合

シングルトンクラスが変更不可で、デフォルトコンストラクタや静的ファクトリー関数が存在しない場合、
QML_FOREIGNラッパーを使用して、静的ファクトリー関数を定義することが可能である。

 struct SingletonForeign
 {
    Q_GADGET
    QML_FOREIGN(MySingleton)
    QML_SINGLETON
    QML_NAMED_ELEMENT(MySingleton)
 
 public:
    static MySingleton *create(QQmlEngine *, QJSEngine *engine)
    {
       MySingleton *result = nullptr;
       // 何らかのカスタムコンストラクタやファクトリを用いてインスタンスを生成する
       // QMLエンジンは、最終的にそのインスタンスの所有権を取得および削除する
       return result;
    }
 };


もし、特定のシングルトンオブジェクトを提供する場合、その生成はコントロールできないが、静的ファクトリー関数からそれを返すことができる。
これは、qmlRegisterSingletonInstance関数の代わりとなるものである。

 qmlRegisterSingletonInstance("MyModule", 1, 0, "MySingleton", myObject);


もし、上記のqmlRegisterSingletonInstance("MyModule", 1, 0, "MySingleton", myObject)を記述している場合、MyObjectがMySingleton *型であれば、
代わりに、以下の例のようにクラスを定義することができる。

これは、既存のクラスであるMySingletonクラスは、MySingletonというQMLのシングルトンであることが宣言される。
s_singletonInstanceメンバを設定することにより、使用する前にいつでもそのインスタンスを指定することができるため、MySingletonクラス自身を変更する必要はない。

 struct SingletonForeign
 {
    Q_GADGET
    QML_FOREIGN(MySingleton)
    QML_SINGLETON
    QML_NAMED_ELEMENT(MySingleton)
 
 public:
    // Initialize this using myObject where you would previously
    // call qmlRegisterSingletonInstance().
    inline static MySingleton *s_singletonInstance = nullptr;
 
    static MySingleton *create(QQmlEngine *, QJSEngine *engine)
    {
       // The instance has to exist before it is used. We cannot replace it.
       Q_ASSERT(s_singletonInstance);
 
       // The engine has to have the same thread affinity as the singleton.
       Q_ASSERT(engine->thread() == s_singletonInstance->thread());
 
       // There can only be one engine accessing the singleton.
       if (s_engine)
           Q_ASSERT(engine == s_engine);
       else
           s_engine = engine;
 
        // Explicitly specify C++ ownership so that the engine doesn't delete
        // the instance.
        QJSEngine::setObjectOwnership(s_singletonInstance, QJSEngine::CppOwnership);
 
        return s_singletonInstance;
    }
 
 private:
    inline static QJSEngine *s_engine = nullptr;
 };


※注意
このパターンは、シングルトンが複数のQMLエンジンからアクセスされる場合、または、アクセスするQMLエンジンがシングルトンオブジェクト自身と異なるスレッドアフィニティを持っている場合は動作しない。
上記で示したように、create()メソッドのパラメータでエンジンのIDやスレッドアフィニティを確認することにより、アサーションすることができる。


QML_SINGLETONを使用する場合

使用例 : デフォルトコンストラクタを使用する場合

以下の例では、QML_SINGLETONマクロを使用してConfigServiceをシングルトンクラスとして定義して、アプリケーションのテーマとダークモードの設定を管理している。

C++側では、main関数内でqmlRegisterSingletonType関数を使用して、このシングルトンをQMLエンジンに登録している。

QML側では、ConfigServiceシングルトンを直接使用して、現在の設定の表示やユーザの操作に応じて設定を変更している。
これにより、アプリケーション全体で一貫した設定管理が可能になり、C++とQML間でシームレスにデータを共有することができる。

シングルトンの使用により、コードの重複を避けてアプリケーション全体で一貫したステート管理を実現できる。

 // ConfigService.hファイル
 
 #include <QObject>
 #include <QQmlEngine>
 #include <QSettings>
 
 class ConfigService : public QObject
 {
    Q_OBJECT
    QML_SINGLETON
 
    Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged)
    Q_PROPERTY(bool darkMode READ darkMode WRITE setDarkMode NOTIFY darkModeChanged)
 
 private:
    QString m_theme;
    bool    m_darkMode;
 
 public:
    explicit ConfigService(QObject *parent) : QObject(parent), m_theme("Default"), m_darkMode(false)
    {
       loadSettings();
    }
 
    QString theme() const
    {
       return m_theme;
    }
 
    void setTheme(const QString &theme)
    {
       if (m_theme != theme) {
          m_theme = theme;
          emit themeChanged();
       }
    }
 
    bool darkMode() const
    {
       return m_darkMode;
    }
 
    void setDarkMode(bool darkMode)
    {
       if (m_darkMode != darkMode) {
          m_darkMode = darkMode;
          emit darkModeChanged();
       }
    }
 
    Q_INVOKABLE void saveSettings()
    {
       QSettings settings("MyCompany", "MyApp");
       settings.setValue("theme", m_theme);
       settings.setValue("darkMode", m_darkMode);
    }
 
    Q_INVOKABLE void loadSettings()
    {
       QSettings settings("MyCompany", "MyApp");
       setTheme(settings.value("theme", "Default").toString());
       setDarkMode(settings.value("darkMode", false).toBool());
    }
 
 signals:
    void themeChanged();
    void darkModeChanged();
 };


 // main.cppファイル
 
 #include <QGuiApplication>
 #include <QQmlApplicationEngine>
 #include "ConfigService.h"
 
 int main(int argc, char *argv[])
 {
    QGuiApplication app(argc, argv);
 
    qmlRegisterSingletonType<ConfigService>("com.mycompany.configservice", 1, 0, "ConfigService",
       [](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * {
          Q_UNUSED(engine)
          Q_UNUSED(scriptEngine)
          return new ConfigService();
       });
 
    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
 
    return app.exec();
 }


 // main.qmlファイル
 
 import QtQuick
 import QtQuick.Controls
 import com.mycompany.configservice 1.0
 
 ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: qsTr("Config Service Example")
 
    Column {
       anchors.centerIn: parent
       spacing: 10
 
       Text {
          text: "Current Theme: " + ConfigService.theme
       }
 
       Text {
          text: "Dark Mode: " + (ConfigService.darkMode ? "On" : "Off")
       }
 
       Button {
          text: "Toggle Dark Mode"
          onClicked: {
             ConfigService.darkMode = !ConfigService.darkMode
             ConfigService.saveSettings()
          }
       }
 
       ComboBox {
          model: ["Default", "Light", "Dark"]
          onCurrentTextChanged: {
             ConfigService.theme = currentText
             ConfigService.saveSettings()
          }
       }
    }
 
    Component.onCompleted: {
       ConfigService.loadSettings()
    }
 }


使用例 : 静的ファクトリー関数を使用する場合

QML_SINGLETONマクロは、C++クラスをQMLシングルトンとして登録するために使用する。

デフォルトコンストラクタが存在せず、静的ファクトリー関数が存在する場合、
まず、クラス宣言において、QML_SINGLETONマクロを使用する。
これは通常、クラスのpublicセクションに配置する。

次に、静的ファクトリー関数を定義する。
この関数は、シングルトンインスタンスを作成して返す役割を持つ。
以下の例では、createSingletonという名前の関数を定義している。

QML_SINGLETONマクロの引数には、この静的ファクトリー関数を指定する。
つまり、QML_SINGLETON(<シングルトンクラス名>, <静的ファクトリー関数名>)のような形式となる。
例えば、QML_SINGLETON(MySingleton, createSingleton)となる。

実装する場合には、静的メンバ変数を使用してシングルトンインスタンスを保持して、静的ファクトリー関数内でこのインスタンスを生成・返却する方法が一般的である。

QMLエンジンがシングルトンを必要とするまで、実際のインスタンス生成を遅延させることができる。
これにより、リソースの効率的な利用が可能になる。

※注意
スレッドセーフティに関しても考慮が必要である。
複数のスレッドから同時にアクセスされる可能性がある場合、適切な同期メカニズムを実装することが重要となる。

静的ファクトリー関数を使用することにより、デフォルトコンストラクタを持たないクラスでも、QMLシングルトンとして効果的に機能させることができる。
静的ファクトリー関数を通じて、インスタンスの生成・返却を細かく制御できるため、より柔軟性の高い設計が可能になる。

 // MySingleton.hファイル
 
 #include <QObject>
 #include <QtQml/qqmlregistration.h>
 #include <QMutex>
 #include <QMutexLocker>
 #include <memory>
 #include <QDebug>
 
 class MySingleton : public QObject
 {
    Q_OBJECT
    QML_SINGLETON(MySingleton, createSingleton)
 
 private:
    explicit MySingleton(QObject *parent = nullptr) : QObject(parent) {}
    static std::unique_ptr<MySingleton> instance;  // インスタンスの生存期間を適切に管理する
    QMutex                              m_mutex;
 
    // コピーコンストラクタと代入演算子の無効化
    MySingleton(const MySingleton&) = delete;
    MySingleton& operator=(const MySingleton&) = delete;
 
 public:
    Q_INVOKABLE void doSomething()
    {
       // このメソッドが複数のスレッドから同時に呼ばれても、安全に実行される
       QMutexLocker locker(&m_mutex);
       qDebug() << "MySingleton is doing something!";
    }
 
    static MySingleton *createSingleton(QQmlEngine *engine, QJSEngine *scriptEngine)
    {
       Q_UNUSED(engine)
       Q_UNUSED(scriptEngine)
 
       // 複数のスレッドから同時に呼ばれても、インスタンスが1度だけ生成されることを保証する
       static QMutex mutex;
       QMutexLocker locker(&mutex);
 
       if (!instance) {
          // インスタンスの生存期間を適切に管理する
          instance.reset(new MySingleton());
       }
 
       return instance.get();
    }
 };
 
 // メモリリークを防いで、リソース管理を改善する
 std::unique_ptr<MySingleton> MySingleton::instance = nullptr;


 // main.cppファイル
 
 #include <QGuiApplication>
 #include <QQmlApplicationEngine>
 #include "MySingleton.h"
 
 int main(int argc, char *argv[])
 {
    QGuiApplication app(argc, argv);
 
    // Qt 6では、QMLエンジンが起動時に自動的にタイプを検出・登録する
    // そのため、qmlRegisterSingletonType関数を使用する必要はない
    // この自動登録メカニズムは、プロジェクトが適切に設定されている場合にのみ機能する
 
    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
 
    return app.exec();
 }


 // main.qmlファイル
 
 import QtQuick
 import QtQuick.Window
 
 Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("QML Singleton Example")
 
    Text {
       anchors.centerIn: parent
       text: "Click me!"
       font.pixelSize: 24
 
       MouseArea {
          anchors.fill: parent
          onClicked: {
             MySingleton.doSomething()
          }
       }
    }
 }



QML_SINGLETONを使用しない場合

使用例: 複数のQMLから同一のシングルトンクラスを参照する

以下の例では、2つの画面 (Screen1.qmlとScreen2.qml) から同一のシングルトンクラスを参照する。

これにより、複数のQMLファイルから同一のシングルトンインスタンスにアクセスすることができる。
いずれかの画面にてシングルトンインスタンスのメンバ変数を変更した場合でも、複数の画面に反映される。

このアプローチを使用することにより、複数のQMLファイルから同一のC++シングルトンクラスにアクセスして、データを共有することができる。

まず、C++でシングルトンクラスを定義および生成して、QMLエンジンに登録する必要があるため、
C++でシングルトンクラスを定義する。

 // hoge.cpp
 
 #include "hoge.h"
 
 Hoge::Hoge(QObject *parent) : QObject(parent), m_message("Hello from Hoge!")
 {
 }
 
 Hoge& Hoge::getInstance()
 {
    static Hoge instance;
    return instance;
 }
 
 QString Hoge::message() const
 {
    return m_message;
 }
 
 void Hoge::setMessage(const QString &message)
 {
    if (m_message != message) {
       m_message = message;
       emit messageChanged();
    }
 }


 // hoge.h
 
 #ifndef HOGE_H
 #define HOGE_H
 
 #include <QObject>
 
 class Hoge : public QObject
 {
    Q_OBJECT
    Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged)
 
 private:
     QString m_message;
 
 private:
    Hoge(QObject *parent = nullptr);
    ~Hoge() = default;
    Hoge(const Hoge&) = delete;
    Hoge& operator=(const Hoge&) = delete;
 
 public:
    static Hoge& getInstance();
 
    QString message() const;
    void setMessage(const QString &message);
 
 signals:
    void messageChanged();
 };
 
 #endif


次に、main関数において、QMLエンジンにシングルトンクラスを登録する。

 // main.cpp
 
 #include <QGuiApplication>
 #include <QQmlApplicationEngine>
 #include <QQmlContext>
 #include "hoge.h"
 
 int main(int argc, char *argv[])
 {
    QGuiApplication app(argc, argv);
 
    QQmlApplicationEngine engine;
 
    // シングルトンクラスをQMLに登録
    engine.rootContext()->setContextProperty("hoge", &Hoge::getInstance());
 
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
 
    return app.exec();
 }


複数のQMLファイルからシングルトンクラスを使用する。

 // Screen1.qml
 
 import QtQuick
 import QtQuick.Controls
 
 Item {
    width: 300
    height: 200
 
    Text {
        anchors.centerIn: parent
        text: hoge.message
    }
 
    Button {
        anchors.top: parent.top
        anchors.horizontalCenter: parent.horizontalCenter
        text: "Set Message from Screen1"
        onClicked: hoge.setMessage("Message set from Screen1")
    }
 }


 // Screen2.qml
 
 import QtQuick
 import QtQuick.Controls
 
 Item {
    width: 300
    height: 200
 
    Text {
        anchors.centerIn: parent
        text: hoge.message
    }
 
    Button {
        anchors.bottom: parent.bottom
        anchors.horizontalCenter: parent.horizontalCenter
        text: "Set Message from Screen2"
        onClicked: hoge.setMessage("Message set from Screen2")
    }
 }


最後に、メイン画面 (main.qml) において、上記の2つの画面を表示する。

 // main.qml
 
 import QtQuick
 import QtQuick.Window
 
 Window {
    width: 600
    height: 400
    visible: true
    title: qsTr("Hoge Singleton Example")
 
    Row {
       anchors.fill: parent
 
       Screen1 {
          width: parent.width / 2
          height: parent.height
       }
 
       Screen2 {
          width: parent.width / 2
          height: parent.height
       }
    }
 }