Iced 入门:使用 State 、Messages 、Update 和 View 构建 Rust GUI

Iced 是一个 Rust GUI (图形用户界面) 框架,它使得构建跨平台界面变得简单有趣。其灵感来源于 Elm 架构,鼓励您将 UI (用户界面) 逻辑划分为四个部分:State (状态)、Messages (消息)、View (视图) 逻辑和 Update (更新) 逻辑。这种类似 Elm 的“信号系统 (signal system)”让您的应用程序逻辑能够直观地流动:用户交互发送 Messages (消息),这些消息会更新 State (状态),然后 View (视图) 会重新渲染以反映新的 State (状态)。在这篇文章中,我们将探讨 Iced 的主要特性,并通过一个简单的示例来逐步了解其信号系统 (signal system)。读完本文后,您将明白这些部分是如何协同工作,从而创建一个流畅的 GUI (图形用户界面) 工作流程。

为何选择 Iced?主要特性

在深入代码之前,让我们先强调一下 Iced 在 Rust 社区中广受欢迎的几个原因:

  • 跨平台支持 (Cross-Platform Support): 用一套代码库为 Windows、macOS、Linux 甚至 Web (WebAssembly (Wasm)) 构建 UI (用户界面)。您的 Rust GUI (图形用户界面) 可以在任何地方运行。
  • 简洁且类型安全的 API (应用程序编程接口): Iced 提供了一个清晰、“开箱即用 (batteries-included)”的 API (应用程序编程接口),易于使用。其响应式编程模型 (reactive programming model) 利用了 Rust 的类型安全特性,能够在编译时捕获许多 UI (用户界面) 逻辑错误。
  • 内置控件 (Widgets) 与布局 (Layout): 它自带一个响应式布局系统 (layout system) 和多种开箱即用的控件 (widgets)(按钮、文本输入框、可滚动容器等)。您可以组合这些控件轻松构建复杂的界面。
  • 异步友好 (Asynchronous Friendly): 需要在不冻结 UI (用户界面) 的情况下执行后台任务(如获取数据)?Iced 通过 futures 对异步操作提供了一流的支持,因此您可以保持应用程序的响应性。
  • 可扩展与可定制 (Extensible and Customizable): Iced 允许您定义自定义控件 (widgets) 和样式。虽然它有合理的默认设置,但您可以定制应用程序的外观和感觉,使其在视觉上更具吸引力。

了解了这些特性后,让我们来看看 Iced 的架构是如何运作的。

理解 Iced 的信号系统 (Signal System) (受 Elm 启发的架构)

Iced 遵循 Elm 风格的架构,这意味着 UI (用户界面) 是由 State (状态) 和 Messages (消息) 的循环来定义的。核心概念是:

  • State (状态): 您的应用程序在任何给定时间所持有的数据。
  • Messages (消息): 您关心的事件或用户交互(按钮点击、文本输入等)。
  • Update (更新) 逻辑: 响应 Messages (消息) 来更新 State (状态) 的代码。
  • View (视图) 逻辑: 基于当前 State (状态) 渲染用户界面的代码(并且可以在交互时触发 Messages (消息))。

这种模式将应用程序的 State (状态) 和逻辑与 UI (用户界面) 渲染解耦。事实上,您的 State (状态)、Messages (消息) 和 Update (更新) 代码不需要了解任何关于 GUI (图形用户界面) 控件 (widgets) 的信息——它们只是处理数据和事件的 Rust 代码。Iced 的运行时 (runtime) 负责将 UI (用户界面) 事件转换成 Message (消息) 信号并重新绘制界面。这是如何工作的呢? 在一个持续的循环中,当用户与控件 (widgets) 交互时,控件 (widgets) 会产生交互(即 Messages (消息))。这些 Messages (消息) 会更新应用程序的 State (状态),而改变后的 State (状态) 则决定了 View (视图) 接下来应该显示什么。这个反馈循环使得使用 Iced 感觉非常直观。

为了更具体地说明这一点,让我们使用 Iced 的信号系统 (signal system) 一步步构建一个简单的计数器应用。这个经典示例将展示 State (状态)、Messages (消息)、Update (更新) 和 View (视图) 是如何交互的。

示例:一个简单的计数器应用

假设我们想要一个窗口,其中包含一个数字显示和两个按钮:Increment (增加) (+) 和 Decrement (减少) (–)。按下按钮将增加或减少数字。以下是我们如何用 Iced 实现它:

1. 定义 State (状态): 我们首先定义应用程序的 State (状态) 来保存我们的计数器值。在 Iced (以及通常的 Elm 架构) 中,State (状态) 通常由一个包含必要字段的简单结构体 (struct) 表示。对于我们的计数器,我们只需要一个整数值:

#[derive(Default)]
struct Counter {
    value: i32,
}

上方: 我们的 Counter​ State (状态) 包含一个整数 value​。我们派生 Default​ 以便轻松创建一个初始 State (状态) (默认值为 0)。

2. 定义 Messages (消息): 接下来,我们列举可能的用户交互。在这个应用中,用户可以按下 Increment (增加) 按钮或 Decrement (减少) 按钮。我们将每个交互表示为一个 Message (消息) 。在 Rust 中,enum​ (枚举) 非常适合此目的:

#[derive(Debug, Clone, Copy)]
pub enum Message {
    Increment,
    Decrement,
}

上方: Message​ enum (枚举) 定义了我们的 UI (用户界面) 将处理的两个事件。当用户点击 "+" 时,我们将发送一个 Increment​ Message (消息),点击 "–" 则发送一个 Decrement​ Message (消息)。(此处派生 Debug​、Clone​ 和 Copy​ 只是为了方便日志记录,并且因为这些 Messages (消息) 很简单。)

3. 创建 View (视图): View (视图) 是一个函数,它接收当前 State (状态) 并返回一个控件 (widgets) 布局。在 Iced 中,您通过组合库提供的控件 (widgets) 来构建 UI (用户界面)。对于我们的计数器,View (视图) 将包含一个列 (column),其中有三个元素:一个 Increment (增加) 按钮,一个显示计数器值的文本,以及一个 Decrement (减少) 按钮。每个按钮都被连接到在按下时产生一个 Message​ (消息):

use iced::widget::{button, column, text, Column};

impl Counter {
    pub fn view(&self) -> Column<Message> {
        column![
            button("+").on_press(Message::Increment),
            text(self.value).size(50),
            button("-").on_press(Message::Decrement),
        ]
    }
}

上方: view​ 方法构建了一个简单的垂直列布局 (column layout)。我们创建一个 button("+")​ 并调用 .on_press(Message::Increment)​ 来指示点击此按钮应发送一个 Increment​ Message (消息)。类似地,“-”按钮发送一个 Decrement​ Message (消息)。在这两者之间,我们使用一个 text​ 控件 (widget) 来显示当前的计数器值 (self.value​),并将其字体大小设置为 50 以便查看。column![...]​ 宏只是将这些元素垂直排列。

请注意,View (视图) 使用当前 State (状态) (self.value​) 来显示数字,并且它本身不直接改变任何 State (状态)。它只描述 UI (用户界面) 以及在交互时要产生的 Messages (消息)。这种 声明式 (declarative) 方法意味着 View (视图) 是 State (状态) 的纯函数:每当 State (状态) 改变时,Iced 都会再次调用 view​ 来生成更新后的 UI (用户界面)。

4. 处理 Updates (更新): 现在我们需要处理 Messages (消息) 并相应地更新 State (状态)。我们编写一个 update​ 函数(或方法),它接收一个 Message (消息) 并改变 State (状态)。在我们的计数器中,逻辑很简单:将值加 1 或减 1:

impl Counter {
    pub fn update(&mut self, message: Message) {
        match message {
            Message::Increment => {
                self.value += 1;
            }
            Message::Decrement => {
                self.value -= 1;
            }
        }
    }
}

上方: update​ 方法匹配传入的 Message​ (消息) 并更新 Counter​ State (状态)。一个 Increment​ Message (消息) 使值增加 1,而 Decrement​ Message (消息) 使值减少 1。这是我们应用程序 State (状态) 发生改变的唯一地方——其他地方的用户界面代码不能直接改变 self.value​,除非通过发送 Messages (消息)。

有了这四个部分(State (状态)、Message (消息)、View (视图)、Update (更新)),我们就定义了 UI (用户界面) 的完整行为。我们只需要 运行 (run) 应用程序。在 Iced 中,您通常通过调用一个运行时 (runtime) 函数或实现一个将所有内容连接起来的 trait 来启动应用程序。例如,官方示例使用一个简单的调用 iced::run("Title", Counter::update, Counter::view)​ 来启动 GUI (图形用户界面) 窗口。在底层,Iced 将会:

  • 初始化 State (状态) (使用我们的 Counter​ 结构体 (struct),默认从值 0 开始)。
  • 渲染 View (视图) 通过调用我们的 Counter.view()​ 并在屏幕上布局控件 (widgets)。
  • 等待事件 (按钮点击等),当事件发生时,它会将其映射到相应的 Message​ (消息) (例如,"+" 按钮点击变成 Message::Increment​)。
  • 调用 Update (更新) 方法并传入该 Message (消息),从而更新 State (状态)。
  • 重新渲染 View (视图) 通过再次调用 view​ 来反映更新后的 State (状态),然后更新 GUI (图形用户界面)。

这个循环会自动持续。总而言之,Iced 自动获取您的 View (视图) 输出,处理事件以产生 Messages (消息),应用您的 Update (更新) 逻辑,并重绘界面。作为开发者,您只需关心定义应该发生什么;Iced 会负责何时调用正确的部分。结果是一个流畅、直观的流程:您按下一个按钮,一个 Message (消息) 被发出,您的 update​ 逻辑运行,屏幕上的标签发生变化。无需手动调用 UI (用户界面) 刷新!

结论 (个人经验)

使用 Iced 的信号风格架构 (signal-style architecture) 是一种耳目一新的体验。作为初学者,我发现这种逻辑结构非常流畅和令人愉快——将 State (状态)、Messages (消息) 和 View (视图) 更新分开感觉很自然。在我自己的项目中,这种清晰的结构使得推理用户行为如何转化为 State (状态) 变化和 UI (用户界面) 更新变得容易。

然而,随着我的应用程序逐渐变大,我确实遇到了一些挑战。最初,我把我所有的 State (状态)、Message (消息) 定义和 Update (更新) 逻辑都放在一个大文件里(例如 state.rs​)。这很快就变得难以管理。我学到的是,随着应用程序规模的扩大,最好将代码拆分成多个模块或组件——例如,您可以为 UI (用户界面) 的不同部分设置独立的 State (状态)/Update (更新) 逻辑,然后再将它们组合起来。Iced 的设计可以扩展到更大的应用程序,但这需要一些规划来保持代码的组织性(就像构建一个大型 Elm 应用或任何程序架构一样)。

我遇到的另一个难题是让 UI (用户界面) 在视觉上具有吸引力 (visually appealing) 。Iced 提供了基本的样式 (styling) 和主题化 (theming) 功能(当然,您也可以创建自定义样式或使用主题),但开箱即用的外观相当简约。为了获得一个精美的 UI (用户界面),您可能需要深入研究样式 API (styling API)——定义自定义控件 (widget) 样式或使用主题化 (theming) 支持来调整颜色、间距和其他视觉细节。这在一开始有点挑战性,但这是以类型安全的方式获得完全控制权的权衡,另外,它的中文字体支持也要手动设置。

Iced 的架构为在 Rust 中构建 GUI (图形用户界面) 提供了一个流畅直观的工作流程。关注点分离(State (状态)、Message (消息)、Update (更新)、View (视图))的清晰划分帮助我编写了易于理解和维护的逻辑。如果您刚开始,可以尝试像计数器这样的小例子来熟悉消息传递循环。

你可能也感兴趣