QMLの基礎 - シングルトン

提供:MochiuWiki : SUSE, EC, PCB
2024年9月6日 (金) 05:52時点におけるWiki (トーク | 投稿記録)による版 (→‎QML_SINGLETONマクロ)
ナビゲーションに移動 検索に移動

概要

オブジェクト指向のプログラム言語のクラスのデザインパターンの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から同一のシングルトンクラスを参照する

以下の例では、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
       }
    }
 }