写在前面
尽管 QML模块 已经存在很长时间了,它们的使用在 Qt6 之前相当稀少。 qt_add_qml_module
在 Qt6 中,它们变得更加普遍。并且有充分的理由:只有将所有相关的 QML 放在一个模块中,才能使 qmllint
或 Qt Quick 编译器
等工具正常工作。
但是,到目前为止,Qt 自己的 API 的某些部分还不知道 module。与 QML 类型相互作用时,例如通过 QQmlComponent
,您需要使用明确的文件路径。
从 Qt 6.5 开始,现在有一个替代解决方案来利用模块,我们将在此博客文章中介绍。
此外,我们现在还提供了一种解决方案,该解决方案在大多数常见情况下都不会篡改导入路径。
新的默认导入路径
让我们从已添加的新 默认导入路径 开始:您现在可以将模块放在下面 qrc:/qt/qml
,它们将在那里被自动找到。
注意:
应用程序可以通过简单地从资源系统中的路径加载其主要入口点,并依靠隐式导入查找模块的QMLDIR,假设主文件和QMLDIR都是模块的一部分,并且放置在同一文件夹中。我们将在下面看到为什么该解决方案有其自己的缺点,对于库来说,无论如何,它从来都不是可行的。
CMake 集成
很遗憾, qt_add_qml_module
一直在使用 /
作为到目前为止资源前缀的默认值,我们无法静默地更改它。
幸运的是,Qt 6.5 还带来了对 Qt CMake 策略的支持,它允许以一种受控的方式来改变我们的 CMake API 中的默认值。
当未提供显式值时,启用 QTP0001 设置为默认值。
启用此策略的最简单方法是调用:
qt_standard_project_setup(REQUIRES 6.5)
传递到 qt_standard_project_setup
将在全局范围内启用所有引入 Qt 6.5 的新策略。这恰好是关于资源前缀的问题。
有关控制策略的更多详细信息,请参阅其文档和 qt_policy 命令的文档。
如果正在使用 QMake
如果您正在使用一个项目 qmake
,您仍然可以通过在新的默认导入路径中受益 RESOURCES变量 。
QML_IMPORT_NAME = MyModule
QML_IMPORT_MAJOR_VERSION = 1
SOURCES += \
main.cpp
HEADERS += \
filesystemmodel.h
qml_resources.files = \
qmldir \
Main.qml \
MyType.qml
qml_resources.prefix = /qt/qml/MyModule
RESOURCES += qml_resources
通过此设置,引擎可以在运行时找到该模块,而无需进行任何进一步的设置。
模块感知 API
与 QML 模块相关的第二个实质性变化是一组新的 API,用于与 QML 类型的交互,包括来自 QML 和来自 C++ 的 QML 类型。
加载组件:到目前为止的状态
在我们深入研究新 API 之前,让我们首先看看一个简单的 QML 应用程序项目以前的样子。
我们具有以下目录结构:
helloqml
├── CMakeLists.txt
├── main.cpp
└── main.qml
CMakelists.txt 看起来像:
cmake_minimum_required(VERSION 3.21)
project(helloqml VERSION 0.1 LANGUAGES CXX)
find_package(Qt6 6.5 COMPONENTS Quick REQUIRED)
qt_standard_project_setup(REQUIRES 6.5)
qt_add_executable(helloqmlapp
main.cpp
)
qt_add_qml_module(helloqmlapp
URI helloqml
QML_FILES main.qml
)
target_link_libraries(helloqmlapp
PRIVATE Qt6::Quick)
请注意,我们已经在使用上述新的默认导入路径, main.cpp 包含
#include <QGuiApplication>
#include <QQmlApplicationEngine>
using namespace Qt::Literals::StringLiterals;
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
const QUrl url(u "qrc:/qt/qml/helloqml/main.qml" _s);
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
&app, []() { QCoreApplication::exit(-1); },
Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
最后,main.qml 包含
import QtQuick
Window {
width: 640
height: 480
visible: true
title: qsTr( "Hello World" )
}
值得注意的是 const QUrl url(u"qrc:/qt/qml/helloqml/main.qml"_s);
,这里有多个陷阱:
首先是因为我们正在处理QML模块,所以 main.qml
现在已放置在 HelloQML 文件夹下(请记住,QML 模块必须位于与模块 URI 相对应的文件夹中的导入路径之一)。
第二个陷阱是 qt_add_qml_module
有助于将 QML 文件放入资源系统中,如果我们想避免潜在的慢速文件系统操作,我们仍然必须记住从那里加载而不是直接从文件系统加载(并确保执行 AOT 编译的绑定,而不是在运行时解释)。
另一方面,如果我们确实想在开发过程中从普通文件系统加载(以便在不必重新编译的情况下更快地迭代我们的 QML 文件),我们将不可能实现,因为现在我们已经对路径进行了硬编码。
在 Qt 6.5 中,我们有了一个避免所有问题的新方式。
加载组件:新方法
使用QML模块,我们知道给定的 QML 元素可以通过其模块的名称和类型名称来识别。
因此,让我们使用它们:我们将 main.qml
重命名为 Main.qml
(默认情况下只有大写文件会导致导出的 QML 类型),并将 main.cpp 更改为使用。
我们不再需要关心文件的确切位置,并且从资源文件系统的加载在后台处理,但可以做的不仅仅是替换现有调用,它还实现了一些以前不可能的事情:不要求我们加载的 QML 元素是一个文件。
相反,它支持可以在 QML 中创建的任何元素,这包括:
-
由文件支持的元素(正如我们已经看到的)
-
内联部分:
// Outer.qml, in module MyModule Item { component Inner : Rectangle { color: "red" } }
我们可以通过
engine.loadFromModule("MyModule", "Outer.Inner")
。 -
和 C++ 定义的类型:
// part of MyModule struct MyFancyItem : public QQuickItem { QML_ELEMENT // ... }
我们可以通过
engine.loadFromModule("MyModule", "MyFancyItem")
。
之前,最后两种情况需要创建一个 QML 包装器文件来实例化所需元素。
此外,以上不限于 QQmlApplicationEngine
,最后,还有一个新的重载,暴露了 QML 中的功能:
import QtQuick
ListView {
model: 10
delegate: Qt.createComponent( "MyModule" , "MyFancyItem" )
}
单例
到目前为止,该API用于创建新对象,因此它不适合 QML 单例。然而,为了保持一致性,使用单例也存在新的重载。这在设置某些应用程序全局数据(例如公开 QAbstractItemModel 或从网络获取的某些应用程序配置数据)时非常有用:给定 C++ 中定义的单例:QQmlEngine::singletonInstance
class Globals : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(QAbstractItemModel *model READ model WRITE setModel NOTIFY modelChanged)
//...
}
然后我们可以写出:
QQmlApplicationEngine engine;
// access the singleton
auto globals = engine.singletonInstance<Globals>( "MyModule" , "Globals" );
// set up global state
globals.setModel(fancyModel);
// start the application after the initial setup
engine.loadFromModule( "MyModule" , "Main" )
请注意,即使在以前的 Qt 版本中,也可以通过将 qmlTypeId
与基于 id
的重载结合使用来做到这一点。
新函数有两个好处:写入速度略低,如果只需要调用一次,速度会快一点。