编写QML模块

您应该使用CMake QML模块API声明QML模块,以

  • 生成qmldir*.qmltypes文件
  • 注册使用QML_ELEMENT注解的C++类型。
  • 在同一模块中将QML文件和基于C++的类型结合在一起。
  • 对所有QML文件调用qmlcachegen
  • 在模块内部使用预编译的QML文件。
  • 以物理和资源文件系统的方式提供模块。
  • 创建一个后端库和一个可选的插件。将后端库链接到应用程序中,以避免在运行时加载插件。

上述所有操作都可以分别配置。有关更多信息,请参阅CMake QML模块API

一个二进制文件中的多个QML模块

您可以将多个QML模块添加到同一二进制文件中。为每个模块定义一个CMake目标,然后将目标链接到可执行文件。如果额外的目标都是静态库,则结果将为包含多个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中说明这一点

# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

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
    RESOURCE_PREFIX /
)

这生成了两个目标:用于后端库的extra_module,以及用于插件的extra_moduleplugin。由于它也是一个静态库,因此插件不能在运行时加载。

在myProject/CMakeLists.txt中,您需要指定包含main.qml和在任何类型中声明的onething.h的QML模块

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)

为了保证正确链接额外模块,您需要

  • 在额外模块中定义一个符号。
  • 从主程序中创建对符号的引用。

QML插件包含用于此目的的符号。您可以使用Q_IMPORT_QML_PLUGIN宏创建对符号的引用。将以下代码添加到main.cpp中

#include <QtQml/QQmlExtensionPlugin>
Q_IMPORT_QML_PLUGIN(ExtraModulePlugin)

ExtraModulePlugin是生成的插件类的名称。它由模块URI附加Plugin组成。然后,在主程序的CMakeLists.txt中,将插件链接到主程序,而不是后端库

target_link_libraries(main_program PRIVATE extra_moduleplugin)

版本

QML有一个复杂的系统来为组件和模块分配版本。在大多数情况下,您应该通过忽略所有这些来

  1. 永不要在你的导入语句中添加版本
  2. 永不要在qt_add_qml_module中指定任何版本
  3. 永不要使用QML_ADDED_IN_VERSIONQT_QML_SOURCE_VERSIONS
  4. 永不要使用Q_REVISIONQ_PROPERTY中的REVISION()属性
  5. 避免未指定权限的访问
  6. 慷慨使用导入命名空间

版本管理最好是在语言本身外进行处理。例如,你可以为不同的QML模块保留不同的导入路径。或者你可以使用操作系统提供的版本管理机制来安装或卸载带有QML模块的包。

在某些情况下,Qt自己提供的QML模块可能会根据导入的版本而表现出不同的行为。特别是,如果某个属性被添加到QML组件中,并且你的代码中有对同名的另一个属性的未指定权限访问,你的代码将会出错。在下面的示例中,代码的行为将根据Qt的版本不同而不同,因为topLeftRadius属性是在Qt 6.7中添加的。

import QtQuick

Item {
    // property you want to use
    property real topLeftRadius: 24

    Rectangle {

        // correct for Qt version < 6.7 but uses Rectangle's topLeftRadius in 6.7
        objectName: "top left radius:" + topLeftRadius
    }
}

避免未指定权限访问的解决方案是避免这种访问。可以使用qmllint查找此类问题。以下示例使用安全、指定权限的方式访问实际要访问的属性。

import QtQuick

Item {
    id: root

    // property you want to use
    property real topLeftRadius: 24

    Rectangle {

        // never mixes up topLeftRadius with unrelated Rectangle's topLeftRadius
        objectName: "top left radius:" + root.topLeftRadius
    }
}

你还可以通过导入特定的QtQuick版本来避免这种不兼容。

// make sure Rectangle has no topLeftRadius property
import QtQuick 6.6

Item {
    property real topLeftRadius: 24
    Rectangle {
        objectName: "top left radius:" + topLeftRadius
    }
}

版本管理解决的另一个问题是,由不同模块导入的QML组件可能会相互影响。在以下示例中,如果MyModule在较新版本中引入了一个名为Rectangle的组件,那么这个文档创建的Rectangle将不再是QQuickRectangle,而是MyModule引入的新Rectangle

import QtQuick
import MyModule

Rectangle {
    // MyModule's Rectangle, not QtQuick's
}

避免这种影响的好方法是将QtQuick和/或MyModule导入到类型命名空间中,如下所示

import QtQuick as QQ
import MyModule as MM

QQ.Rectangle {
   // QtQuick's Rectangle
}

或者,如果你以固定版本导入MyModule,并且新组件通过QML_ADDED_IN_VERSIONQT_QML_SOURCE_VERSIONS接收了正确的版本标签,那么这种影响也可以避免

import QtQuick 6.6

// Types introduced after 1.0 are not available, like Rectangle for example
import MyModule 1.0

Rectangle {
    // QtQuick's Rectangle
}

要让这生效,你需要使用MyModule中的版本。有一些需要注意的事项。

如果你添加了版本,请确保在各个地方都添加

你需要向qt_add_qml_module添加一个VERSION属性。该版本应该是你的模块提供的最新版本。旧版本的次版本会自动注册。对于旧的主版本,请参见下文。

你应该向每一个在你的模块的x.0版本中未引入的类型添加QML_ADDED_IN_VERSIONQT_QML_SOURCE_VERSIONS,其中x是当前的主版本号。

如果你忘记添加版本标签,组件将在所有版本中可用,这使得版本管理变得无效。

然而,无法为在QML中定义的属性、方法和信号添加版本。唯一添加QML文档版本的方法是为每个更改添加一个新的文档,并为每个更改保留一个带有单独QT_QML_SOURCE_VERSIONS的文档。

版本不是传递的

如果您的模块A中有一个组件导入了另一个模块B并将该模块中的一个类型实例化为根元素,那么导入的B版本将与结果组件中可用的属性相关,无论用户导入的A版本是什么。

考虑模块A中的一个文件TypeFromA.qml,其版本为2.6

import B 2.7

// Exposes TypeFromB 2.7, no matter what version of A is imported
TypeFromB { }

现在考虑一个 用户TypeFromA

import A 2.6

// This is TypeFromB 2.7.
TypeFromA { }

用户希望看到版本2.6,但实际上得到的是基类TypeFromB的版本2.7

因此,为了安全起见,您不仅必须复制自己的QML文件,在添加属性时给它们赋予新的版本号,还必须在升级您导入的模块版本时如此。

合格的访问不尊重版本控制

版本控制仅影响对类型成员或类型的非限定访问。在包含topLeftRadius的示例中,如果您编写了this.topLeftRadius,即使您使用Qt 6.7,只要您import QtQuick 6.6,该属性也会被解析。

版本和修订

通过 QML_ADDED_IN_VERSIONQ_REVISIONQ_PROPERTY 的两个参数版本 REVISION(),您只能声明与元对象在 QMetaMethod::revisionQMetaProperty::revision 中公开的修订紧密耦合的版本。这意味着您的类型层次结构中的所有类型都必须遵循相同的版本控制方案。这包括您从Qt本身继承的任何类型。

通过 qmlRegisterType 和相关函数,您可以注册元对象修订和类型版本之间的任何映射。然后您需要使用 Q_REVISION 的一参数形式和 Q_PROPERTYREVISION 属性。然而,这可能变得相当复杂且令人困惑,因此不推荐这样做。

从同一模块导出多个主要版本

qt_add_qml_module 默认会考虑其 VERSION 参数中给出的主版本,即使单个类型在其通过 QT_QML_SOURCE_VERSIONSQ_REVISION 添加的具体版本中声明了其他版本也是如此。如果一个模块在多个版本下都可用,您还需要决定单个 QML 文件在什么版本下可用。为了声明更多主版本,您可以使用 qt_add_qml_modulePAST_MAJOR_VERSIONS 选项以及单个 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.qml 和 OtherThing.qml,我们添加了显式版本信息。Thing.qml 可用自版本 1.4,而 OtherThing.qml 可用自版本 2.2。您还必须在每个 set_source_files_properties() 中指定较晚的版本,因为在升级主版本时可能会从模块中删除 QML 文件。对于 OneMoreThing.qml 没有显式版本信息。这意味着 OneMoreThing.qml 在所有主版本中可用,从小版本 0 开始。

在这种情况下,生成的注册代码将为每个主版本使用 qmlRegisterModule() 注册模块 versions。这样,所有版本都可以导入。

自定义目录布局

组织 QML 模块的最简单方法是将它们保存在以它们的 URI 命名的目录中。例如,一个名为 My.Extra.Module 的模块将位于使用它的应用程序相关的目录 My/Extra/Module 中。这样,它们可以在运行时和任何工具中轻松找到。

在更复杂的项目中,这种约定可能过于限制。例如,您可能希望将所有 QML 模块放置在一个地方,以避免污染项目的根目录。或者您想在多个应用程序中重复使用单个模块。在这种情况下,可以使用 QT_QML_OUTPUT_DIRECTORYRESOURCE_PREFIXIMPORT_PATH

要将 QML 模块收集到特定的输出目录中,例如在构建目录中的子目录 "qml" 内,请在顶级 CMakeLists.txt 中设置以下内容

set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml)

QML 模块的输出目录移动到新位置。同样,qmllintqmlcachegen 调用也会自动适应以使用新的输出目录作为 import path。因为新的输出目录不在默认的 QML 导入路径中,您必须在运行时显式添加它,以便找到 QML 模块。

现在,已经处理了物理文件系统,您可能仍然想将 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 导入路径是一个好主意,因为与从物理文件系统加载相比,从资源文件系统加载大多数模块速度更快。

如果 QML 模块打算在一个包含多个导入路径的大型项目中使用,您需要做额外的一个步骤:即使您在运行时添加了导入路径,像 qmllint 这样的工具也无权访问它,可能会失败地找到正确的依赖项。使用 IMPORT_PATH 来告诉工具它必须考虑的附加路径。例如

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

消除运行时文件系统访问

如果所有 QML 模块始终从资源文件系统中加载,则可以将应用程序作为一个单独的二进制文件部署。

如果 QTP0001 政策设置为 NEW,则 qt_add_qml_module()RESOURCE_PREFIX 参数默认为 /qt/qml/,因此您的模块将放置在资源文件系统的 :/qt/qml/。这是默认的 QML 导入路径 的一部分,但 Qt 本身不会使用它。对于要在您的应用程序中使用模块,这是正确的位置。

如果您指定了自定义的 RESOURCE_PREFIX,则必须将自定义资源前缀添加到 QML 导入路径。您还可以添加多个资源前缀

QQmlEngine qmlEngine;
qmlEngine.addImportPath(QStringLiteral(":/my/resource/prefix"));
qmlEngine.addImportPath(QStringLiteral(":/other/resource/prefix"));
// Use qmlEngine to load the main.qml file.

在使用第三方库以避免模块名称冲突时可能需要这样做。在其他所有情况下,都应避免使用自定义资源前缀。

路径 :/qt-project.org/imports/ 也属于默认的 QML导入路径。对于在不同项目或Qt版本中大量重复使用的模块,:/qt-project.org/imports/ 作为资源前缀是可以接受的。Qt自己的QML模块都放在那里,但需要注意不要覆盖它们。

不要添加任何不必要的导入路径。这样可能会导致QML引擎在错误的位置找到您的模块。这可能会引发只能在特定环境中重现的问题。

集成自定义QML插件

如果您在QML模块中打包了图片提供器,则需要实现QQmlEngineExtensionPlugin::initializeEngine() 方法。这反过来又使得编写自己的插件成为必要。为了支持这种情况,可以使用NO_GENERATE_PLUGIN_SOURCE

让我们考虑一个提供自己的插件源码的模块

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中声明一个图片提供器,如下所示

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

这将使得图片提供器可用。插件和后端库都在同一个CMake目标imageproviderplugin中。这样做是为了确保在各种情况下链接器不会丢弃模块的部分。

由于插件创建了一个图片提供器,它不再有简单的initializeEngine函数。因此,插件不再是可选的。

© 2024 Qt公司有限公司。此处包含的文档贡献是各自所有者的版权。此处提供的文档是根据自由软件基金会发布的GNU自由文档许可证版本1.3的条款许可的。Qt及其相关标志是芬兰以及/或全球其他国家的Qt公司商标。所有其他商标均为各自所有者的财产。