【xilem0.4基础语法学与练】第13课:Xilem 0.4 最简短代码体现“一切皆设计图“

一、本课目标

用 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 渲染。

相关推荐
小雪崩1 小时前
嵌入式学习 day45:51单片机基础
学习·51单片机
彧azz3 小时前
Java学习语法篇:变量
java·学习
Yanjun2i3 小时前
Agent学习记录五:Pydantic验证
人工智能·python·学习
xqqxqxxq5 小时前
AI Agent学习:Agent协作工具技术笔记(李博杰《深入理解 AI Agent》4.6观后总结)
笔记·学习
白猫不黑5 小时前
大学网安方向学习路线:零基础与有基础的学习顺序整理
学习·web安全·计算机·网络安全·信息安全·编程·src漏洞
工业HMI实战笔记6 小时前
玩具制造HMI:注塑成型的快速换模与质量监控界面
人工智能·学习·交互·制造
你在我身后7 小时前
发展路线建议
学习
彧azz7 小时前
数据结构:关于图的学习
c语言·数据结构·笔记·学习·算法
苦猿的大模型日记7 小时前
Day57|从0学习 Claude Code(七):我没把说明书全塞给它,用到哪本才翻哪本
学习