QML 中的单例
在 QML 中,单例是指每个engine 最多创建一次的对象。在本指南中,我们将解释如何创建单例以及如何使用它们。我们还将提供一些使用单子的最佳实践。
如何在 QML 中创建 singletons?
在 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
file 属性:
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} )
注意: 需要先调用 set_source_files_properties 属性,然后再调用 qt_add_qML_module。 qt_add_qml_module
如果没有使用 qt_add_qml_module
如果不使用qt_add_qml_module
,则需要手动创建一个qmldir 文件。在那里,你需要相应地标记你的 singletons:
module MyModule singleton MySingleton 1.0 MySingleton.qml singleton MyOtherSingleton 1.0 MyOtherSingleton.qml
详情另请参阅对象类型声明。
在 C++ 中定义单子
从 C++ 向 QML 公开 singletons 有多种方法。主要区别在于 QML 引擎需要时,是否要创建一个类的新实例;或者是否需要向 QML 程序公开一些现有对象。
注册类以提供单子
定义单例的最简单方法是建立一个默认的可构造类(default-constructible class),它派生自QObject ,并用QML_SINGLETON 和QML_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; };
注意: create 函数需要一个QJSEngine 和一个QQmlEngine 参数。这是历史原因造成的。它们都指向同一个对象,而这个对象实际上是QQmlEngine 。
将现有对象公开为单例
有时,您可能有一个通过第三方 API 创建的现有对象。在这种情况下,正确的选择通常是使用一个单例,将这些对象作为其属性公开(请参阅相关数据的分组)。但如果情况并非如此,例如只需公开一个对象,则可使用以下方法向引擎公开MySingleton
类型的实例。我们首先将单例作为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; };
然后在启动第一个引擎前设置SingletonForeign::s_singletonInstance
SingletonForeign::s_singletonInstance = getSingletonInstance(); QQmlApplicationEngine engine; 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");
© 2025 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.