QMLの基礎 - シングルトン
概要
オブジェクト指向のプログラム言語のクラスのデザインパターンの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
}
}
}