编写 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 和 组成。然后,在主程序的 CMakeLists.txt 中,将插件而不是后备库链接到主程序中:Plugin

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_REVISION 或 QT_QML_SOURCE_VERSIONS 中的REVISION() 属性Q_PROPERTY
  5. 避免非限定访问
  6. 大方地使用导入命名空间

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

在某些情况下,Qt 自身的 QML 模块可能会显示不同的行为,这取决于导入的版本。特别是,如果在 QML 组件中添加了一个属性,而你的代码包含对另一个同名属性的非限定访问,你的代码就会中断。在下面的示例中,由于topLeftRadius 属性是在 Qt 6.7 中添加的,因此代码会因 Qt 版本的不同而表现不同:

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 版本中没有引入QT_QML_SOURCE_VERSIONS类型,则应为每个类型添加QML_ADDED_IN_VERSIONQT_QML_SOURCE_VERSIONS,其中x 是当前的主要版本。

如果您忘记添加版本标签,组件将在所有版本中都可用,从而使版本控制失效。

然而,没有办法为 QML 中定义的属性、方法和信号添加版本。对 QML 文档进行版本控制的唯一方法是为每次更改添加一个新文档,并为其添加单独的QT_QML_SOURCE_VERSIONS

版本不具有传递性

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

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

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_VERSION 以及Q_REVISIONQ_PROPERTY 的双参数变体REVISION() ,您只能声明与metaobject's 修订版紧密耦合的版本,如QMetaMethod::revisionQMetaProperty::revision 所示。这意味着您的类型层次结构中的所有类型都必须遵循相同的版本控制方案。这包括由 Qt 本身提供并由您继承的任何类型。

通过qmlRegisterType 和相关函数,您可以注册元对象修订和类型版本之间的任何映射。然后,您需要使用Q_REVISION 的单参数形式和Q_PROPERTYREVISION 属性。不过,这可能会变得相当复杂和混乱,因此不建议使用。

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

qt_add_qml_module默认会考虑其 VERSION 参数中给出的主要版本,即使个别类型通过QT_QML_SOURCE_VERSIONSQ_REVISION 在其添加的特定版本中声明了其他版本。如果一个模块有多个版本,您还需要决定个别 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。对于 Thing.qml 和 OtherThing.qml,我们添加了明确的版本信息。Thing.qml 从 1.4 版开始可用,OtherThing.qml 从 2.2 版开始可用。您还必须在每个 中指定以后的版本,因为在提升主要版本时,您可能会删除模块中的 QML 文件。OneMoreThing.qml 没有明确的版本信息。这意味着 OneMoreThing.qml 从次要版本 0 开始在所有主要版本中都可用。set_source_files_properties()

通过这种设置,生成的注册代码将使用qmlRegisterModule() 为每个主要版本注册模块versions 。这样,所有版本都可以导入。

自定义目录布局

构造 QML 模块的最简单方法是将它们保存在以 URI 命名的目录中。例如,My.Extra.Module 模块将存放在 My/Extra/Module 目录中,与使用该模块的应用程序相对应。这样,运行时和任何工具都能轻松找到它们。

在更复杂的项目中,这种约定可能会限制太多。例如,你可能想把所有 QML 模块集中在一处,以避免污染项目的根目录。或者您想在多个应用程序中重复使用一个模块。在这种情况下,QT_QML_OUTPUT_DIRECTORYRESOURCE_PREFIXIMPORT_PATH结合使用。

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

set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml)

QML 模块的输出目录会移动到新位置。同样,qmllintqmlcachegen 的调用也会自动调整为使用新的输出目录作为导入路径。因为新的输出目录不是默认 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 模块中捆绑image provider ,则需要实现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 功能。因此,该插件不再是可选的。

另请参阅 Qt QML 的变化现代 QML 模块将 QML 模块移植到 CMake

© 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.