QML 单例

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.

如何以 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++.

定义 QML 单例

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 文件 .

使用 qt_add_qml_module (CMake)

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

Without 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 了解更多细节。

定义 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.

注册类以提供单例

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.

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

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 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");