Qt6 Porting Guide - CMake

随着 Qt6.2 进入 rc 阶段,标志着 Qt6 各模块已经趋近完整,可以看到身边越来越多的项目开始了从 Qt5 迁移到 Qt6 繁重工作,作为从 Qt6 alpha 还没发就尝试迁移的资深人员。 鄙人自认为在 Qt6 / CMake 方面算是比较了解。

直入主题

不同于 6.0,Qt 6.2 包含了更多的 CMake API,本文就要说说个人认为最晦涩难懂的 qt_add_qml_module

qt_add_qml_module 非常复杂,单参数就有 26 个,首先来看一下这个巨大函数的 signature

qt_add_qml_module(
    # 必需参数
    target
    URI uri
    VERSION version

    # 可选参数
    [PAST_MAJOR_VERSIONS ...] [STATIC | SHARED] [PLUGIN_TARGET plugin_target]
    [OUTPUT_DIRECTORY output_dir] [RESOURCE_PREFIX resource_prefix] [CLASS_NAME class_name]
    [TYPEINFO typeinfo] [IMPORTS ...] [OPTIONAL_IMPORTS ...] [DEPENDENCIES ...] [IMPORT_PATH ...]
    [SOURCES ...] [QML_FILES ...] [OUTPUT_TARGETS out_targets_var] [DESIGNER_SUPPORTED]

    # No 系列可选参数
    [NO_PLUGIN_OPTIONAL] [NO_CREATE_PLUGIN_TARGET] [NO_GENERATE_PLUGIN_SOURCE] [NO_GENERATE_QMLTYPES]
    [NO_GENERATE_QMLDIR] [NO_LINT] [NO_CACHEGEN] [NO_RESOURCE_TARGET_PATH]
)

必需参数

在这 26 个奇奇怪怪的参数中,有 3 个必需参数(targeturiversion

Target

对于这个 Target 参数,要提供的是一个符合 CMake 命名规范的 target name,这个 target 既可以是一个已经存在的 library(别跟我说 INTERFACE library),也可以是一个现存 executable target,甚至也可以不存在(如果不存在的话,此函数会以此名称创建一个 library)

有关这个 Target 的作用与他和我们 QML module 的关系,将在下文慢慢讲解

URI

一个很通俗易懂的参数(吧?),就是这个 QML 模块的 uri (可以为 qml.myapp.me 这样的域名形式,也可以是 MyApp 一类)

所以你可以用这个 URI 来导入此 QML module 到任何其他程序(还记得 import QtQuick.Controls 吧,你现在可以 import qml.myapp.me 了)

VERSION

大概用不着多说,此 QML module 的版本号,遵循 MAJOR.MINOR 格式(但其实你可以偷偷写 MAJOR.MINOR.PATCH,只不过是没用罢了)


可选参数

再来说说不带 NO_ 开头的那些可选参数:

PAST_MAJOR_VERSIONS

在 Qt5 时代,一个 QML module 里是可以包含很多小版本(大版本)的,这也就是 import Something 1.15 中最后版本号的由来,而这在 Qt6 中变了,一个不带版本号的 import 语句默认导入的是最新版本的 module。

如果我的 module 里有多个版本呢?

那就通过 PAST_MAJOR_VERSIONS 1 2 3 4 5 来告诉函数你的 module 包含五个之前的版本,另外,你需要给对应版本的 qml 文件添加 propertiesQT_QML_SOURCE_VERSIONS)来标记此文件输入哪个版本的 module

我觉得用到这个参数的人不多吧

[STATIC | SHARED]

这个选项指定了此 QML module 将会是一个静态库还是一个动态库,与 CMake 中 add_library 时使用的 STATICSHARED 是同一效果

要注意的是,这一参数只适用于给定 Target 不存在(即当此函数负责创建 target)的时候,如果必需参数 Target 已经存在,则不能指定这个参数

PLUGIN_TARGET

建议放松心情慢慢看

在 Qt QML 系统中,一个 ‘模块’ 是以 QQml*PluginQQmlExtensionPluginQQmlEngineExtensionPlugin) 插件形式存在的,此参数中要填写的是这个 Plugin 的 target 名称,这时产生了几种情况:

  1. 必需参数 Target 为非 executable 或不存在,且 PLUGIN_TARGETTarget 相同
  2. 必需参数 Target 为非 executable 或不存在,且 PLUGIN_TARGETTarget 不相同或未指定
  3. 必需参数 Target 为 executable

情况 1,PLUGIN_TARGET != Target

在这种情况下,本函数将会创建一个名称为 ${PLUGIN_TARGET} 的 library,动态或静态跟随上一个参数,并生成 C++ 代码作为此 library 的实现代码,这部分代码实现了一个 QQmlEngineExtensionPlugin${Target}plugin_${URI}Plugin.cpp),并自动在 runtime 进行类型注册:

// This file is autogenerated by CMake. Do not edit.
...
extern void qml_register_types_YOUR_QML_MODULE();
class YOUR_QML_MODULEPlugin : public QQmlEngineExtensionPlugin
{
    .....
    YOUR_QML_MODULEPlugin(QObject *parent = nullptr) : QQmlEngineExtensionPlugin(parent)
    {
        volatile auto registration = &qml_register_types_YOUR_QML_MODULE;
        Q_UNUSED(registration);
    }
};

可以看到,类构造器包含一个 volatile auto registration = &qml_register_types_YOUR_QML_MODULE; 变量,并使用了 volatile 避免被编译器优化掉,而 qml_register_types_YOUR_QML_MODULE 被声明为 extern,经过仔细寻找可以发现此函数在 ${Target}_qmltyperegistration.cpp 定义

Q_QMLTYPE_EXPORT void qml_register_types_URI()
{
    //...
    qmlRegisterModule("URI", 1, 0);
}

static const QQmlModuleRegistration registration("URI", qml_register_types_URI);

因此,在这个 QQmlEngineExtensionPlugin 被加载时,会同时调用 QQmlModuleRegistration 构造器,在此构造器中进行 QML 类型注册(见 Woboq Code Browser),

Licensed under CC BY-NC-SA 4.0
Moooooooooooooooooooody
Built with Hugo
Theme Stack designed by Jimmy