In QML, a singleton is an object which is created at most once per engine . In this guide, we'll explain how to create singletons and 如何使用它们 . We'll also provide some best practices for working with singletons.
There are two separate ways of creating singletons in QML. You can either define the singleton in a QML file, or register it from C++.
To define a singleton in QML, you first have to add
pragma Singleton
to the top of your file. There's one more step: You will need to add an entry to the QML module's qmldir 文件 .
When using CMake, the qmldir is automatically created by
qt_add_qml_module
. To indicate that the QML file should be turned into a singleton, you need to set the
QT_QML_SINGLETON_TYPE
file property on it:
set_source_files_properties(MySingleton.qml
PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
You can pass multiple files at once to
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 needs to be called before
qt_add_qml_module
If you aren't using
qt_add_qml_module
, you'll need to manually create a
qmldir 文件
. There, you'll need to mark your singletons accordingly:
module MyModule singleton MySingleton 1.0 MySingleton.qml singleton MyOtherSingleton 1.0 MyOtherSingleton.qml
另请参阅 Object Type Declaration 了解更多细节。
There are multiple ways of exposing singletons to QML from C++. The main difference depends on whether a new instance of a class should be created when needed by the QML engine; or if some existing object needs to be exposed to a QML program.
The simplest way of defining a singleton is to have a default-constructible class, which derives from QObject and mark it with the QML_SINGLETON and QML_ELEMENT 宏。
class MySingleton : public QObject { Q_OBJECT QML_SINGLETON QML_ELEMENT public: MySingleton(QObject *parent = nullptr) : QObject(parent) { // ... } };
This will register the
MySingleton
class under the name
MySingleton
in the QML module to which the file belongs. If you want to expose it under a different name, you can use
QML_NAMED_ELEMENT
代替。
If the class can't be made default-constructible, or if you need access to the
QQmlEngine
in which the singleton is instantiated, it is possible to use a static create function instead. It must have the signature
MySingleton *create(QQmlEngine *, QJSEngine *)
,其中
MySingleton
is the type of the class that gets registered.
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; };
注意: The create function takes both a QJSEngine 和 QQmlEngine parameter. That is for historical reasons. They both point to the same object which is in fact a QQmlEngine .
Sometimes, you have an existing object that might have been created via some third-party API. Often, the right choice in this case is to have one singleton, which exposes those objects as its properties (see
Grouping together related data
). But if that is not the case, for example because there is only a single object that needs to be exposed, use the following approach to expose an instance of type
MySingleton
to the engine. We first expose the Singleton as a
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; };
Then we set
SingletonForeign::s_singletonInstance
before we start the first engine
SingletonForeign::s_singletonInstance = getSingletonInstance(); QQmlApplicationEngine engine; engine.loadFromModule("MyModule", "Main");
注意: It can be very tempting to simply use qmlRegisterSingletonInstance in this case. However, be wary of the pitfalls of imperative type registration listed in the next section.
Before Qt 5.15, all types, including singletons were registered via the
qmlRegisterType
API. Singletons specifically were registered via either
qmlRegisterSingletonType
or
qmlRegisterSingletonInstance
. Besides the minor annoyance of having to repeat the module name for each type and the forced decoupling of the class declaration and its registration, the major problem with that approach was that it is tooling unfriendly: It was not statically possible to extract all the necessary information about the types of a module at compile time. The declarative registration solved this issue.
注意:
There is one remaining use case for the imperative
qmlRegisterType
API: It is a way to expose a singleton of non-
QObject
type as a
var
property via
the QJSValue based
qmlRegisterSingletonType
overload
. Prefer the alternative: Expose that value as the property of a (
QObject
) based singleton, so that type information will be available.
Singletons can be accessed both from QML as well as from C++. In QML, you need to import the containing module. Afterwards, you can access the singleton via its name. Reading its properties and writing to them inside JavaScript contexts is done in the same way as with normal objects:
import QtQuick import MyModule Item { x: MySingleton.posX Component.onCompleted: MySingleton.ready = true; }
Setting up bindings on a singletons properties is not possible; however, if it is needed, a Binding element can be used to achieve the same result:
import QtQuick import MyModule Item { id: root Binding { target: MySingleton property: "posX" value: root.x } }
注意: Care must be taken when installing a binding on a singleton property: If done by more than one file, the results are not defined.
Singletons allow you to expose data which needs to be accessed in multiple places to the engine. That can be globally shared settings, like the spacing between elements, or data models which need to be displayed in multiple places. Compared to context properties which can solve a similar use case, they have the benefit of being typed, being supported by tooling like the QML 语言服务器 , and they are also generally faster at runtime.
It is recommended not to register too many singletons in a module: Singletons, once created, stay alive until the engine itself gets destroyed and come with the drawbacks of shared state as they are part of the global state. Thus consider using the following techniques to reduce the amount of singletons in your application:
Adding one singleton for each object which you want to expose adds quite some boiler plate. Most of the time, it makes more sense to group data you want to expose together as properties of a single singleton. Assume for instance that you want to create an ebook reader where you need to expose three abstract item models , one for local books, and two for remote sources. Instead of repeating the process for exposing existing objects three times, you can instead create one singleton and set it up before starting the main application:
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"); }
In the last section, we had the example of exposing three models as members of a singleton. That can be useful when either the models need to be used in multiple places, or when they are provided by some external API over which we have no control. However, if we need the models only in a single place it might make more sense have them as an instantiable type. Coming back to the previous example, we can add an instantiable RemoteBookModel class, and then instantiate it inside the book browser QML file:
// 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"} } }
While singletons can be used to pass state to QML, they are wasteful when the state is only needed for the initial setup of the application. In that case, it is often possible to use QQmlApplicationEngine::setInitialProperties . You might for instance want to set Window::visibility to fullscreen if a corresponding command line flag has been set:
QQmlApplicationEngine engine; if (parser.isSet(fullScreenOption)) { // assumes root item is ApplicationWindow engine.setInitialProperties( { "visibility", QVariant::fromValue(QWindow::FullScreen)} ); } engine.loadFromModule("MyModule, "Main");