#qt #qml #编译时 #宏推导 #q-meta-object

qmetaobject_impl

为 qmetaobject crate 提供自定义推导

18 个版本

0.2.10 2023 年 11 月 3 日
0.2.9 2023 年 6 月 17 日
0.2.7 2022 年 2 月 23 日
0.2.5 2021 年 11 月 19 日
0.0.1 2018 年 7 月 9 日

#9 in #qml

Download history 213/week @ 2024-03-11 255/week @ 2024-03-18 556/week @ 2024-03-25 563/week @ 2024-04-01 170/week @ 2024-04-08 236/week @ 2024-04-15 237/week @ 2024-04-22 254/week @ 2024-04-29 264/week @ 2024-05-06 241/week @ 2024-05-13 274/week @ 2024-05-20 210/week @ 2024-05-27 181/week @ 2024-06-03 287/week @ 2024-06-10 276/week @ 2024-06-17 284/week @ 2024-06-24

1,035 个月下载量
208 个 crate 中使用(通过 qmetaobject

MIT 许可证

89KB
1.5K SLoC

Rust 的 QMetaObject crate

Crates.io Documentation

一个框架,使每个人都能使用 Rust 创建 Qt/QML 应用程序。它通过在编译时构建 QMetaObject,注册 QML 类型(可选通过暴露 QQmlExtensionPlugin)并提供惯用包装来实现。

目标

  • Rust 过程宏(自定义推导)在编译时生成 QMetaObject
  • 使用 cpp crate 的 cpp! 宏为主要的 Qt 类型创建绑定。
  • 使用此 crate 的用户不需要输入任何 C++ 代码或使用除 cargo 之外的其他构建系统。
  • 性能:避免任何不必要的转换或堆分配。

博客文章: https://woboq.com/blog/qmetaobject-from-rust.html

概述

use cstr::cstr;
use qmetaobject::prelude::*;

// The `QObject` custom derive macro allows to expose a class to Qt and QML
#[derive(QObject, Default)]
struct Greeter {
    // Specify the base class with the qt_base_class macro
    base: qt_base_class!(trait QObject),
    // Declare `name` as a property usable from Qt
    name: qt_property!(QString; NOTIFY name_changed),
    // Declare a signal
    name_changed: qt_signal!(),
    // And even a slot
    compute_greetings: qt_method!(fn compute_greetings(&self, verb: String) -> QString {
        format!("{} {}", verb, self.name.to_string()).into()
    })
}

fn main() {
    // Register the `Greeter` struct to QML
    qml_register_type::<Greeter>(cstr!("Greeter"), 1, 0, cstr!("Greeter"));
    // Create a QML engine from rust
    let mut engine = QmlEngine::new();
    // (Here the QML code is inline, but one can also load from a file)
    engine.load_data(r#"
        import QtQuick 2.6
        import QtQuick.Window 2.0
        // Import our Rust classes
        import Greeter 1.0

        Window {
            visible: true
            // Instantiate the rust struct
            Greeter {
                id: greeter;
                // Set a property
                name: "World"
            }
            Text {
                anchors.centerIn: parent
                // Call a method
                text: greeter.compute_greetings("hello")
            }
        }
    "#.into());
    engine.exec();
}

功能

  • 创建继承自 QObject、QQuickItem、QAbstractListModel、QQmlExtensionPlugin 等的对象
  • 导出 Qt 属性、信号、方法等
  • 也支持 #[derive)](与 Q_GADGET 相同)
  • 创建 Qt 插件(见 examples/qmlextensionplugins)
  • 部分场景图支持

需要 Qt >= 5.8

Cargo 功能

Cargo 提供了一种启用(或禁用默认)可选 功能 的方法。

日志

默认情况下,Qt 的日志系统未初始化,例如来自 QML 的 console.log 的消息不会去向任何地方。 "日志" 功能使您能够与 log crate(Rust 日志外观)集成。

该功能默认启用。要激活它,请尽早在 main() 中执行以下代码。

fn main() {
    qmetaobject::log::init_qt_to_rust();
    // don't forget to set up env_logger or any other logging backend.
}

chrono_qdatetime

启用 QDateQTime 与 Rust chrono 包的互操作性。

默认情况下禁用此功能。

webengine

启用 QtWebEngine 功能。有关更多详细信息,请参阅示例

默认情况下禁用此功能。

如果缺少 Qt C++ API 的包装器怎么办?

很可能你想要调用一个未被此 crate 包装的特定 Qt 函数。

在这种情况下,始终可以使用 cpp! 宏直接从你的 Rust 代码中访问 C++。

我们努力提高包装 API 的覆盖率,所以无论你需要什么,但目前在 GitHub 问题上提交功能请求或立即发送 Pull Request 都是受欢迎的。

教程:为 Qt C++ API 添加 Rust 包装器

本节介绍了如何创建自己的 crate 并带有新的 Qt 包装器,并遍历本存储库中提供的图形示例。

首先,设置你的 Cargo.tomlbuild.rs

  1. qttypes 添加到依赖项。很可能,你只会坚持最近在 crates.io 上发布的版本。

    [dependencies]
    qttypes = { version = "0.2", features = [ "qtquick" ] }
    

    将您需要的更多 Qt 模块添加到功能数组中。有关支持模块的完整列表,请参阅qttypes crate 文档
    如果您 绝对需要 最新未发布的变化,请使用此代替 version = "..."

    • path = "../path/to/qmetaobject-rs/qttypes"
    • git= "https://github.com/woboq/qmetaobject-rs"
  2. cpp 添加到依赖项,将 cpp_build 添加到构建依赖项。您可以在 cpp 文档 页面上找到最新的说明。

    [dependencies]
    cpp = "0.5"
    
    [build-dependencies]
    cpp_build = "0.5"
    
  3. qmetaobject/build.rs 复制 build.rs 脚本。它将运行 cpp_build 对您的包,使用由 qttypes/build.rs 提供的环境。

现在,每次构建您的包时,cpp! 宏的内容都将收集到一个大 C++ 文件中,并将其编译成静态库,稍后将其链接到最终二进制文件。您可以在 Cargo 目标目录中找到此 cpp_closures.cpp 文件。理解其内容可能有助于故障排除。

cpp! 宏有两种形式。

  • 带有双花括号 {{ 的形式将内容原样追加到 C++ 文件中。用于 #include 头文件,定义 C++ 结构体和类等。

  • 另一种是在运行时调用表达式。它通常使用(括号)编写,它需要一个参数列表[ ],并需要一个unsafe标记(要么在周围块中,要么作为第一个关键字内部)。

宏调用的顺序在每个文件(Rust模块)的基础上得到保留;但文件的处理顺序并不保证与mod声明的顺序相同。所以不要假设可见性——确保在每一个Rust模块的顶部包含所需的一切。

查看cpp文档以了解更多关于其内部工作方式的信息。

现在我们准备就绪,让我们看看Graph示例的代码。它位于examples/graph目录中。

在添加包装器之前,我们将相关的#include行放入一个双大括号{{宏中。

cpp! {{
    #include <QtQuick/QQuickItem>
}}

如果您需要包含您自己的本地C++头文件,您也可以这样做!查看main qmetaobject crate是如何在需要它的每个Rust模块中包含qmetaobject_rust.hpp头文件的。

接下来,我们声明一个自定义QObject,就像在概述中一样,但这次它派生自QQuickItem。尽管其名称如此,#[derive(QObject)]过程宏可以与多个基类一起工作,只要它被正确包装并实现了QObject特质。

#[derive(Default, QObject)]
struct Graph {
    base: qt_base_class!(trait QQuickItem),

    // ...
}

我们希望调用当前在qmetaobject-rs API中没有公开的QQuickItem::setFlag方法,所以让我们直接调用它。

impl Graph {
    fn appendSample(&mut self, value: f64) {
        // ...
        let obj = self.get_cpp_object();
        cpp!(unsafe [obj as "QQuickItem *"] {
            obj->setFlag(QQuickItem::ItemHasContents);
        });
        // ...
    }
}

或者,我们可以添加一个适当的方法包装器,然后在没有unsafe的情况下调用它。

#[repr(C)]
enum QQuickItemFlag {
    ItemClipsChildrenToShape = 0x01,
    ItemAcceptsInputMethod = 0x02,
    ItemIsFocusScope = 0x04,
    ItemHasContents = 0x08,
    ItemAcceptsDrops = 0x10,
}

impl Graph {
    fn set_flag(&mut self, flag: QQuickItemFlag) {
        let obj = self.get_cpp_object();
        assert!(!obj.is_null());
        cpp!(unsafe [obj as "QQuickItem *", flag as "QQuickItem::Flag"] {
            obj->setFlag(flag);
        });
    }

    fn appendSample(&mut self, value: f64) {
        // ...
        self.set_flag(QQuickItemFlag::ItemHasContents);
        // ...
    }
}

请注意,C++方法接受可选的第二个参数,但由于Rust和FFI粘合剂不支持可选参数,它通常省略(并默认为true)。为了改善这种情况,我们可以在Rust函数中添加第二个必需参数,或者实现两个具有稍微不同名称的“重载”,例如set_flag(Flag, bool) & set_flag_on(Flag)enable_flag(Flag)等。

如果对象在使用前被保证正确实例化和初始化,则不需要断言非空。这适用于以下情况:

  • 直接调用QObject::cpp_construct()并存储在不可移动的内存位置;

  • 构造QObjectPinned实例:对固定对象的任何访问或转换为QVariant都确保创建C++对象;

  • 将对象实例化为QML组件。在设置任何属性或调用任何信号/槽之前,QML引擎总是正确地将其初始化为默认值。

就这样!您已经实现了一个Qt C++类方法的包装器。现在给我们发送一个Pull Request吧。😄

与其他项目的比较

这个crate的主要目标是提供QML的惯用Rust绑定。它仅关注QML,而不是QWidgets或其他任何非图形Qt API。目标是消除用户了解或使用C++和其他构建系统的需求。这个crate在实现这一目标方面表现出色。

  • CXX-Qt 是将一些Rust集成到现有C++项目中的理想解决方案。CXX-Qt比这个crate更新,并利用了Rust特性,如属性宏,这在qmetaobject crate设计时还不存在。(当时稳定Rust中只有 derive procedural macro 是可用的)

  • Rust Qt Binding Generator 是另一个项目,有助于将Rust逻辑集成到现有的C++/Qt项目中。这个项目也是在Rust有了过程宏之前开发的,所以它使用外部.json文件来生成C++和Rust代码。

  • 还有一些较旧的crate试图为Qt C++ API提供Rust绑定。这些通常是自动生成的绑定,不是惯用的Rust,需要使用不安全的代码,并且不再维护。

  • Slint 是这个crate的作者创建的项目。Slint不是一个QML或Qt绑定,而是一个从QML启发的全新语言,完全用Rust实现。它与提供一种方法以惯用Rust API将UI添加到Rust项目的目标相同,但它使用自己的语言来替代QML进行UI。
    Slint Logo

由于重点已转向Slint的开发,qmetaobject crate目前仅处于被动维护状态。鼓励您探索 Slint 作为在Rust项目中创建GUI的激动人心、创新性的替代方案。

使用此crate构建的应用程序

依赖项

~1.5MB
~35K SLoC