Singletons
Design Pattern: Singletons
The Singleton design pattern is a useful software design pattern for Qt/QML applications that need access to certain services or logic-heavy backend components.
My favorite explanation of the problem solved by the singleton design pattern:
The application needs one, and only one, instance of an object. Additionally, lazy initialization and global access are necessary.
By SourceMaking.
Method 1: Exposing a Qt C++ Class as a QML Singleton
Qt's preferred way to expose a Qt C++ Class as a QML Singleton is by using qmlRegisterSingletonType
which is fairly well documented.
This method can be used to register a singleton provider (basically an instance()
method with a different signature) of a certain class to a uri which can then be imported in QML through a QQuickExtensionPlugin
.
The singleton provider, or instance method, is called by the QQmlEngine
when the singleton is first used in QML – and not when the import is first used.
Because of this behavior, it is very difficult to precisely control the instantiation of the singleton object.
In this example, we are using a singleton called Theme
to build up a custom Text
element:
import QtQuick 2.7
import QmlGuide 1.0
Text {
font.pixelSize: Theme.adjustedFontSize(24)
font.family: Theme.fontFamily
}
The singleton called Theme
is under the QmlGuide 1.0
import. This import is where we want to register our Theme
class:
qmlRegisterSingletonType("QmlGuide", 1, 0, "Theme", Theme::singletonProvider);
The last argument in the qmlRegisterSingletonType
macro is a callback, defined as QObject *(*callback)(QQmlEngine *, QJSEngine *)
.
This callback is invoked the first time the Theme
singleton is used, i.e. in the example above, the Theme::singletonProvider
callback is executed when font.pixelSize
is set to Theme.adjustedFontSize(24)
.
In this example, the Theme::singletonProvider
callback would never be executed, and therefore, the Theme
object would never be created, even though import QmlGuide 1.0
is specified:
import QtQuick 2.7
import QmlGuide 1.0
Text {
}
This can potentially lead to dangerous code follies, as the simplest case of a developer logging the Theme
object can create the singleton:
import QtQuick 2.7
import QmlGuide 1.0
Text {
Component.onCompleted: {
console.log("Theme: ", Theme);
}
}
Generally, once a developer sees the Theme
singleton being logged in the example above – they will automatically assume the Theme
object is being initialized correctly. However, once the console.log
statement is removed, the Theme
object is no longer initialized.
For most users, this behavior of extreme lazy instantiation is perfectly okay. For those users who are focused on performance optimization or application start up times, may find this a PITA.
Method 2: Using the QmlEngine's root context to set context properties
This method counters method #1 in stating clearly that:
I do not want the QmlEngine to manage my singletons – I will manage them myself.
QmlContext
allows us to manipulate the context hierarchy of a tree of elements within a QmlEngine.
This feature can be used to add global properties to all QML files within a single QmlEngine
. This allows us to replicate the behavior of a singleton by exposing a QmlContext
property through the QmlEngine::rootContext
In the Theme
class, we can define a registerSingleton(..)
method:
...
void Theme::registerSingleton(QQmlEngine *qmlEngine)
{
if (!s_instance) {
s_instance = new Theme(qmlEngine);
}
QQmlContext *rootContext = qmlEngine->rootContext();
rootContext->setContextProperty("Theme", s_instance);
}
...
Now, in main.cpp
, we can invoke this method by passing in a reference to the QQmlEngine
object:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Theme::registerSingleton(&engine);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
Now, we can use the Theme
object in QML:
...
Text {
anchors.centerIn: parent
font.pixelSize: 42
font.family: Theme.fontFamily
text: "Hello World"
}
...
Note that the import QmlGuide 1.0
statement is no longer necessary as the Theme
object was registered to the rootContext
of the QQmlEngine
which means it is available in all QML files that are built by this QQmlEngine
.
As can be seen, the statement:
Theme::registerSingleton(&engine);
can be invoked in any order the developer sees fit. This helps when tuning an application for startup and even for runtime performance. If a QML developer tries to use the Theme
object before it is instantiated, a warning will be output. And as always, warnings guide developers between good practices and evil ones.
Sample code is available in the qml.guide GitHub repository.
Subscribe to QML Guide
Get the latest posts delivered right to your inbox