Rust Floem UI 框架使用简介

发布于:2025-06-06 ⋅ 阅读:(16) ⋅ 点赞:(0)

之前一篇博客Rust 开发的一些GUI库提到了floem库,今天我决定试用了一下,根据官网文档,食用使用方式如下:
floem

开始使用

让我们通过一个简单的项目来快速了解 Floem 的基础知识。

创建一个新的 Rust 项目

cargo new floem-app

添加 Floem 依赖

cd floem-app
cargo add floem

Hello World

在你的 main.rs 文件中,写入以下内容:

fn main() {
    floem::launch(|| "Hello, World!");
}

然后运行 cargo run,你将在 Floem 窗口中看到你的第一个 hello world。

看起来很简单,对吧?如果你查看 floem::launch 的定义,会发现它接收一个返回 IntoView 的闭包。如果你熟悉 Rust,IntoView 是一个 trait,包含方法 into_view,可以将实现该 trait 的类型转换为 Floem View。Floem View 是 Floem 的核心,可以是文本标签、按钮、文本输入框或滚动视图等。Floem 为 &str 类型内部实现了 IntoView,所以我们可以直接返回 “Hello, World!” 作为 IntoView

现在让我们深入一个更复杂的例子:计数器应用。

计数器应用

我们将创建一个非常简单的计数器,显示当前计数值,并有一个递增按钮和一个递减按钮。

在 main.rs 中写入以下内容:

use floem::{views::button, IntoView};

fn app_view() -> impl IntoView {
    (
        "Value: ",
        button("Increment"),
        button("Decrement"),
    )
}

fn main() {
    floem::launch(app_view);
}

运行后,你应该能看到 "Value: " 和两个巨大的 “Increment” 和 “Decrement” 按钮。

让我们通过添加一些“样式”来改进它:

use floem::{
    views::{button, Decorators},
    IntoView,
};

fn app_view() -> impl IntoView {
    (
        "Value: ",
        (button("Increment"), button("Decrement")).style(|s| s.flex_row().gap(6)),
    ).style(|s| s.flex_col().gap(6).items_center())
}

fn main() {
    floem::launch(app_view);
}

再次运行后,你会发现布局变得更合理了。在 Floem 中,视图通过 style 方法进行样式设置,该方法由 floem::views::Decorators 提供给所有实现了 IntoView 的类型。所以记得先导入该 trait,否则 Rust 编译器会报错。

Floem 的样式是通过 Taffy crate 实现的 Rust 版 Flexbox。如果你熟悉 Flexbox,会发现很多类似的地方。如果不熟悉,网上有很多关于 Flexbox 的教程,这些知识也适用于 Floem 样式。

响应式

但现在点击按钮还没有任何效果,因为还没有可变的值被显示或改变。那如何在 Floem 中实现响应式呢?

Signal (信号)是 Floem 响应式的基础。如果你熟悉 Solidjs 或 Leptos,这里的 signal 与那些框架类似。简单来说,如果你更新了一个 Signal 的值,所有“监听”该 signal 的内容都会收到通知,并根据更新触发相应操作。在我们的计数器例子中,我们希望计数值在 UI 中被更新。让我们创建一个信号 signal。

use floem::{
    prelude::create_rw_signal,
    views::{button, dyn_view, Decorators},
    IntoView,
};

fn app_view() -> impl IntoView {
    let counter = create_rw_signal(0);
    (
        dyn_view(move || format!("Value: {}", counter)),
        (button("Increment"), button("Decrement")).style(|s| s.flex_row().gap(6)),
    )
        .style(|s| s.flex_col().gap(6).items_center())
}

fn main() {
    floem::launch(app_view);
}

你会注意到我们创建了一个计数器信号 signal,并用闭包包裹了值的显示,通过 dyn_view 格式化计数器的值。你可能会问,为什么不用 format!("Value: {}", counter),毕竟 String 也实现了 IntoView。这样写虽然能编译通过,但会失去响应式。

解释如下:

与 Reactjs 等框架不同,视图创建函数只会被调用一次。因此 format!("Value: {}", counter) 也只会被调用一次,并且只会获取计数器的初始值 0。即使之后更新了计数器 signal 的值,Value: 0 也不会更新,因为不会再次运行。

所以 Floem 的响应式依赖于闭包,每当 signal 更新时,闭包会被反复执行。这带来了 Floem 的“细粒度响应式”特性,UI 不需要与之前的状态做 diff,只会更新需要更新的部分,无论视图树多深。即使计数值显示在很深的嵌套中,也能“直接”更新 UI,仅仅是那个文本标签部分,无需从根遍历视图树。

这也形成了 Floem 响应式的基本规则:如果你希望某些内容被更新,需要使用闭包。如果你在使用 Floem 时遇到某些内容没有更新,首先要检查“是不是用了闭包”。

动作

但按钮依然没有任何作用,因为我们还没有让按钮点击去更新计数器 signal。如果你这样写:

use floem::{
    prelude::create_rw_signal,
    views::{button, dyn_view, Decorators},
    IntoView,
};

fn app_view() -> impl IntoView {
    let mut counter = create_rw_signal(0);
    (
        dyn_view(move || format!("Value: {}", counter)),
        (
            button("Increment").action(move || counter += 1),
            button("Decrement").action(move || counter -= 1),
        )
            .style(|s| s.flex_row().gap(6)),
    )
        .style(|s| s.flex_col().gap(6).items_center())
}

fn main() {
    floem::launch(app_view);
}

我们只需在按钮上调用 action 方法,递增或递减计数器 signal 的值。现在你就有了一个可用的计数器示例。

本教程到此结束。想了解更多 Floem,可以查看我们仓库中的 示例,或阅读 API 文档

后记

在Windows系统中,默认生成的GUI界面在运行的时候会弹出命令行窗口(控制台),要去掉这个窗口,可以通过以下cargo命令实现:

  • 对于debug版:
cargo rustc -- -Clink-args="/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup"
  • 对于release版:
cargo rustc --release -- -Clink-args="/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup"