进阶 iced.rs 2026-09-13 21:01:26 · 1 阅读

第5章 Iced框架中的运行时机制

上一章我们用 iced 和 The Elm Architecture 构建了经典计数器界面。我们逐一拆解了它的每个基本组成部分——状态消息更新逻辑视图逻辑

但现在接下来该做什么呢?没错,我们已经拥有了用户界面的所有基本组件——正如我们在解剖分析中所学到的——但如何让它跑起来,仍不太清晰。

看来我们缺少某个东西,能把所有部分整合起来,让它们协同运行某个能创建并执行用户界面基本循环的东西——向用户展示控件,并对任何交互做出响应。

这个东西就是运行时(runtime)。可以把它理解为用户界面反馈循环发生的环境。运行时负责循环中的每个环节:初始化状态、生成消息、执行更新逻辑以及运行我们的视图逻辑

The Runtime

你可以这样想象运行时:它像一台巨大的引擎,却缺少四个基本组件。我们的任务就是把这些组件填上去——然后引擎就能运转了!

一个“神奇”的运行时

为了更好理解一个界面的生命周期,我们不妨探索一下一个基本运行时(虽然它非常“魔法”)的内部机制。

事实上,我们其实已经开始写运行时了!在实现计数器更新逻辑时,我们写了一个很小的测试来模拟用户:

#[test]
fn it_counts_properly() {
    let mut counter = Counter { value: 0 };

    counter.update(Message::Increment);
    counter.update(Message::Increment);
    counter.update(Message::Decrement);

    assert_eq!(counter.value, 1);
}

从技术上说,这是一个非常简陋的运行时。它初始化了状态、产生了一些交互,并执行了更新逻辑

当然,这些交互是编造的,而且非常短暂,也没涉及什么视图逻辑——离我们真正想要的目标还差很远。不过这是个不错的开始!让我们一步步扩展它。

初始化状态

我们的小 runtime 已经能正确初始化应用状态了:

// Initialize the state
let mut counter = Counter { value: 0 };

不过,我们可以利用 Default trait 来避免硬编码初始状态。直接派生它就行:

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

然后在 runtime 里使用 Counter::default

// Initialize the state
let mut counter = Counter::default();

区别看起来微不足道,但我们其实是在分离关注点——把应用的初始状态放在状态定义旁边,而不是和 runtime 混在一起。这样一来,我们的 runtime 最终或许就能配合任意应用工作了!

显示界面

好了!状态已经初始化完成,接下来呢?在用户能和界面交互之前,我们得先把界面显示出来。

这很简单!只需要在用户使用的操作系统上打开一个窗口,初始化合适的图形后端,然后把视图逻辑返回的 widget 渲染出来——当然还得正确排布!

什么?你完全不知道该怎么做?别担心,我有一个神奇的函数:display。它接收一个界面引用,然后把界面展示给用户。绝对好用!

use magic::display;

// Initialize the state
let mut counter = Counter::default();

// Run our view logic to obtain our interface
let interface = counter.view();

// Display the interface to the user
display(&interface);

看到了吧?很简单!开个玩笑——本章的目的不是教大家图形编程,而是帮助我们更好地理解 runtime 的工作原理。来点魔法也无妨!

收集交互

用户正在使用我们的界面,并已与界面进行了交互。我们需要仔细关注所有的交互操作,并生成相应 消息,即我们组件所定义的消息。

怎么做呢?当然又是靠魔法!我刚刚从我的高帽子里找到了这个 interact 函数——它接收一个界面,并输出对应用户最新交互的 消息

use magic::{display, interact};

// 初始化状态
let mut counter = Counter::default();

// 运行视图逻辑以获取界面
let interface = counter.view();

// 向用户显示该界面
display(&interface);

// 处理用户交互并获取消息
let messages = interact(&interface);

太棒了!interact 为我们返回了一个 消息列表——可以直接迭代使用。

响应交互

至此,我们已经收集了用户交互并将其转换为一系列 消息。为了正确响应用户,我们需要根据每条 消息 相应地更新 状态

幸运的是,这一步不再涉及什么魔法技巧——我们只需要使用 更新逻辑

use magic::{display, interact};

// Initialize the state
let mut counter = Counter::default();

// Run our view logic to obtain our interface
let interface = counter.view();

// Display the interface to the user
display(&interface);

// Process the user interactions and obtain our messages
let messages = interact(&interface);

// Update our state by processing each message
for message in messages {
    counter.update(message);
}

这样能确保状态始终与最新的用户交互保持同步。

循环往复

好了!状态已根据用户交互更新完毕。接下来,需要再次向用户显示生成的界面。然后,继续处理后续交互…… 接着,再次更新状态。然后…… 从头再来一遍!

这就是循环!当然,循环本身并不神奇——至少在我们写 Rust 时是这样:

use magic::{display, interact};

// Initialize the state
let mut counter = Counter::default();

// Be interactive. All the time! 
loop {
    // Run our view logic to obtain our interface
    let interface = counter.view();

    // Display the interface to the user
    display(&interface);

    // Process the user interactions and obtain our messages
    let messages = interact(&interface);

    // Update our state by processing each message
    for message in messages {
        counter.update(message);
    }
}

恭喜!我们刚刚写出了一个功能完备的运行时——当然,那些魔法属性除外。在这里,我们可以清晰地看到 Elm 架构(The Elm Architecture)的每个核心部分如何在应用程序的生命周期中发挥作用。

具体来说,

  • 状态 只初始化一次,
  • 视图逻辑 在启动时运行一次,之后每当一批交互处理完毕后再运行,
  • 更新逻辑 会为每一个由交互产生的 消息 执行一次。

冰雪法师

你可能会说:“这确实挺酷,但我又不是什么法师,到现在还是不知道怎么运行我写的那个计数器界面。我还有东西要数呢!”

没问题!iced 内置了一个和我们刚搭建的非常相似的 runtime,它自带了所需的魔法1——所以你不用自己去钻研那些黑魔法。

要运行我们的 Counter,只需调用 run

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

pub fn main() -> iced::Result {
    iced::run(Counter::update, Counter::view)
}

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

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

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

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

我们只需向runtime提供更新逻辑视图逻辑——剩下的它会自行搞定!

运行时能够从我们的更新逻辑视图逻辑的类型签名中,自动推断出状态消息的类型。状态的初始化利用了Default,正如前文所述。

需要注意的是,run 可能会失败,因此它返回的是 iced::Result。如果仅仅是为了运行应用程序,可以直接在 main 中返回这个结果。

就这样了!去享受计数吧——至少能坚持3亿年。

来自作者的备注

你暂时读到了本书的结尾!

我认为这已经足以作为库基础知识的快速入门介绍。还有更多深入的内容待发掘——但希望你能在此基础上开始动手实验、探索并从中获得乐趣。

本书尚未完全写完——还有许多我想在此涵盖的主题,例如:

  • 布局
  • 样式
  • 并发
  • 应用规模化
  • 扩展运行时
  • 以及更多内容!

在这些章节撰写完成之前,如果你想进一步探索和深入学习,可以查看额外资源章节。

希望你喜欢目前的阅读体验。敬请期待!

— Héctor


  1. 主要指 winitsoftbufferwgputiny-skiacosmic-text

上一篇
第4章 初探
下一篇
第6章 6章:文本

本篇用到的工具

iced
iced
Rust 生态中的跨平台 GUI 框架,采用 Elm 风格的 retained mode 架构

评论 (0)