Qt6.2 中的 QML Modules (译文)

4 篇文章 0 订阅
本文详细介绍了Qt 6.2中QML模块的构建系统API,包括QML模块的基础知识、历史、6.2版本的新特性,如多模块打包、版本控制、自定义目录布局等。文章揭示了Qt 6.2如何简化QML模块的创建和管理,使得QML开发更加高效和便捷。
摘要由CSDN通过智能技术生成

QML Modules

在Qt 6.2中,首次出现了一个全面的构建系统API,允许您将QML模块指定为一个完整的、封装的单元。这是一个显著的改进,但由于QML模块的概念在Qt 5中还很不成熟,甚至经验丰富的QML开发人员现在可能会问“QML模块到底是什么”。在上一篇文章中,我们介绍了用于定义它们的CMake API,只触及了表面。我们将在这篇文章中仔细看看。

The basics

QML模块已经存在很长时间了,至少从Qt 5.0开始。

每个QML模块都有一个qmldir文件。qmldir文件指定一个URI。具有相同uri的多个qmldir文件被认为是同一模块的替代位置。通过这种方式,您可以使您的模块在物理文件系统和资源文件系统中都可用。qmldir文件指定了我们在运行时需要了解的所有比特和片段,甚至更多

  • QML Components declared in *.qml files and their attributes
  • JavaScript code in *.js and *.mjs files
  • dependencies of the module
  • versions of the module and its components
  • location of the *.qmltypes file with details on QML types defined in C++
  • any plugin that belongs to the module

它的语法很简单,都是纯文本,易于人类阅读。有人可能会认为,编写QML模块很简单。然而,这并不完全正确。

Some History

我们过去常常在Qt5中手动编写这样的qmldir文件。虽然语法很简单,但语义却不简单。有许多方法可以在qmldir文件中处理微妙的错误

  • Mis-spell the versions and have some components fail on imports of specific versions of the module.
  • Omit some *.qml files and the module works when using those files from the same directory, but not from elsewhere.
  • Mis-spell the name of the *.qmltypes file and throw Qt Creator’s QML tooling off (unless the file is called plugins.qmltypes which Qt Creator always tries to read).
  • Omit a dependency and discover that the module doesn’t work in certain builds because qmlimportscanner does not know to add the relevant library.

这些错误不会在典型的测试周期中显示出来。相反,它们只会在特定条件下出现。人们讨厌这些东西,这是理所当然的。

而且,在编写构建系统时,必须复制qmldir文件中提供的许多信息

  • *.qml|js|mjs files had to be added to the resource file system
  • The plugin needed to be compiled
  • The *.qmltypes needed to be generated using qmltyperegistrar (or, previously, qmlplugindump)
  • Ideally, qmlcachegen should be used to pre-compile any QML and JavaScript code

在qmake中编写了各种帮助函数来帮助完成所有这些工作。在Qt5的开发过程中,这一领域开发出了大量脚踏实地的武器,以至于许多人完全回避了它,只是没有编写任何QML模块。

然而,他们确实编写了QML模块。一个带有几个*的目录。没有qmldir文件的qml文件仍然是一个qml模块,由所谓的“隐式导入”定义。隐式导入只是使同一目录中的所有QML文件在其文件名下可用,而不分配任何版本、uri或类似的内容。这种简单的模块在许多情况下当然是实用的

  • They are immediately visible to any tooling such as Qt Creator
  • You can just run them using the qml tool, without any further setup
  • You can relocate them by just copying the directory elsewhere

然而,它们也有一些严重的缺点

  • You cannot add any C+±declared types to such modules
  • You cannot immediately pre-compile them using qmlcachegen
  • You cannot import them from elsewhere in a structured way
  • You cannot use singletons

对于这些缺点,人们找到了各种各样的解决方法。c++类型是通过手动调用应用程序的main()中的qmlRegisterType()来添加的。这是另一种没有qmldir文件的简并QML模块:qmlRegisterType()调用中指定的模块URI可以导入到使用相同应用程序加载的任何QML文件中。然而,当用其他任何东西加载相同的QML文件时,URI不可用。

* .qml和* .js文件被添加到* .qrc文件,然后用来使用qmlcachegen预先编译代码。然后,如果从资源文件系统加载相应的组件,您将获得预编译的快速加载版本。如果继续从物理文件系统加载相同的组件,则会默默地忽略预编译的字节代码,并且您只需遍历可执行文件就会浪费一些内存。

目录导入(导入“some/where/else/”作为共享)用于导入没有QMLDIR文件的模块。这在QML模块之间创建了额外的延迟绑定。您可以替换某些/何处/ else / elder的代码,直到您尝试在运行时在运行时执行模块的内容不支持,所有内容都会工作。如果不知道要导入的模块的名称,则QML引擎无法验证它实际导入正确的东西。每当项目具有单独的源,构建,安装和可能部署位置时,此类目录导入引用的目录都是一个有趣的练习。

在Qt5生命周期接近尾声时,这种情况显然不太理想。

在Qt 6.2中,QML模块的概念吸收先进的灵感。第一次为QML模块的CMake API提供了一个工具箱,用于创建表现良好的QML模块以及周围的各种规定,确保它们足够安全。

QML Modules in Qt 6.2

使用Qt 6.2时,您可以使用CMake API声明QML模块的QML模块,上面提到的所有复杂问题都是其他人的问题。特别是:

  • the qmldir and *.qmltypes files are automatically generated
  • C++ types annotated with QML_ELEMENT and friends are automatically registered
  • qmlcachegen is automatically invoked
  • The module is provided both in the physical and in the resource file system.
  • When loaded from the physical file system, the module redirects any access to QML and JavaScript files into the resource file system, so that the pre-compiled versions are used.
  • You can combine QML files and C+±based types seamlessly in the same module
  • A backing library and an optional plugin are created. You can link the backing library into your application to avoid loading the plugin at run time (but see below for caveats).

对于所有这些点,我们必须添加一个“*除非配置为其他”。Qt 6.2中的QML模块在默认情况下是正常的。仅这一点就使它们比您在Qt 5中使用的更容易使用。然而,如果你真的想的话,你仍然可以射脚。你拿枪的时候会稍微有点困难。

当然,这也带来了一个问题:为了充分利用QML模块,您必须改变编写QML和QML公开的c++代码的方式,以及您的构建系统。考虑到这些好处,它应该是值得的,至少对于新的应用程序来说。不过,有一些事情需要特别考虑。由于用于QML模块的CMake API的基本思想已经在本系列的前一篇博客文章中介绍过,所以我将直接进入更高级的用例。

Multiple QML modules in one binary

在Qt5中,您可以调用qmlRegisterType()并将任何字符串作为URI传递。通过这种方式,您可以在一个C++文件中定义多个(退化的)QML模块。人们开始欣赏这个功能,因为它允许以一种简单的方式构造QML应用程序。在Qt6中,不推荐使用这种特殊的技术。毕竟,您不应该手动调用qmlRegisterType()。而且,对于每个CMake目标,只能指定一个带有一个URI的QML模块。这是故意的,否则您必须指定哪个文件属于哪个模块。这样的映射会给你很多机会

以最直接的方式,我们可以创建这样一个应用程序

myProject
    | - CMakeLists.txt
    | - main.cpp
    | - main.qml
    | - onething.h
    | - onething.cpp
    | - ExtraModule
        | - CMakeLists.txt
        | - Extra.qml
        | - extrathing.h
        | - extrathing.cpp

在不详细介绍这个模块内容的情况下,让我们假设Main.qml包含extra.qml的实例化,如此:

import ExtraModule
Extra { ... }

但是,让我们先看看额外的模块。这必须是一个静态库,以便我们可以将其链接到主程序中。因此,我们在ExtraModule/Cmakelists.txt中陈述:

qt_add_library(extra_module STATIC)
qt_add_qml_module(extra_module
    URI "ExtraModule"
    VERSION 1.0
    QML_FILES
        Extra.qml
    SOURCES
        extrathing.cpp extrathing.h
)

这将生成两个目标:用于后台库的额外模块,以及用于插件的额外模块插件。这个插件也是一个静态库,但是不能在运行时加载。我们会回到这一点。

在myProject/CMakeLists.txt中,您需要指定main.qml和在onthing.h中声明的任何类型:

qt_add_executable(main_program main.cpp)

qt_add_qml_module(main_program
    VERSION 1.0
    URI myProject
    QML_FILES
        main.qml
    SOURCES
        onething.cpp onething.h
)

然后,为额外的模块添加子目录。

add_subdirectory(ExtraModule)

为了链接额外的模块,你可能会倾向于将额外的模块目标链接到主程序中。如果能成功就好了。然而,由于C和c++链接的工作方式,它不会让我们走得太远。链接器可以自由地消除二进制文件中未引用的符号。通常,对QML模块的所有引用都是用QML编写的,连接器无法看到QML。结果是额外的模块很可能被丢弃。为了解决这个问题,我们需要做两件事

  1. In the extra module, define a symbol we can refer to.
  2. Create a reference to the symbol from the extra module in the main program.

我们观察到Qt插件已经包含符号可以使用为这个目的:我们通常在静态构建的Qt中导入插件时使用的静态插件实例。还需注意,我们可以在qqmlextensionplugin.h中使用新的Q IMPORT QML PLUGIN宏来创建对这个符号的引用。我们添加到main.cpp:

#include <QtQml/qqmlextensionplugin.h>
Q_IMPORT_QML_PLUGIN(ExtraModulePlugin)

ExtraModulePlugin是生成的插件类的名称。它由包含附加Plugin的模块URI组成,在主程序的Cmakelists.txt中,我们将在main程序中链接plugin,而不是后端库

target_link_libraries(main_program PRIVATE extra_moduleplugin)

我们打算在6.3中简化此用例。

Exporting multiple major versions from the same module

在Qt5中,如果您想导出QML模块的多个主要版本,您可以根据需要简单地混合和匹配类型注册,或者您可以为您的QML模块创建版本编码目录。在Qt6中,由于QML模块有了新的CMake API,就不能这样做了。版本编码目录,如QtQuick/Controls/2/已被证明是一种相当复杂的事件,最好避免。此外,qt_add_qml_module在默认情况下只考虑其URI参数中给出的主要版本,即使个别类型在其版本标记中声明了其他版本。这是经过深思熟虑的。在多个版本下制作一个模块会增加开销。不应该让这个过程自动完成。此外,如果一个模块在多个版本下可用,那么我们还需要决定每个QML文件在哪个版本下可用。为了进一步显式地声明主要版本,您可以有选项地使用PAST_MAJOR_VERSIONS来qt_add_qml_module,这和单个QML文件中的QT_QML_SOURCE_VERSIONS属性一样。让我们来看一个例子:

set_source_files_properties(Thing.qml
    PROPERTIES
        QT_QML_SOURCE_VERSIONS "1.4;2.0;3.0"
)

set_source_files_properties(OtherThing.qml
    PROPERTIES
        QT_QML_SOURCE_VERSIONS "2.2;3.0"
)

qt_add_qml_module(my_module
    URI MyModule
    VERSION 3.2
    PAST_MAJOR_VERSIONS 
        1 2
    QML_FILES
        Thing.qml
        OtherThing.qml
        OneMoreThing.qml
    SOURCES
        everything.cpp everything.h
)

在这个场景中,MyModule有主要版本1、2和3。可用的最大版本是3.2。您可以导入任何版本1.x或2.x+x。 对于 Thing.qmlOtherThing.qml在QML中我们添加了明确的版本信息。Thing.qml从版本1.4可用,OtherThing.qml从版本2.2开始可用,请注意,我们必须在每个set_source_files_properties()中指定更高版本。这是因为您可能会在修改主版本时从模块中删除QML文件。OneMoreThing.qml中没有明确的版本信息。这意味着onemorething.qml可用于所有主要版本,次要版本0。

通过这种设置,生成的注册代码将在每个主要版本中包含一个qmlRegisterModule()。这样它们才可以被导入。

解释了这些之后,我鼓励你们退后一步。很明显,版本控制增加了很多复杂性和一些运行时开销。QML中的版本如此深入地集成到该语言中,是因为我们支持从QML文件的任何子对象中无条件查找根对象中的成员。这使得向组件添加的成员与源代码不兼容。新成员可能会影响其他对象的现有成员。qmllint工具允许您检查此类不合格的访问操作。如果您从QML代码中消除了所有非法访问,您将不再需要使用版本。为了支持这种无版本的方法,在Qt 6中导入语句不需要指定版本。你可以导入QtQuick,它会导入QtQuick模块的最新版本。如果将所有导入转换为省略版本,那么在QML语言中,模块的实际版本号就不再重要了。一个更合理的版本控制机制可以应用在不同的层次上,例如作为通用软件包管理器。

Custom directory layouts

构建QML模块的最简单方法是将它们保留在其URI命名的目录中。例如,模块My.Extra.Module将存在应用程序的 My/Extra/Module该相对目录下。这样可以在运行时和任何工具中轻松找到它们。最终,你可以很容易地过度到这种制定上来。如果多个应用程序使用相同的模块,该怎么办?如果要在一个地方组合所有QML模块,而不是让它们污染项目的根目录?

对于这些需求,你可以使用QT_QML_OUTPUT_DIRECTORY变量,以及RESOURCE PREFIX和IMPORT PATH选项来qt_add_qml_module()。

最常见的是,您希望将您的QML模块收集到特定的输出目录中,那么在您的构建目录中新增子目录qml/。这是用于QT_QML_OUTPUT_DIRECTORY的使用。为实现这一目标,只需在您的顶级Cmakelists.txt中简单设置以下内容:

set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml)

您可能注意到QML模块的输出目录移到了新的位置。类似地,qmllint和qmlcachegen调用也会自动调整,以使用新的输出目录作为导入路径。但是,新的输出目录不是默认QML导入路径的一部分。因此,您必须在运行时显式地添加它,以便能够找到您的QML模块。有多个选项可以在运行时添加导入路径。最常用的技术是用于特别导入路径的QML_IMPORT_PATH环境变量(用于调试或测试)和QQmlEngine::addImportPath()函数用于固定的导入路径,这些路径需要总是可访问的。

现在已经处理好了物理文件系统,您可能还想将QML模块移动到资源文件系统中的另一个位置。这就是RESOURCE PREFIX选项的作用。现在,您必须在每个qt_add_qml_module模块中分别指定它。然后,您的QML模块将被放置在指定的前缀下,并附加从URI生成的目标路径。例如,考虑以下模块:

qt_add_qml_module(
    URI My.Great.Module
    VERSION 1.0
    RESOURCE_PREFIX /example.com/qml
    QML_FILES
        A.qml
        B.qml
)

这将为资源文件系统添加一个目录:/example.com/qml/My/Great/Module并放置在其中定义的QML模块中。您不必严格需要将资源前缀添加到QML导入路径,因为仍然可以在物理文件系统中找到模块。通常,将资源前缀添加到QML导入路径是一个好主意,因为对于大多数模块来说,从资源文件系统加载比从物理文件系统加载要快。

除此之外,您所依赖的模块可能完全位于当前项目之外的一个单独的导入路径中。即使您在运行时添加了相应的导入路径,qmllint和其他工具也可能会在您的QML代码上出错,因为它们在编译时找不到这样的依赖项。这就是您可以使用IMPORT PATH选项的目的。例如:

qt_add_qml_module(
    URI My.Dependent.Module
    VERSION 1.0
    QML_FILES
        C.qml
    IMPORT_PATH "/some/where/else"
)

Eliminating run time file system access

特别是当您将所有的QML模块链接到相同的二进制文件中时(如上所述),您可能不希望应用程序在文件系统中查询QML模块。如果总是从资源文件系统加载所有的QML模块,则可以将应用程序部署为单个二进制文件。让我们首先考虑一个简单的情况:

QQmlEngine qmlEngine;
qmlEngine.addImportPath(QStringLiteral(":/"));
// Use qmlEngine to load your main.qml file.

如果您已经将所有模块链接到应用程序中,并且遵循默认的资源目录布局,那么这就是您所要做的。不要再添加任何导入路径,因为这些路径可能会覆盖刚才添加的路径。

如果指定了自定义资源前缀,则必须将自定义资源前缀添加到导入路径。您还可以使用多个资源前缀,并将它们全部添加。但是,由于搜索QML模块的多个导入路径的开销,您应该在这里限制自己。

路径:/qt-project.org/imports/是默认QML导入路径的一部分。如果您使用它,您不必特别添加它。但是,QT的QML模块放在那里。你必须小心不要覆盖它们。这就是为什么它不是用户项目的默认资源前缀。对于跨不同项目重新使用的模块:建议使用:/qt-project.org/imports/。通过使用它,您可以避免强制所有用户添加自定义导入路径。

Installation

如果您没有将所有模块链接到一起,那么QML引擎需要访问物理文件系统以加载它们。如果您希望保持应用程序的模块化并在运行时根据情况加载不同的QML模块,那么这是可取的。最终,您将不得不负责QML模块的安装和部署。QML模块的CMake API还没有为此提供抽象。我们打算在Qt 6.3中提供一个。在此之前,您可以手动使用CMake的install函数来安装QML模块。

The NO options

在qt中有很多选项去以大写NO开头来执行qt_add_qml_module。它们以大写NO为开头,如果你不想使用它们,将会在这里停止继续读取。

最突出的NO选项可能是NO_GENERATE_PLUGIN_SOURCE。不幸的是,仍然需要为每个QML引擎配置 image providers。因此,如果您在QML模块中绑定了一个图像提供程序,则需要实现QQmlEngineExtensionPlugin::initializeEngine()方法。这反过来又使你有必要编写自己的插件。我们正在努力为 image providers找到一个不同的位置。它们不应该与引擎绑定在一起,因为它们本质上是一个图像事务,应该完全存在于QtQuick中。然而,就目前而言,我们必须利用现有的资源。让我们考虑一个提供自己插件源代码的模块:

qt_add_qml_module(imageproviderplugin
    VERSION 1.0
    URI "ImageProvider"
    PLUGIN_TARGET imageproviderplugin
    NO_PLUGIN_OPTIONAL
    NO_GENERATE_PLUGIN_SOURCE
    CLASS_NAME ImageProviderExtensionPlugin
    QML_FILES
        AAA.qml
        BBB.qml
    SOURCES
        moretypes.cpp moretypes.h
        myimageprovider.cpp myimageprovider.h
        plugin.cpp
)

你可以像这样在myimageprovider.h中声明一个 image provider程序,比如以下:

class MyImageProvider : public QQuickImageProvider
{
    [...]
};

在plugin.cpp中,你可以定义QQmlEngineExtensionPlugin:

#include <myimageprovider.h>
#include <QtQml/qqmlextensionplugin.h>

class ImageProviderExtensionPlugin : public QQmlEngineExtensionPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid)
public:
    void initializeEngine(QQmlEngine *engine, const char *uri) final
    {
        Q_UNUSED(uri);
        engine->addImageProvider("myimg", new MyImageProvider);
    }
};

这将使image provider可用。需要注意的是,我们在这里删除了插件和后台库之间的分离。两者都是相同的CMake目标imageproviderplugin。这样做是为了让连接器在各种情况下不会删除模块的某些部分。它可能会使从其他地方访问moretypes.h中声明的类型变得更加困难。

你可能已经注意到,在上面的CMakeLists .txt中,你自由获取一个NO选项:NO_PLUGIN_OPTIONAL。通常,如果QML引擎发现它已经知道给定URI的类型注册函数,它就不会忙于加载相同URI的插件。它只会在模块被导入时注册类型。它知道类型注册函数的方法是将后台库(或者,在本例中是插件)链接到你的应用程序中,并确保链接器不会忽略你。由于插件加载需要消耗资源,一般情况下,跳过不必要的加载是个不错的主意。然而,在imageproviderplugin的例子中,插件做的不仅仅是使moretypes.h中的类型可用。它还初始化image provider。因此,即使已知类型的注册函数是已知的,QML引擎仍然必须加载插件并调用初始化方法。NO_PLUGIN_OPTIONAL选项确切地表明:它将插件声明为强制性。

所有其他的NO选项实际上都是一个大的NO,只应该用作临时移植辅助。它们已经被引入到我们在Qt本身发现的各种情况中,在这些情况下,Qt 6.2无法及时完成适当的修复。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值