QML类型编译器

QML类型编译器(qmltc),是Qt自带的工具,可以将QML类型转换为C++类型,这些类型将以即时编译的形式作为用户代码的一部分。使用qmltc可以因编译器有更多的优化机会而导致更好的运行时性能,相对于基于QQmlComponent的对象创建。qmltc是Qt Quick编译器工具链的一部分。

按照设计,qmltc会输出面向用户的面板代码。该代码预期会直接由C++应用程序使用,否则您不会看到任何好处。这个生成的代码本质上替换了QQmlComponent及其API,用于从QML文档中创建对象。您可以在在QML应用程序中使用qmltc生成输出基础下找到更多信息。

为了启用qmltc

  • 为您的应用程序创建一个合适的QML模块
  • 调用qmltc,例如,通过CMake API
  • 在应用程序源代码中包含生成的头文件。
  • 实例化生成的类型的对象。

在这个工作流程中,qmltc通常在构建过程中运行。因此,当qmltc拒绝一个QML文档(无论是因为错误或警告,还是因为qmltc尚不支持的结构),构建过程将失败。这类似于您在创建QML模块时启用自动生成linting目标并尝试“构建”以运行qmllint时接收qmllint错误。

警告:qmltc目前正在技术预览阶段,可能无法编译任何任意的QML程序(有关更多详细信息,请参阅已知限制)。当qmltc失败时,不会生成任何内容,因为您的应用程序无法合理地使用qmltc的输出。如果您的程序包含错误(或无法解决的警告),则应修复它们以启用编译。一般规则是要遵循最佳实践并遵循qmllint的建议。

注意:qmltc不保证生成的C++代码在过去的或未来的版本(即使是补丁版本)之间保持API、源代码或二进制兼容性。此外,使用Qt的QML模块编译的应用程序将需要链接到私有Qt API。这是因为Qt的QML模块通常不提供公共C++ API,因为它们的主要用途是通过QML。

在QML应用程序中使用qmltc

从构建系统的角度来看,添加qmltc编译与添加qml缓存生成没有太大区别。简单来说,构建过程可以描述为

尽管实际编译过程要复杂得多,但此图描绘了qmltc使用的核心组件:QML文件本身以及包含qmltypes信息的qmldir。通常,简单应用中的qmldir较为原始,但从总体上来说,qmldir可能非常复杂。它提供了对qmltc执行正确的QML到C++翻译所必需的基本、精心打包的类型信息。

但是,在qmltc的情况下,仅仅增加额外的构建步骤是不够的。应用程序代码也必须修改为使用qmltc生成的类,而不是QQmlComponent或其更高级别的替代方案。

使用qmltc编译QML代码

从Qt 6开始,Qt使用CMake来构建其各种组件。用户项目可以将——并被鼓励使用CMake来使用Qt构建其组件。将开箱即用的qmltc编译支持添加到您的项目中也需要一个CMake驱动的构建流程,因为这种流程围绕适当的QML模块及其基础设施进行构建。

添加qmltc编译的简单方法是在创建应用时作为QML模块的一部分使用专属的CMake API。考虑一个简单的应用目录结构

.
├── CMakeLists.txt
├── myspecialtype.h     // C++ type exposed to QML
├── myspecialtype.cpp
├── myApp.qml           // main QML page
├── MyButton.qml        // custom UI button
├── MySlider.qml        // custom UI slider
└── main.cpp            // main C++ application file

然后CMake代码通常会类似以下这样

# Use "my_qmltc_example" as an application name:
set(application_name my_qmltc_example)

# Create a CMake target, add C++ source files, link libraries, etc...

# Specify a list of QML files to be compiled:
set(application_qml_files
    myApp.qml
    MyButton.qml
    MySlider.qml
)

# Make the application into a proper QML module:
qt6_add_qml_module(${application_name}
    URI QmltcExample
    QML_FILES ${application_qml_files}

    # Compile qml files (listed in QML_FILES) to C++ using qmltc and add these
    # files to the application binary:
    ENABLE_TYPE_COMPILER
)

# (qmltc-specific) Link *private* libraries that correspond to QML modules:
target_link_libraries(${application_name} PRIVATE Qt::QmlPrivate Qt::QuickPrivate)

使用生成的C++

与QQmlComponent实例化不同,qmltc的输出是C++代码,因此它可以直接由应用程序使用。通常,在C++中构建一个新的对象等同于通过QQmlComponent::create()创建一个新对象。一旦创建,对象就可以从C++进行操作,例如,与QQuickWindow结合以在屏幕上绘制。给定一个myApp.qml文件,应用代码(在两种情况下)通常如下所示

#include <QtQml/qqmlcomponent.h>

QGuiApplication app(argc, argv);
app.setApplicationDisplayName(QStringLiteral("This example is powered by QQmlComponent :("));

QQmlEngine e;
QQuickWindow window;

QQmlComponent component(&e);
component.loadUrl(
            QUrl(QStringLiteral("qrc:/qt/qml/QmltcExample/myApp.qml")));

QScopedPointer<QObject> documentRoot(component.create());
QQuickItem *documentRootItem = qobject_cast<QQuickItem *>(documentRoot.get());

documentRootItem->setParentItem(window.contentItem());
window.setHeight(documentRootItem->height());
window.setWidth(documentRootItem->width());
// ...

window.show();
app.exec();
#include "myapp.h" // include generated C++ header

QGuiApplication app(argc, argv);
app.setApplicationDisplayName(QStringLiteral("This example is powered by qmltc!"));

QQmlEngine e;
QQuickWindow window;

QScopedPointer<QmltcExample::myApp> documentRoot(new QmltcExample::myApp(&e));

documentRoot->setParentItem(window.contentItem());
window.setHeight(documentRoot->height());
window.setWidth(documentRoot->width());
// ...

window.show();
app.exec();

QML引擎

生成的代码使用QQmlEngine与QML文档的动态部分交互,主要是JavaScript代码。为了使它工作,无需特殊安排。任何传递给qmltc生成的类对象的QQmlEngine构造函数的QQmlEngine实例都应该像QQmlComponent(engine)一样正常工作。这也意味着您可以使用影响QML行为的QQmlEngine方法。但是,也存在一些注意事项。与基于QQmlComponent的对象创建不同,qmltc自己在编译代码到C++时并不依赖于QQmlEngine。例如,QQmlEngine::addImportPath("/foo/bar/")——通常会导致扫描的额外导入路径——将被提前时间qmltc程序完全忽略。

注意:要将导入路径添加到qmltc编译,请考虑使用相关CMake命令的相关参数。

通常,您可以这样理解:QQmlEngine涉及到应用过程运行,而qmltc不,因为它在你的应用甚至编译之前就已经运行。由于qmltc没有试图使用C++源代码来探究你的应用,所以它无法知道你作为用户所做的一些类型QML操作,因此使用QQmlEngine和相关运行时例程来暴露类型到QML,添加导入路径等。实际上,您需要创建行为良好的QML模块,并使用声明式QML类型注册

警告:尽管qmltc与QQmlEngine紧密结合并创建C++代码,但生成的类不能进一步暴露给QML并通过QQmlComponent使用。

生成输出基础

qmltc 的目标是与现有的 QML 执行模型兼容。这意味着生成的代码大致等同于内部 QQmlComponent 设置逻辑,因此您可以像现在一样理解您的 QML 类型 behavior、语义和 API —— 通过查看相应的 QML 文档来可视化检查。

然而,生成的代码仍然有些令人困惑,特别是考虑到您的应用程序应该直接在 C++ 端使用 qmltc 输出。生成的代码有两部分:CMake 构建文件结构和生成的 C++ 格式。前者在 qmltc 的 CMake API 中介绍,而后者在这里介绍。

考虑一个简单的 HelloWorld 类型,它有一个 hello 属性,一个打印该属性的函数,以及在创建该类型的对象时发出的信号

// HelloWorld.qml
import QtQml

QtObject {
    id: me
    property string hello: "Hello, qmltc!"

    function printHello(prefix: string, suffix: string) {
        console.log(prefix + me.hello + suffix);
    }

    signal created()
    Component.onCompleted: me.created();
}

当提供一个此 QML 类型的 C++ 代替品时,C++ 类需要 QML 特定的元对象系统宏Q_PROPERTY 装饰 hello 属性,Q_INVOKABLE C++ 打印函数和正常的 Qt 信号定义。同样,qmltc也会将给定的 HelloWorld 类型翻译成大约以下内容

class HelloWorld : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(QString hello WRITE setHello READ hello BINDABLE bindableHello)

public:
    HelloWorld(QQmlEngine* engine, QObject* parent = nullptr);

Q_SIGNALS:
    void created();

public:
    void setHello(const QString& hello_);
    QString hello();
    QBindable<QString> bindableHello();
    Q_INVOKABLE void printHello(passByConstRefOrValue<QString> prefix, passByConstRefOrValue<QString> suffix);

    // ...
};

尽管生成的类型的特定细节可能不同,但普遍的方面仍然存在。例如

  • 文档中的 QML 类型根据编译器可见信息翻译成 C++ 类型。
  • 属性被翻译成具有 Q_PROPERTY 声明的 C++ 属性。
  • JavaScript 函数变成 Q_INVOKABLE C++ 函数。
  • QML 信号被转换成 C++ Qt 信号。
  • QML 枚举被转换成具有 Q_ENUM 声明的 C++ 枚举。

另外,需要指出的是 qmltc 生成类名的方式。给定 QML 类型的类名是从定义该类型的 QML 文档自动推导出来的:不带扩展名(包括第一个 . 前面和后面的内容,也称为基本名称)的 QML 文件名成为类名。保留文件名的大小写。因此,HelloWorld.qml 会得到一个 class HelloWorld,而 helloWoRlD.qml 会在一个 class helloWoRlD 中。根据 QML 的一致性,如果 QML 文档的文件名以小写字母开头,则生成的 C++ 类被认为是匿名的,并带有 QML_ANONYMOUS 标记。

到目前为止,虽然生成的代码可以从 C++ 应用程序端使用,但您通常应限制对生成的 API 的调用。相反,您应该首选在 QML/JavaScript 中实现应用程序逻辑,并使用手写的 C++ 类型暴露给 QML,同时使用 qmltc 创建的类进行简单的对象实例化。虽然生成的 C++ 给您提供直接(并且通常是更快的)访问 QML 定义的类型元素,但理解此类代码可能是一项挑战。

已知限制

尽管涵盖了许多常见的 QML 功能,但 qmltc 仍在开发初期,还有一些内容尚未得到支持。

由 QML 定义的类型(如 QtQuick.Controls)组成的导入 QML 模块可能无法正确编译,即使这些由 qmltc 编译的 QML 定义的类型。目前,您可以可靠地使用 QtQmlQtQuick 模块以及任何只包含暴露给 QML 的 C++ 类的其他 QML 模块。

除了这些,还有一些更基本的特殊性需要考虑

  • Qt 的 QML 模块通常依赖于 C++ 库来执行繁重的任务。通常情况下,这些库不提供公共 C++ API(因为它们的主要用途是通过 QML)。对于 qmltc 的用户来说,这意味着他们的应用程序需要链接到私有 Qt 库。
  • 由于 qmltc 代码生成的特性,QML 插件在编译过程中无法使用。相反,使用插件的 QML 模块必须确保插件数据在编译时是可访问的。这样,这些 QML 模块将有可选的插件。在大多数情况下,编译时信息可以通过包含 C++ 声明的头文件和包含 C++ 定义的链接库来提供。负责用户代码的是(通常通过 CMake)包含头文件路径并链接到 QML 模块库。

注意:鉴于编译器的预览状态,您也可能会在 qmltc、生成的代码或其他相关部分中遇到错误。我们鼓励您在这种情况下提交错误报告

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