QML中的单例

开始使用

如何在 QML 中创建单例?

在 QML 中有两种创建单例的方法。您可以在 QML 文件中定义单例,或从 C++ 中注册它。

在 QML 中定义单例

要定义 QML 中的单例,首先必须在文件顶部添加

pragma Singleton

。还有一个步骤:您需要将条目添加到 QML 模块的 qmldir 文件 中。

使用 qt_add_qml_module (CMake)

当使用 CMake 时,qmldir 文件由 qt_add_qml_module 自动创建。要指示应该将 QML 文件转换为单例,您需要在该文件上设置 QT_QML_SINGLETON_TYPE 文件属性

set_source_files_properties(MySingleton.qml
    PROPERTIES QT_QML_SINGLETON_TYPE TRUE)

您一次可以向 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}
)

注意:必须在 qt_add_qml_module 调用之前调用 set_source_files_properties

不使用 qt_add_qml_module

如果您不使用 qt_add_qml_module,则需要手动创建一个 qmldir 文件。在那里,您需要相应地标记您的单例

module MyModule
singleton MySingleton 1.0 MySingleton.qml
singleton MyOtherSingleton 1.0 MyOtherSingleton.qml

有关更多详细信息,请参阅 对象类型声明

在 C++ 中定义单例

从 C++ 中将单例暴露给 QML 有多种方法。主要区别在于当 QML 引擎需要时是否应该创建类的新实例;或者是否需要将某个现有对象暴露给 QML 程序。

将类注册为提供单例

定义单例最简单的方法是有一个可以默认构造的类,它继承自 QObject 并用 QML_SINGLETONQML_ELEMENT 宏进行标记。

class MySingleton : public QObject
{
    Q_OBJECT
    QML_SINGLETON
    QML_ELEMENT
public:
    MySingleton(QObject *parent = nullptr) : QObject(parent) {
        // ...
    }
};

这将在所属的 QML 模块中注册 MySingleton 类,其名为 MySingleton。如果您想用不同的名称暴露,可以使用 QML_NAMED_ELEMENT

如果类无法默认构造,或者需要访问单例实例化的QQmlEngine,可以使用静态创建函数代替。该函数必须具有以下签名:MySingleton *create(QQmlEngine *, QJSEngine *),其中MySingleton是获得注册的类的类型。

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

注意:创建函数接受一个QJSEngine和一个QQmlEngine参数。这是出于历史原因。它们都指向同一个实际上是一个QQmlEngine的对象。

将现有对象公开为单例

有时,你有一个可能通过某些第三方API创建的现有对象。在这种情况下,正确的选择通常是有一个单例,将那些对象作为其属性公开(参见组织相关数据)。但如果情况不是这样,例如,只需要公开一个对象,可以使用以下方法将类型为MySingleton的实例公开给引擎。我们首先将Singleton公开为一个外部类型

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

然后在启动第一个引擎之前,我们设置SingletonForeign::s_singletonInstance

SingletonForeign::s_singletonInstance = getSingletonInstance();
QQmlApplicationEngine engine;
engine.loadFromModule("MyModule", "Main");

注意:在这种情况下简单使用qmlRegisterSingletonInstance可能很有诱惑力。然而,要当心下一个部分中列出的强制性类型注册的陷阱。

强制性类型注册

在Qt 5.15之前,所有类型,包括单例,都是通过qmlRegisterType API进行注册的。具体来说,单例是通过qmlRegisterSingletonTypeqmlRegisterSingletonInstance进行注册的。除了为每种类型必须重复模块名称的轻微不便和强制将类声明与其注册解耦之外,该方法的重大问题在于它对工具不友好:在编译时无法静态提取有关模块类型的所有必要信息。声明性注册解决了这个问题。

注意:存在一个额外的用例用于强制qmlRegisterType API:它是通过基于QJSValue的重载qmlRegisterSingletonType将非QObject类型的单例作为var属性公开的方式。建议使用替代方案:将值公开为基于(QObject)单例的属性,以便可用类型信息。

访问单例

单例可以从QML以及从C++访问。在QML中,您需要导入包含模块。之后,您可以通过其名称访问单例。在JavaScript上下文中读取其属性和写入它们的方式与普通对象相同

import QtQuick
import MyModule

Item {
    x: MySingleton.posX
    Component.onCompleted: MySingleton.ready = true;
}

在单例属性上设置绑定是不可能的;但是,如果需要,可以使用绑定元素来达到相同的结果

import QtQuick
import MyModule

Item {
    id: root
    Binding {
        target: MySingleton
        property: "posX"
        value: root.x
    }
}

注意:在单例属性上安装绑定时必须小心:如果由多个文件执行,结果是不确定的。

关于(不)使用单例的指南

单例允许您将需要在多个地方访问的数据公开给引擎。这可以是全局共享设置,如元素之间的间距,或者需要在多个地方显示的数据模型。与可以解决类似用例的上下文属性相比,它们具有以下优势:类型化、受QML 语言服务器等工具支持,并且在运行时通常更快。

建议不要在模块中注册太多的单例:单例一旦创建,就会一直存在,直到引擎本身被销毁。由于它们是全局状态的一部分,因此具有共享状态的缺点。因此,考虑使用以下技术来减少应用程序中的单例数量

为每个要公开的对象添加单例会增加很多样板代码。大多数时候,将您要公开的数据作为单一单例的属性分组在一起更有意义。例如,如果您想创建一个电子书阅读器,其中需要公开三个抽象模型,一个用于本地书籍,两个用于远程来源,那么您不需要三次重复公开现有对象的过程,而可以创建一个单例并在启动主应用程序之前设置它。

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

使用对象实例

在上一个章节中,我们有了一个将三个模型作为单例成员公开的示例。当模型需要在多个地方使用,或者由我们无法控制的某些外部 API 提供时,这可能会很有用。然而,如果我们只需要在单个地方使用模型,那么将它们作为可实例化的类型更有意义。回到之前的例子,我们可以添加一个可实例化的 RemoteBookModel 类,然后在电子书浏览器 QML 文件中实例化它。

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

传递初始状态

虽然单例可以用来将状态传递给 QML,但在状态只为应用程序的初始设置所需时,它们是浪费的。在这种情况下,通常可以使用 QQmlApplicationEngine::setInitialProperties。例如,您可能希望将 Window::visibility 设置为全屏,如果相应的命令行标志已被设置。

QQmlApplicationEngine engine;
if (parser.isSet(fullScreenOption)) {
    // assumes root item is ApplicationWindow
    engine.setInitialProperties(
        { "visibility", QVariant::fromValue(QWindow::FullScreen)}
    );
}
engine.loadFromModule("MyModule, "Main");

© 2024 The Qt Company Ltd. 本文档中的文档贡献属于其各自的拥有者。本提供的文档根据自由软件基金会在 GNU 自由文档许可证版本 1.3 的条款进行许可。Qt 和其相应的标志是芬兰的 The Qt Company Ltd. 以及/或全球其他国家的商标。所有其他商标均为其各自所有者的财产。