QMLにおけるシングルトン

QMLにおいて、シングルトンとはengine につき一度だけ生成されるオブジェクトのことです。このガイドでは、シングルトンの作成方法とその使い方について説明します。また、シングルトンを扱うためのベストプラクティスも紹介します。

QMLでシングルトンを作るには?

QMLでシングルトンを作成するには2つの方法があります。QMLファイルでシングルトンを定義する方法と、C++からシングルトンを登録する方法です。

QMLでシングルトンを定義する

QMLでシングルトンを定義するには、まずファイルの先頭に

pragma Singleton

をファイルの先頭に追加します。もう一つステップがあります:QMLモジュールのqmldirファイルにエントリを追加する必要があります。

qt_add_qml_module (CMake) を使う

CMakeを使うと、qt_add_qml_moduleによってqmldirが自動的に作成されます。QML ファイルをシングルトンにするためには、QT_QML_SINGLETON_TYPE file プロパティを設定する必要があります:

set_source_files_properties(MySingleton.qml
    PROPERTIES QT_QML_SINGLETON_TYPE TRUE)

一度に複数のファイルをset_source_files_properties に渡すことができます:

set(plain_qml_files
    MyItem1.qml
    MyItem2.qml
    FancyButton.qml
)
set(qml_singletons
    MySingleton.qml
    MyOtherSingleton.qml
)
set_source_files_properties(${qml_singletons}
    PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
qt_add_qml_module(myapp
    URI MyModule
    QML_FILES ${plain_qml_files} ${qml_singletons}
)

注意: set_source_files_properties は事前に呼び出す必要があります。qt_add_qml_module

qt_add_qml_module を使わない場合

qt_add_qml_module を使っていない場合は、手動でqmldir ファイルを作成する必要があります。そこで、シングルトンを適切にマークする必要があります:

module MyModule
singleton MySingleton 1.0 MySingleton.qml
singleton MyOtherSingleton 1.0 MyOtherSingleton.qml

詳しくはオブジェクト型の宣言も参照してください。

C++でのシングルトンの定義

C++からQMLにシングルトンを定義する方法はいくつかあります。主な違いは、QMLエンジンが必要なときに新しいクラスのインスタンスを生成するのか、 既存のオブジェクトをQMLプログラムに公開するのかによるものです。

シングルトンを提供するクラスの登録

シングルトンを定義する最も簡単な方法は、QObject から派生した デフォルトで構成可能なクラスを用意し、QML_SINGLETONQML_ELEMENT マクロでマークすることです。

class MySingleton : public QObject
{
    Q_OBJECT
    QML_SINGLETON
    QML_ELEMENT
public:
    MySingleton(QObject *parent = nullptr) : QObject(parent) {
        // ...
    }
};

これにより、ファイルが属するQMLモジュールにMySingleton クラスがMySingleton という名前で登録されます。別の名前で公開したい場合は、QML_NAMED_ELEMENT を使ってください。

クラスをデフォルトで構築できない場合、あるいはシングルトンがインスタンス化されるQQmlEngine にアクセスする必要がある場合は、代わりに静的な create 関数を使用することができます。この関数のシグネチャはMySingleton *create(QQmlEngine *, QJSEngine *) で、MySingleton は登録されるクラスの型です。

class MyNonDefaultConstructibleSingleton : public QObject
{
    Q_OBJECT
    QML_SINGLETON
    QML_NAMED_ELEMENT(MySingleton)
public:
    MyNonDefaultConstructibleSingleton(QJSValue id, QObject *parent = nullptr)
        : QObject(parent)
        , m_symbol(std::move(id))
    {}

    static MyNonDefaultConstructibleSingleton *create(QQmlEngine *qmlEngine, QJSEngine *)
    {
         return new MyNonDefaultConstructibleSingleton(qmlEngine->newSymbol(u"MySingleton"_s));
    }

private:
    QJSValue m_symbol;
};

注: create関数はQJSEngineQQmlEngine の両方のパラメータを取ります。これは歴史的な理由によるものです。これらは両方とも同じオブジェクトを指し、実際にはQQmlEngine です。

既存のオブジェクトをシングルトンとして公開する

サードパーティのAPIで作成された既存のオブジェクトがある場合があります。多くの場合、この場合の正しい選択は、1つのシングルトンを持ち、それらのオブジェクトをプロパティとして公開することです(関連するデータをグループ化するを参照)。しかし、公開する必要があるオブジェクトが1つだけなど、そうでない場合は、MySingleton 型のインスタンスをエンジンに公開するために、以下のアプローチを使用します。まず、シングルトンをforeign type として公開します:

struct SingletonForeign
{
    Q_GADGET
    QML_FOREIGN(MySingleton)
    QML_SINGLETON
    QML_NAMED_ELEMENT(MySingleton)
public:

    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;
};

次に、最初のエンジンを起動する前にSingletonForeign::s_singletonInstance を設定します。

SingletonForeign::s_singletonInstance = getSingletonInstance();
QQmlApplicationEngine engine;
engine.loadFromModule("MyModule", "Main");

注意: この場合、単純にqmlRegisterSingletonInstance 。しかし、次のセクションで説明する命令型登録の落とし穴に注意してください。

命令型登録

Qt 5.15 以前では、シングルトンを含むすべての型はqmlRegisterType API を使って登録されていました。特にシングルトンはqmlRegisterSingletonTypeqmlRegisterSingletonInstance のどちらかで登録されていました。それぞれの型に対してモジュール名を繰り返さなければならないという小さな煩わしさと、クラス宣言とその登録が強制的に切り離されるということに加えて、このアプローチの大きな問題は、ツールに不親切であるということでした:コンパイル時にモジュールの型に関するすべての必要な情報を静的に抽出することができませんでした。宣言的登録はこの問題を解決した。

注: 命令型qmlRegisterType APIには、残り1つの使用例がある:それは、QObject 型以外のシングルトンを、var プロパティとして the QJSValue based qmlRegisterSingletonType overload.別の方法を選びましょう:その値を (QObject) ベースのシングルトンのプロパティとして公開することで、型情報を利用できるようになります。

シングルトンへのアクセス

シングルトンはQMLからもC++からもアクセスすることができます。QMLでは、そのモジュールをインポートする必要があります。その後、その名前を使ってシングルトンにアクセスすることができます。JavaScript コンテキスト内でのプロパティの読み込みや書き込みは、 通常のオブジェクトと同じように行います:

import QtQuick
import MyModule

Item {
    x: MySingleton.posX
    Component.onCompleted: MySingleton.ready = true;
}

シングルトンプロパティにバインディングを設定することはできませんが、必要であればBinding

import QtQuick
import MyModule

Item {
    id: root
    Binding {
        target: MySingleton
        property: "posX"
        value: root.x
    }
}

注意: シングルトンプロパティにバインディングを設置する際には注意が必要です:複数のファイルで行われた場合、結果は定義されません。

シングルトンを使う(使わない)ためのガイドライン

シングルトンを使用すると、複数の場所でアクセスする必要があるデータをエンジンに公開することができます。それは、要素間の間隔のようなグローバルに共有される設定であったり、複数の場所で表示される必要があるデータモデルであったりします。同様のユースケースを解決できるコンテキスト・プロパティと比較すると、型付きであること、QML言語サーバーのようなツールでサポートされていること、一般的に実行速度が速いことなどの利点があります。

モジュールにシングルトンを登録しすぎないことをお勧めします:シングルトンは一度作成されると、エンジン自体が破壊されるまで生き続け、グローバルステートの一部となるため、共有ステートの欠点があります。したがって、アプリケーション内のシングルトンの量を減らすために、以下のテクニックを使うことを検討してください:

公開したいオブジェクトごとにシングルトンを1つ追加すると、かなりのボイラープレートが追加されます。たいていの場合、公開したいデータを1つのシングルトンのプロパティとしてまとめる方が理にかなっています。例えば、3つのabstract item models 、1つはローカルの書籍、2つはリモートのソースを公開する必要がある電子書籍リーダーを作成したいとします。既存のオブジェクトを公開するプロセスを3回繰り返す代わりに、1つのシングルトンを作成し、メインアプリケーションを起動する前にセットアップすることができます:

class GlobalState : QObject
{
    Q_OBJECT
    QML_ELEMENT
    QML_SINGLETON
    Q_PROPERTY(QAbstractItemModel* localBooks MEMBER localBooks)
    Q_PROPERTY(QAbstractItemModel* digitalStoreFront MEMBER digitalStoreFront)
    Q_PROPERTY(QAbstractItemModel* publicLibrary MEMBER publicLibrary)
public:
    QAbstractItemModel* localBooks;
    QAbstractItemModel* digitalStoreFront;
    QAbstractItemModel* publicLibrary
};

int main() {
    QQmlApplicationEngine engine;
    auto globalState = engine.singletonInstance<GlobalState *>("MyModule", "GlobalState");
    globalState->localBooks = getLocalBooks();
    globalState->digitalStoreFront = setupLoalStoreFront();
    globalState->publicLibrary = accessPublicLibrary();
    engine.loadFromModule("MyModule", "Main");
}

オブジェクト・インスタンスの使用

前のセクションでは、3つのモデルをシングルトンのメンバーとして公開する例を挙げました。これは、モデルを複数の場所で使う必要がある場合や、私たちがコントロールできない外部のAPIから提供される場合に便利です。しかし、モデルが必要な場所が1つだけであれば、インスタンス化可能な型として持つ方が理にかなっているかもしれません。先ほどの例に戻ると、インスタンス化可能な RemoteBookModel クラスを追加し、ブックブラウザの QML ファイル内でインスタンス化することができます:

// remotebookmodel.h
class RemoteBookModel : public QAbstractItemModel
{
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)
    // ...
};

// bookbrowser.qml
Row {
    ListView {
        model: RemoteBookModel { url: "www.public-lib.example"}
    }
    ListView {
        model: RemoteBookModel { url: "www.store-front.example"}
    }
}

初期状態を渡す

シングルトンは QML に状態を渡すために使うことができますが、アプリケーションの初期設定にのみ状態が必要な場合には無駄が多くなります。そのような場合は、QQmlApplicationEngine::setInitialProperties を使うことができます。例えば、対応するコマンドラインフラグが設定されている場合、Window::visibility をフルスクリーンに設定したいかもしれません:

QQmlApplicationEngine engine;
if (parser.isSet(fullScreenOption)) {
    // assumes root item is ApplicationWindow
    engine.setInitialProperties(
        { "visibility", QVariant::fromValue(QWindow::FullScreen)}
    );
}
engine.loadFromModule("MyModule, "Main");

本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。