QML 中的单例
在 QML 中,单例是指在每个engine 中至多被创建一次的对象。在本指南中,我们将介绍如何创建单例以及如何使用它们。此外,我们还将提供一些关于使用单例的最佳实践。
如何在 QML 中创建单例?
在 QML 中创建单例有两种不同的方法。您可以选择在 QML 文件中定义单例,或者通过 C++ 进行注册。
在 QML 中定义单例
要在 QML 中定义单例,首先需要在
pragma Singleton到文件开头。还有一步:您需要在 QML 模块的qmldir 文件中添加一条条目。
使用 qt_add_qml_module(CMake)
使用 CMake 时,qt_add_qml_module 会自动创建 qmldir 文件。要指定该 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
不使用 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_SINGLETON` 和 `QML_ELEMENT ` 宏对其进行标记。
class MySingleton : public QObject
{
Q_OBJECT
QML_SINGLETON
QML_ELEMENT
public:
MySingleton(QObject *parent = nullptr) : QObject(parent) {
// ...
}
};这将把MySingleton 类以MySingleton 为名注册到该文件所属的QML模块中。若需以其他名称暴露该类,可改用QML_NAMED_ELEMENT 。
如果该类无法被设为可默认构造,或者您需要访问用于实例化单例的QQmlEngine ,则可以使用静态 create 函数代替。该函数的签名必须为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;
};注意: create函数 同时接受QJSEngine 和QQmlEngine 两个参数。这是出于历史原因。它们都指向同一个对象,该对象实际上是一个QQmlEngine 。
将 C++ 实例化的对象作为单例暴露
有时,从 C++ 控制单例的实例化会很有用,而不是让 `QQmlEngine ` 来实例化该对象。此类单例无需由引擎进行实例化,因此您可以省略默认构造函数或静态 `create ` 函数。若要这样做,您必须在单例声明中包含 `QML_UNCREATABLE ` 宏:
class MyNonDefaultConstructibleSingleton : public QObject
{
Q_OBJECT
QML_SINGLETON
QML_NAMED_ELEMENT(MySingleton)
QML_UNCREATABLE("Provided by C++")
public:
MyNonDefaultConstructibleSingleton(BackendObject* backend, QObject *parent = nullptr)
: QObject(parent)
, m_backend(backend)
{}
private:
class BackendObject* backend;
};然后,在启动引擎之前,我们将要使用的实例设置给引擎:
MyNonDefaultConstructibleSingleton singleton(backend);
QQmlApplicationEngine engine;
engine.setExternalSingletonInstance("MyModule", "MySingleton", &singleton);
engine.loadFromModule("MyModule", "Main");将现有对象作为单例公开
有时,您可能有一个通过某些第三方 API 创建的现有对象。在这种情况下,通常正确的选择是创建一个单例,并将这些对象作为其属性进行暴露(参见“将相关数据分组”)。 但如果情况并非如此——例如,仅需暴露单个对象——请使用以下方法将类型为 `MySingleton ` 的实例暴露给引擎。首先,我们将该单例作为 `foreign type` 进行暴露:
struct SingletonForeign
{
Q_GADGET
QML_FOREIGN(MySingleton)
QML_SINGLETON
QML_NAMED_ELEMENT(MySingleton)
QML_UNCREATABLE("Provided from C++")
};然后,我们只需实例化 MySingleton,并将其设置为引擎的属性:
MySingleton instance = getSingletonInstance();
QQmlApplicationEngine engine;
engine.setExternalSingletonInstance("MyModule", "MySingleton", &instance);
engine.loadFromModule("MyModule", "Main");注意: 在此情况下,直接使用 `qmlRegisterSingletonInstance `可能 非常诱人。但请警惕下一节中列出的命令式类型注册的陷阱。
命令式类型注册
在 Qt 5.15 之前,所有类型(包括单例)都是通过qmlRegisterType API 进行注册的。 单例具体是通过qmlRegisterSingletonType 或qmlRegisterSingletonInstance 进行注册的。除了必须为每个类型重复模块名称这一小麻烦,以及类声明与其注册被迫解耦之外,这种方法的主要问题在于它对开发工具不友好: 在编译时,无法通过静态方式提取模块中所有类型的相关必要信息。声明式注册解决了这一问题。
注意: 命令式qmlRegisterType API仍存在一种 使用场景:它是一种将非QObject 类型的单例作为var 属性暴露出来的方式,通过 the QJSValue based qmlRegisterSingletonType overload 。建议采用替代方案:将该值作为基于 (QObject) 的单例的属性进行暴露,以便获取类型信息。
访问单例
既可以在 QML 中访问单例,也可以在 C++ 中访问。在 QML 中,您需要导入包含该单例的模块。之后,您可以通过其名称访问该单例。在 JavaScript 上下文中读取和写入其属性,与处理普通对象的方式相同:
import QtQuick
import MyModule
Item {
x: MySingleton.posX
Component.onCompleted: MySingleton.ready = true;
}无法直接在单例的属性上设置绑定;但如果需要,可以使用Binding 元素来实现相同的效果:
import QtQuick
import MyModule
Item {
id: root
Binding {
target: MySingleton
property: "posX"
value: root.x
}
}注意: 在单例属性上设置绑定时需格外谨慎 :若多个文件同时进行此操作,结果将无法确定。
使用(或不使用)单例的指南
单例允许您将需要在多个位置访问的数据暴露给引擎。这可以是全局共享的设置(例如元素之间的间距),也可以是需要在多个位置显示的数据模型。与能够解决类似用例的上下文属性相比,单例具有类型化、受工具(如 QML Language Server等工具的支持,且通常在运行时速度更快。
建议不要在模块中注册过多的单例:单例一旦创建,就会一直存在直到引擎本身被销毁,并且由于它们是全局状态的一部分,因此存在共享状态的弊端。因此,请考虑使用以下技术来减少应用程序中的单例数量:
将相关数据分组
为每个需要暴露的对象添加一个单例会产生相当多的冗余代码。大多数情况下,将需要暴露的数据聚合为单个单例的属性更为合理。例如,假设您要创建一个电子书阅读器,需要暴露三个abstract item models ,其中一个用于本地书籍,另外两个用于远程来源。 与其为暴露现有对象重复三次该过程,不如创建一个单例,并在启动主应用程序之前对其进行配置:
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");
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.