本页内容

编写 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_VERSION 或QT_QML_SOURCE_VERSIONS
  4. 切勿在以下情况下使用Q_REVISION 或REVISION() 属性Q_PROPERTY
  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_VERSION 或QT_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 版本(其中x 为当前主版本)中引入的每个类型,都应添加QML_ADDED_IN_VERSION 或QT_QML_SOURCE_VERSIONS属性。

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

但是,无法为在 QML 中定义的属性、方法和信号添加版本。对 QML 文档进行版本控制的唯一方法是添加一个新文档,并针对每项更改分别指定QT_QML_SOURCE_VERSIONS。

版本不具有传递性

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

考虑一个文件TypeFromA.qml ,其中模块A 包含版本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 ,即使你import QtQuick 6.6 ,只要使用 Qt 6.7,该属性仍会被解析。

版本与修订版

使用 `QML_ADDED_IN_VERSION`,以及 `Q_REVISION ` 的双参数变体和 `Q_PROPERTY` 的 `REVISION()` 方法时,您只能声明与 `metaobject's ` 修订版紧密耦合的版本,这些修订版在 `QMetaMethod::revision ` 和 `QMetaProperty::revision` 中已公开。这意味着您类型层次结构中的所有类型都必须遵循相同的版本方案。这包括您继承的任何由 Qt 自身提供的类型。

通过qmlRegisterType 及其相关函数,您可以注册元对象修订版与类型版本之间的任意映射。随后,您需要使用Q_REVISION 的单参数形式,以及Q_PROPERTY 中的REVISION 属性。然而,这种做法可能会变得相当复杂且令人困惑,因此不建议使用。

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

qt_add_qml_module默认会采用其 VERSION 参数中指定的主版本,即使各个类型通过QT_QML_SOURCE_VERSIONS或Q_REVISION 在其添加的特定版本中声明了其他版本。 如果一个模块在多个版本下都可用,您还需要决定各个 QML 文件在哪些版本下可用。要声明更多主版本,您可以使用qt_add_qml_module 的PAST_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_DIRECTORY 结合RESOURCE_PREFIX 和IMPORT_PATH。

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

set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml)

QML 模块的输出目录将移动到新位置。同样地,qmllint 和qmlcachegen 的调用也会自动调整,以使用新的输出目录作为导入路径。由于新的输出目录不属于默认的 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》。

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