From ed21a9299fff4340a43f744e6282f063e8cb09eb Mon Sep 17 00:00:00 2001 From: Fabian Kosmale Date: Mon, 9 Oct 2023 17:02:20 +0200 Subject: doc: Create QML Singleton in-depth guide Fixes: QTBUG-104546 Task-number: QTBUG-117093 Change-Id: Icbf892524500cdad00386441e65d27ea43b10131 Reviewed-by: Ulf Hermann Reviewed-by: Mitch Curtis --- src/qml/doc/src/cppintegration/definetypes.qdoc | 1 + src/qml/doc/src/qmlfunctions.qdoc | 2 +- src/qml/doc/src/qmlsingletons.qdoc | 340 ++++++++++++++++++++++++ 3 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 src/qml/doc/src/qmlsingletons.qdoc (limited to 'src/qml/doc/src') diff --git a/src/qml/doc/src/cppintegration/definetypes.qdoc b/src/qml/doc/src/cppintegration/definetypes.qdoc index 1701d2766d..d926b17c09 100644 --- a/src/qml/doc/src/cppintegration/definetypes.qdoc +++ b/src/qml/doc/src/cppintegration/definetypes.qdoc @@ -339,6 +339,7 @@ be aware that properties of such a singleton type cannot be bound to. See \l{QML_SINGLETON} for more information on how implement and register a new singleton type, and how to use an existing singleton type. +See \l{Singletons in QML} for more in-depth information about singletons. \note Enum values for registered types in QML should start with a capital. diff --git a/src/qml/doc/src/qmlfunctions.qdoc b/src/qml/doc/src/qmlfunctions.qdoc index 66bdff59f3..b8fc27dfa3 100644 --- a/src/qml/doc/src/qmlfunctions.qdoc +++ b/src/qml/doc/src/qmlfunctions.qdoc @@ -336,7 +336,7 @@ engine in order to assert on that. \sa QML_ELEMENT, QML_NAMED_ELEMENT(), - qmlRegisterSingletonInstance(), QQmlEngine::singletonInstance() + qmlRegisterSingletonInstance(), QQmlEngine::singletonInstance(), {Singletons in QML} */ /*! diff --git a/src/qml/doc/src/qmlsingletons.qdoc b/src/qml/doc/src/qmlsingletons.qdoc new file mode 100644 index 0000000000..a07d0b9d46 --- /dev/null +++ b/src/qml/doc/src/qmlsingletons.qdoc @@ -0,0 +1,340 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! +\page qml-singleton.html +\title Singletons in QML +\brief A guide for using singletons in QML + +In QML, a singleton is an object which is created at most once per +\l{QQmlEngine}{engine}. In this guide, we'll +\l{How can singletons be created in QML}{explain how to create} singletons +and \l{Accessing singletons}{how to use them}. We'll also provide some +\l{Guidelines for using singletons}{best practices} +for working with singletons. + +\section1 How can singletons be created in QML? + +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++. + +\section2 Defining singletons in QML +To define a singleton in QML, you first have to add +\code +pragma singleton +\endcode +to the top of your file. +There's one more step: You will need to add an entry to the QML module's +\l{Module Definition qmldir Files}{qmldir file}. + +\section3 Using qt_add_qml_module (CMake) +When using CMake, the qmldir is automatically created by \l{qt_add_qml_module}. +To indicate that the QML file should be turned into a singleton, you need to set +the \c{QT_QML_SINGLETON_TYPE} +file property on it: +\code +set_source_files_properties(MySingleton.qml + PROPERTIES QT_QML_SINGLETON_TYPE TRUE) +\endcode + +You can pass multiple files at once to \c{set_source_files_properties}: +\code +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} +) +\endcode + +\note set_source_files_properties needs to be called before \c{qt_add_qml_module} + +\section3 Without qt_add_qml_module +If you aren't using \c{qt_add_qml_module}, you'll need to manually create a +\l{Module Definition qmldir Files}{qmldir file}. +There, you'll need to mark your singletons accordingly: +\code +module MyModule +singleton MySingleton 1.0 MySingleton.qml +singleton MyOtherSingleton 1.0 MyOtherSingleton.qml +\endcode +See also \l{Object Type Declaration} for more details. + + +\section2 Defining singletons in C++ + +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. + +\section3 Registering a class to provide singletons + +The simplest way of defining a singleton is to have a default-constructible +class, which derives from QObject and mark it with the \l{QML_SINGLETON} and +\l{QML_ELEMENT} macros. +\code +class MySingleton : public QObject +{ + Q_OBJECT + QML_SINGLETON + QML_ELEMENT +public: + MySingleton(QObject *parent = nullptr) : QObject(parent) { + // ... + } +}; +\endcode +This will register the \c{MySingleton} class under the name \c{MySingleton} in +the QML module to which the file belongs. +If you want to expose it under a different name, you can use \l{QML_NAMED_ELEMENT} +instead. + +If the class can't be made default-constructible, or if you need access to +the \l{QQmlEngine} in which the singleton is instantiated, it is possible to +use a static create function instead. It must have the signature +\c{MySingleton *create(QQmlEngine *, QJSEngine *)}, where \c{MySingleton} is +the type of the class that gets registered. +\code +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; +}; +\endcode + +\note The create function takes both a \l{QJSEngine} and a \l{QQmlEngine} parameter. That is +for historical reasons. They both point to the same object which is in fact a QQmlEngine. + +\section3 Exposing an existing object as a singleton + +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 +\l{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 +\c{MySingleton} to the engine. +We first expose the Singleton as a \l{QML_FOREIGN}{foreign type}: +\code +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; +}; +\endcode +Then we set \c{SingletonForeign::s_singletonInstance} before we start +the first engine +\code +SingletonForeign::s_singletonInstance = getSingletonInstance(); +QQmlApplicationEngine engine; +engine.loadFromModule("MyModule", "Main"); +\endcode + +\note It can be very tempting to simply use \l{qmlRegisterSingletonInstance} in +this case. However, be wary of the pitfalls of imperative type registration +listed in the next section. + +\section3 Imperative type registration +Before Qt 5.15, all types, including singletons were registered via the +\c{qmlRegisterType} API. Singletons specifically were registered via either +\l{qmlRegisterSingletonType} or \l{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. + +\note There is one remaining use case for the imperative \c{qmlRegisterType} API: +It is a way to expose a singleton of non-QObject type as a \c{var} property via +\l{qmlRegisterSingletonType}{the QJSValue based \c{qmlRegisterSingletonType} overload} +. Prefer the alternative: Expose that value as the property of a (\c{QObject}) based +singleton, so that type information will be available. + +\section2 Accessing singletons +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: + +\code +import QtQuick +import MyModule + +Item { + x: MySingleton.posX + Component.onCompleted: MySingleton.ready = true; +} +\endcode + +Setting up bindings on a singletons properties is not possible; however, if it +is needed, a \l{Binding} element can be used to achieve the same result: +\code +import QtQuick +import MyModule + +Item { + id: root + Binding { + target: MySingleton + property: "posX" + value: root.x + } +} +\endcode + +\note Care must be taken when installing a binding on a singleton property: If +done by more than one file, the results are not defined. + +\section1 Guidelines for (not) using singletons + +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 +\l{QML Language Server}, 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: + +\section2 Grouping together related data +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 \l{QAbstractItemModel}{abstract item models}, one +for local books, and two for remote sources. Instead of repeating the process +for \l{Exposing an already existing object as a singleton}{exposing existing objects} +three times, you can instead create one singleton and set it up before starting +the main application: +\code +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("MyModule", "GlobalState"); + globalState->localBooks = getLocalBooks(); + globalState->digitalStoreFront = setupLoalStoreFront(); + globalState->publicLibrary = accessPublicLibrary(); + engine.loadFromModule("MyModule", "Main"); +} +\endcode + +\section2 Use object instances +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: + + +\code +// 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"} + } +} + +\endcode + +\section2 Passing initial state + +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 \l{QQmlApplicationEngine::setInitialProperties}. +You might for instance want to set \l{Window::visibility} to fullscreen if +a corresponding command line flag has been set: +\code +QQmlApplicationEngine engine; +if (parser.isSet(fullScreenOption)) { + // assumes root item is ApplicationWindow + engine.setInitialProperties( + { "visibility", QVariant::fromValue(QWindow::FullScreen)} + ); +} +engine.loadFromModule("MyModule, "Main"); +\endcode + +*/ -- cgit v1.2.3