一、本课目标
用 Xilem 0.4 官方最小示例,亲手触摸"一切皆设计图"这个核心理念。代码总共不到 20 行,但每一行都在诠释同一个思想:你的代码不创建 UI,只画蓝图。
二、完整代码
以下是 Xilem 0.4 官方文档中的最小计数器示例:
rust
use winit::error::EventLoopError;
use xilem::view::{Axis, text_button, flex, label};
use xilem::{EventLoop, WindowOptions, WidgetView, Xilem};
// ① 状态:一个普通的 Rust 结构体
#[derive(Default)]
struct Counter {
num: i32,
}
// ② 设计图函数:状态 → View Tree
fn app_logic(data: &mut Counter) -> impl WidgetView<Counter> + use<> {
flex(Axis::Vertical, (
label(format!("{}", data.num)),
text_button("increment", |data: &mut Counter| data.num += 1),
))
}
// ③ 启动:把状态 + 设计图函数交给框架
fn main() -> Result<(), EventLoopError> {
let app = Xilem::new_simple(Counter::default(), app_logic, WindowOptions::new("Counter app"));
app.run_in(EventLoop::with_user_event())?;
Ok(())
}
运行效果:
窗口里显示一个数字 0 和一个按钮 increment,每次点击按钮,数字加 1。
三、逐行拆解:哪些是"画图",哪些是"施工"
第①步:定义状态
rust
#[derive(Default)]
struct Counter {
num: i32,
}
这就是你的全部状态------一个整数。没有 UI 对象,没有 Widget 引用,没有 Label、没有 Button。只有一个纯粹的数据结构。
第②步:app_logic 就是"画图函数"
rust
fn app_logic(data: &mut Counter) -> impl WidgetView<Counter> + use<> {
flex(Axis::Vertical, (
label(format!("{}", data.num)),
text_button("increment", |data: &mut Counter| data.num += 1),
))
}
这个函数做了且只做一件事:给定当前状态,返回一张"界面蓝图"(View Tree)。
逐个拆解:
- label(format!("{}", data.num)) ------ 返回一个 Label View 节点。本质是蓝图上标注"这里显示数字"。
- text_button("increment", |...|) ------ 返回一个 Button View 节点。本质是蓝图上标注"这里放个按钮,点击时做某事"。
- flex(Axis::Vertical, (...)) ------ 返回一个 Flex View 节点,包含上面两个子节点。本质是蓝图上标注"纵向排列"。
关键认知:
label(...) 和 text_button(...) 返回的不是屏幕上的控件,而是轻量级的数据结构。它们就像建筑师图纸上的标注------"这里放一扇门""这里刷蓝漆"------图纸本身不是房子,但施工队看着图纸就能把房子盖出来。
第③步:启动框架
rust
fn main() -> Result<(), EventLoopError> {
let app = Xilem::new_simple(Counter::default(), app_logic, WindowOptions::new("Counter app"));
app.run_in(EventLoop::with_user_event())?;
Ok(())
}
Xilem::new_simple 接收三样东西:
- 初始状态:Counter::default()(num = 0)
- 设计图函数:app_logic
- 窗口配置:标题叫 "Counter app"
然后 run_in 启动事件循环,框架接管一切。
四、运行时到底发生了什么
框架启动后,内部循环如下:
┌─────────────────────────────────────────────────────────────┐
│ 1. 框架调用 app_logic(&mut state) │
│ → 返回 View Tree A(第一张蓝图) │
│ A = { flex(Vertical, label("0"), button("increment")) }│
│ │
│ 2. 框架根据蓝图 A,创建真实的 Widget(屏幕上的像素) │
│ → 你看到:数字 0 + 一个按钮 │
│ │
│ 3. 用户点击按钮 │
│ → 闭包执行:state.num += 1 │
│ → 状态变了!num 从 0 变成 1 │
│ │
│ 4. 框架再次调用 app_logic(&mut state) │
│ → 返回 View Tree B(第二张蓝图) │
│ B = { flex(Vertical, label("1"), button("increment")) }│
│ │
│ 5. 框架 diff 蓝图 A 和蓝图 B │
│ → label 的文字从 "0" 变成 "1" → 需要更新 │
│ → button 没变 → 不动 │
│ │
│ 6. 框架只更新 label 的文字,button 保持原样 │
│ │
│ 7. 回到第 3 步,等待下一次交互... │
└─────────────────────────────────────────────────────────────┘
五、"一切皆设计图"到底意味着什么
把上面这段代码和传统框架(比如 Qt/GTK)对比,差异一目了然:
传统框架:你亲手造 UI 对象
rust
// 传统命令式(伪代码)
let label = Label::new("0"); // 你创建了一个 UI 对象
let button = Button::new("increment"); // 你又创建了一个 UI 对象
button.on_click(|| {
count += 1;
label.set_text(&count.to_string()); // 你手动同步 UI
});
你持有 label 和 button 的引用,你手动调用 set_text() 更新它们。状态和 UI 是两个独立的东西,你需要自己保证它们一致。
Xilem:你只画蓝图,从不碰 UI 对象
rust
// Xilem 声明式
fn app_logic(data: &mut Counter) -> impl WidgetView<Counter> + use<> {
flex(Axis::Vertical, (
label(format!("{}", data.num)),
text_button("increment", |data: &mut Counter| data.num += 1),
))
}
你的代码里:
- ❌ 没有 Label::new() ------ 你没创建任何 UI 对象
- ❌ 没有 label.set_text() ------ 你没更新任何 UI 对象
- ❌ 没有 self.label ------ 你没持有任何 UI 对象的引用
你写的 label(...) 返回的是一个数据结构(View 节点),不是屏幕上的控件。你只负责"画蓝图",框架负责"按蓝图施工"。
这就是"一切皆设计图"的含义:你的代码里不存在真实的 UI 对象,只有描述 UI 应该长什么样的数据。
六、三层架构:从蓝图到像素
Xilem 0.4 内部有三层树,你的代码只接触第一层:
第一层:View Tree(你写的)
本质是轻量级不可变数据结构,角色是蓝图/设计图。例子:label("0")、text_button("increment")。
第二层:Element Tree(框架内部)
本质是 View Tree 的运行时表示,角色是项目经理------管理生命周期、协调更新。你不需要关心这一层。
第三层:Widget Tree(框架内部)
本质是 Masonry 的真实 Widget 对象,角色是施工队------负责布局计算和 GPU 渲染。你不需要关心这一层。
你只写第一层。 框架自动把第一层翻译成第二层,再把第二层翻译成第三层。状态变化时,框架重新调用你的函数生成新的第一层,然后 diff 新旧第一层,计算出最小更新集,只更新第二层和第三层中真正变化的部分。
七、Cargo.toml 配置
要运行上面的示例,你的 Cargo.toml 需要添加:
toml
[dependencies]
xilem = "0.4"
winit = "0.30"
Xilem 0.4.0 于 2025 年 10 月发布,当前仍处于 Alpha 阶段,API 可能变动。
八、课后练习
练习 1:改成减法计数器
把 app_logic 改成同时显示一个 decrement 按钮,点击时 num 减 1。
提示: 在 flex 的元组里再加一个 text_button。
练习 2:加一个重置按钮
添加第三个按钮 reset,点击时把 num 重置为 0。
提示: 闭包里写 |data: &mut Counter| data.num = 0。
练习 3(思考题):为什么 app_logic 的参数是 &mut Counter 而不是 &Counter?
提示: 因为闭包需要修改状态(data.num += 1),所以需要可变引用。但 app_logic 本身只是读取状态来画蓝图------它不修改状态。状态修改发生在闭包里,闭包在用户交互时才被调用。
九、本课核心要点回顾
- app_logic 是纯函数 ------ 输入状态,输出 View Tree(蓝图)。
- View Tree 是数据结构 ------ 不是 UI 对象,是描述 UI 的数据。
- 你不碰 UI 对象 ------ 不创建、不更新、不持有引用。
- 框架负责施工 ------ diff 新旧蓝图,增量更新真实 Widget。
- 状态是唯一真相 ------ UI 是状态的"投影",状态变则 UI 自动变。
**一句话总结: **
在 Xilem 里,你写的每一行 UI 代码都是在"画蓝图",而不是在"造房子"。框架拿着你的蓝图去施工,你只管改状态,蓝图自动更新,房子自动翻新。这就是"一切皆设计图"。
练习答案贴代码
练习 1:改成减法计数器
rust
fn app_logic(data: &mut Counter) -> impl WidgetView<Counter> + use<> {
flex(Axis::Vertical, (
label(format!("{}", data.num)),
text_button("increment", |data: &mut Counter| data.num += 1),
text_button("decrement", |data: &mut Counter| data.num -= 1),
))
}
在 flex 的元组中追加一个 text_button,闭包里把 += 改成 -= 即可。框架会自动把新按钮渲染出来,不需要你手动"添加"任何东西。
练习 2:加一个重置按钮
rust
fn app_logic(data: &mut Counter) -> impl WidgetView<Counter> + use<> {
flex(Axis::Vertical, (
label(format!("{}", data.num)),
text_button("increment", |data: &mut Counter| data.num += 1),
text_button("decrement", |data: &mut Counter| data.num -= 1),
text_button("reset", |data: &mut Counter| data.num = 0),
))
}
再追加一个按钮,闭包里直接赋值 data.num = 0。三个按钮互不干扰,每个闭包只关心自己的逻辑。
练习 3(思考题):为什么参数是 &mut Counter 而不是 &Counter?
app_logic 的参数类型是 &mut Counter,但函数体本身只读不写------它只是读取 data.num 来生成蓝图。之所以需要 &mut,是因为闭包捕获了 data,而闭包内部需要修改状态(data.num += 1)。
这里有一个微妙的分层:
- app_logic 被调用时 → 只读状态,画蓝图
- 闭包被调用时(用户点击按钮时) → 修改状态
闭包捕获的是 &mut Counter 的可变借用,所以外层参数也必须是 &mut。如果改成 &Counter,闭包里的 data.num += 1 就编译不过了。
知识要点总结
"一切皆设计图"的核心含义
你的代码里不存在任何真实的 UI 对象。label(...)、text_button(...)、flex(...) 这些函数返回的全部是轻量级数据结构(View 节点),它们组合成一棵 View Tree。这棵树就是"设计图"------它描述"界面应该长什么样",但它本身不是界面。
你和框架的分工
你的职责只有一件事:写一个函数,输入当前状态,输出 View Tree。框架的职责是剩下的所有事:根据 View Tree 创建真实 Widget、监听用户交互、执行闭包修改状态、diff 新旧 View Tree、增量更新屏幕。
状态驱动 UI 的完整链路
状态变化 → 框架重新调用 app_logic → 生成新的 View Tree → diff 新旧 View Tree → 只更新变化的部分 → 屏幕刷新。你不需要手动同步状态和 UI,因为 UI 就是状态的函数输出。
闭包是状态修改的唯一入口
View Tree 中的交互(按钮点击等)通过闭包来表达。闭包捕获 &mut State,在用户交互时执行,修改状态。状态一旦改变,框架就会自动触发下一轮"画蓝图 → diff → 更新"的流程。
三层架构的边界
你只接触 View Tree(第一层)。Element Tree(第二层)和 Widget Tree(第三层)完全由框架内部管理,你不需要了解它们的细节。这种分层让你可以专注于业务逻辑,而不必关心布局计算和 GPU 渲染。