ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【Freya 0.4 基础语法学与练】第3课:事件与交互

【Freya 0.4 基础语法学与练】第3课:事件与交互 引言本课定位前两课我们学会了怎么把元素摆到屏幕上、怎么用 use_state 管理状态。但一个只能看不能动的界面是没有灵魂的。本课的目标是让界面响应用户的操作——鼠标点击、悬停、移动、滚轮、键盘输入。依赖不变[dependencies] freya { git https://github.com/marc2332/freya, branch main }事件系统总览Freya 的事件通过 Builder 方法直接挂在 rect() 或 Button 上。所有事件处理器都接收一个闭包闭包的参数是事件对象里面包含事件的具体数据。事件分三大类鼠标/指针事件点击、进入、离开、移动、滚轮键盘事件按键按下、按键释放按钮专用事件Button 组件的 on_press写法统一为rect().事件方法(move|e|{// e 是事件对象里面包含事件数据})注意闭包前面的 move 关键字——它把外部变量比如 use_state 返回的状态的所有权移进闭包。如果你需要在闭包里修改状态这是必须的。鼠标点击 —— on_presson_press 是最常用的事件当用户在元素上按下并释放鼠标时触发。letmutclickeduse_state(||false);rect().width(Size::Pixels(200.)).height(Size::Pixels(60.)).center().background(if*clicked.read(){(0,180,0)}else{(180,0,0)}).color((255,255,255)).on_press(move|_|{*clicked.write()!*clicked.read();}).child(if*clicked.read(){已点击}else{点我})这段代码做了什么初始状态 clicked 为 false背景红色显示点我每次点击clicked 取反触发重渲染重渲染后背景变绿文字变已点击再点一次又变回去on_press 的闭包参数 _ 是事件对象如果你不需要事件数据用 _ 忽略即可。鼠标悬停 —— on_pointer_enter 和 on_pointer_leave这两个事件成对使用用来实现鼠标移入变色、移出恢复的悬停效果。letmuthovereduse_state(||false);rect().width(Size::Pixels(200.)).height(Size::Pixels(60.)).center().background(if*hovered.read(){(60,60,120)}else{(30,30,30)}).color((255,255,255)).on_pointer_enter(move|_|{*hovered.write()true;}).on_pointer_leave(move|_|{*hovered.write()false;}).child(鼠标移上来试试)原理很简单on_pointer_enter鼠标指针进入元素区域时触发把 hovered 设为 trueon_pointer_leave鼠标指针离开元素区域时触发把 hovered 设为 false背景色根据 hovered 的值动态变化这就是一个最基础的 hover 效果。实际项目中你可以用它来做按钮高亮、卡片浮起、下拉菜单展开等交互。鼠标移动 —— on_pointer_moveon_pointer_move 在鼠标指针在元素区域内移动时持续触发事件对象里包含当前鼠标坐标。letmutpositionuse_state(||(0.0_f64,0.0_f64));rect().width(Size::fill()).height(Size::fill()).center().color((255,255,255)).font_size(24.).on_pointer_move(move|e|{*position.write()(e.x,e.y);}).child(format!(鼠标位置[{:.0}, {:.0}],position.read().0,position.read().1))几个要点事件对象 e 有 e.x 和 e.y 两个字段表示鼠标相对于该元素的坐标这个事件触发频率很高每移动一帧就触发一次所以闭包里只做轻量操作position 的初始值类型标注为 (0.0_f64, 0.0_f64)因为 e.x 和 e.y 是 f64 类型滚轮事件 —— on_wheelon_wheel 在用户滚动鼠标滚轮时触发事件对象包含滚动方向和距离。letmutoffsetuse_state(||0.0_f64);rect().width(Size::fill()).height(Size::fill()).center().color((255,255,255)).font_size(32.).on_wheel(move|e|{*offset.write()e.delta_y;}).child(format!(滚动偏移量{:.0},offset.read()))e.delta_y 表示垂直方向的滚动量向下滚为正向上滚为负每次滚动都会累加到 offset 中键盘事件 —— on_key_down 和 on_key_up键盘事件挂在 rect() 上当该元素获得焦点时才能接收键盘输入。letmutlast_keyuse_state(||String::from(无));rect().width(Size::fill()).height(Size::fill()).center().color((255,255,255)).font_size(24.).focusable(true)// 必须设置否则无法获得焦点.on_key_down(move|e|{*last_key.write()format!(按下: {:?},e.key);}).on_key_up(move|e|{*last_key.write()format!(释放: {:?},e.key);}).child(last_key.read().to_string())关键细节.focusable(true) 是必须的。Freya 中只有标记为可聚焦的元素才能接收键盘事件。如果忘了加这行键盘事件永远不会触发e.key 包含按下的键的信息Button 组件的 on_pressButton 是 Freya 内置的组件它自带样式和点击反馈。用法和第2课学过的一样Button::new().on_press(move|_|{// 点击后的逻辑}).child(按钮文字)Button 的 on_press 和 rect 的 on_press 本质相同区别在于 Button 自带视觉反馈按下时的颜色变化等而 rect 需要你自己实现 hover/press 效果。事件与状态联动 —— 核心模式所有事件的本质都是同一个模式用户操作 → 触发事件 → 闭包内修改状态 → 状态变化触发重渲染 → UI 更新链条-核心运行机制这个链条是 Freya以及所有响应式 UI 框架的核心运行机制。你写的每一个交互功能都是在搭建这条链条。用户点击按钮↓on_press 闭包被调用↓*state.write() 新值↓Freya 检测到状态变化↓重新调用 app() 函数↓新的 UI 被渲染到屏幕上理解这个链条后你就能写出任何交互逻辑——不管多复杂本质上都是这条链的变体。综合练习 —— 可拖拽的色块把本课学到的事件组合起来做一个鼠标按住拖动色块的效果#![cfg_attr( all(not(debug_assertions), target_os windows), windows_subsystem windows)]usefreya::prelude::*;fnmain(){launch(LaunchConfig::new().with_window(WindowConfig::new(app).with_size(500.,400.)));}fnapp()-implIntoElement{// 色块位置状态letmutpos_xuse_state(||200.0_f64);letmutpos_yuse_state(||150.0_f64);// 是否正在拖拽letmutdragginguse_state(||false);// 拖拽开始时鼠标与色块左上角的偏移letmutoffset_xuse_state(||0.0_f64);letmutoffset_yuse_state(||0.0_f64);// 背景区域接收鼠标移动和释放事件letcanvasrect().width(Size::fill()).height(Size::fill()).background((240,240,240)).on_pointer_move(move|e|{if*dragging.read(){*pos_x.write()e.x-*offset_x.read();*pos_y.write()e.y-*offset_y.read();}});// 可拖拽的色块letblockrect().width(Size::Pixels(80.)).height(Size::Pixels(80.)).background((15,163,242)).shadow((0.,4.,12.,2.,(0,0,0,60))).on_press(move|e|{*dragging.write()true;*offset_x.write()e.x-*pos_x.read();*offset_y.write()e.y-*pos_y.read();});// 根容器rect().width(Size::fill()).height(Size::fill()).child(canvas)}这段代码的思路dragging 状态标记是否正在拖拽鼠标在色块上按下on_press→ 记录偏移量标记开始拖拽鼠标在画布上移动on_pointer_move→ 如果正在拖拽更新色块位置偏移量的作用防止色块跳到鼠标正中心保持按下时的相对位置注意事项关于闭包中的 move只要闭包需要访问外部的 use_state 变量就必须加 move。move 会把变量的所有权转移到闭包内部。如果你有两个事件处理器都需要访问同一个状态UseState 内部使用了引用计数克隆是廉价的——可以通过 .clone() 复制一份给第二个闭包。**关于事件触发频率 **on_pointer_move 在鼠标移动时每帧都会触发不要在它的闭包里做重计算。如果只是需要更新坐标直接写入 use_state 即可Freya 会自动做批量渲染。关于 focusable只有需要接收键盘事件的元素才需要设置 .focusable(true)。鼠标事件不需要这个属性。本课小结事件挂在哪直接通过 Builder 方法挂在 rect() 或 Button 上闭包格式move |e| { … }e 是事件对象鼠标点击.on_press(move |_| { … })鼠标悬停.on_pointer_enter() .on_pointer_leave() 成对使用鼠标移动.on_pointer_move(move |e| { … })通过 e.x / e.y 获取坐标滚轮.on_wheel(move |e| { … })通过 e.delta_y 获取滚动量键盘.on_key_down() .on_key_up()必须配合 .focusable(true)核心模式事件 → 修改状态 → 触发重渲染 → UI 更新贴代码第3课 · 练习答案与知识要点总结练习参考答案本课的综合练习是可拖拽的色块以下是标准答案#![cfg_attr( all(not(debug_assertions), target_os windows), windows_subsystem windows)]usefreya::prelude::*;fnmain(){launch(LaunchConfig::new().with_window(WindowConfig::new(app).with_size(500.,400.)));}fnapp()-implIntoElement{// 色块位置letmutpos_xuse_state(||200.0_f64);letmutpos_yuse_state(||150.0_f64);// 是否正在拖拽letmutdragginguse_state(||false);// 拖拽起始偏移量letmutoffset_xuse_state(||0.0_f64);letmutoffset_yuse_state(||0.0_f64);// 背景画布接收移动事件letcanvasrect().width(Size::fill()).height(Size::fill()).background((240,240,240)).on_pointer_move(move|e|{if*dragging.read(){*pos_x.write()e.x-*offset_x.read();*pos_y.write()e.y-*offset_y.read();}});// 可拖拽色块letblockrect().width(Size::Pixels(80.)).height(Size::Pixels(80.)).background((15,163,242)).shadow((0.,4.,12.,2.,(0,0,0,60))).on_press(move|e|{*dragging.write()true;*offset_x.write()e.x-*pos_x.read();*offset_y.write()e.y-*pos_y.read();});// 根容器rect().width(Size::fill()).height(Size::fill()).child(canvas)}答案拆解——这个练习考察了哪些知识点多状态协作5 个 use_state 分别管理位置、拖拽状态和偏移量它们之间互相依赖on_press 获取坐标通过 e.x / e.y 拿到鼠标按下时相对于元素的坐标on_pointer_move 持续追踪鼠标移动时持续触发通过 dragging 状态判断是否需要更新位置偏移量计算offset 鼠标坐标 - 色块坐标保证拖拽时色块不会跳到鼠标中心事件分工画布负责监听移动色块负责监听按下——各司其职知识要点总结要点一事件的统一写法所有事件都通过 Builder 方法挂在 rect() 或 Button 上接收一个闭包rect().事件方法(move|e|{// 处理逻辑})闭包前的 move 关键字把外部 use_state 变量的所有权移入闭包这是必须的——没有 move闭包无法访问外部状态。要点二鼠标点击 on_press当用户在元素上按下并释放鼠标时触发。如果不需要事件数据用 _ 忽略参数.on_press(move|_|{*state.write()新值;})如果需要鼠标坐标用 e.x / e.y 读取.on_press(move|e|{letxe.x;letye.y;})要点三鼠标悬停 on_pointer_enter on_pointer_leave这两个事件成对使用实现悬停效果on_pointer_enter鼠标进入元素区域时触发on_pointer_leave鼠标离开元素区域时触发典型用法是配合一个 bool 类型的 use_state进入时设 true离开时设 false背景色根据状态动态切换。要点四鼠标移动 on_pointer_move鼠标在元素区域内移动时持续触发每帧一次通过 e.x / e.y 获取当前坐标。注意事项触发频率很高闭包里只做轻量操作写状态即可不要做重计算坐标是相对于当前元素的不是相对于窗口的要点五滚轮事件 on_wheel用户滚动鼠标滚轮时触发通过 e.delta_y 获取垂直滚动量向下滚delta_y 为正值向上滚delta_y 为负值要点六键盘事件 on_key_down on_key_up键盘事件有一个前置条件元素必须设置 .focusable(true) 才能获得焦点进而接收键盘输入。rect().focusable(true)// 必须加否则键盘事件永远不触发.on_key_down(move|e|{...}).on_key_up(move|e|{...})鼠标事件不需要 focusable只有键盘事件需要。要点七Button 组件Button::new().on_press(回调).child(“文字”) 是 Freya 内置的按钮组件自带视觉反馈按下变色等。它和 rect 的 on_press 本质相同区别是 Button 帮你处理了 hover/press 的视觉状态rect 需要你自己实现。要点八事件驱动的核心链条所有交互都遵循同一条链路用户操作点击/移动/按键触发事件闭包闭包内通过 *state.write() 修改状态Freya 检测到状态变化重新调用 app() 函数新的 UI 渲染到屏幕不管交互多复杂本质上都是这条链的变体。理解这条链就能设计出任何交互逻辑。要点九闭包中访问状态的注意事项单个闭包访问状态直接 move 即可多个闭包需要访问同一个状态UseState 内部使用引用计数通过 .clone() 复制一份给第二个闭包克隆开销很小read() 返回只读引用write() 返回可变引用——只有 write() 能触发重渲染自查清单学完本课你应该能独立回答以下问题怎么让一个 rect 响应鼠标点击怎么实现鼠标悬停变色效果需要哪两个事件配合on_pointer_move 的 e.x / e.y 是相对于什么的坐标怎么让一个元素接收键盘输入必须加哪个属性on_press 闭包里为什么要加 move事件触发后UI 是怎么更新的完整链条是什么如果以上都能回答说明本课内容已经掌握可以进入第4课。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进