外置事件
外置事件是给已经存在的控件追加事件逻辑的方式。你不需要为了处理一次点击、一次鼠标移动、一个快捷键,就去实现一个新的 View。把控件创建出来以后,接着链式调用 on_* 方法,把闭包交给 Flor 就可以。
这些 on_* 方法来自 EventBuilder,使用前先导入它:
完整签名见 Handler API。这一页先按使用过程来学:从一次点击开始,再逐步用到鼠标位置、键盘、焦点、生命周期和拖放。
第一个点击事件
最常见的用法,是在控件后面直接接 .on_click(...):
on_click 的完整闭包有三个参数:
刚开始可以像上面一样写成无参数闭包,只关注业务逻辑。需要知道事件来源、鼠标位置或修饰键状态时,再把参数名字补回来。鼠标事件也支持只接收 ViewId,或者省略 ViewId 只接收 KeyState 和 MousePosition。
如果你在不同示例里看到参数数量不一样,不是因为事件有多套派发规则,而是同一个 handler 转换机制在起作用。事件 Builder 会根据闭包、函数或方法项的签名,把它转换成目标 handler。带有额外事件参数的 handler 通常支持四种填写形态:
只有 ViewId 一个完整参数的事件,例如 on_mouse_enter、on_create,只有“完整参数”和“无参数”两种有效形态。底层机制见本页的 机制说明,完整签名见 Handler API。
事件闭包通常会写 signal、发命令、打开窗口、请求重绘,或者把业务消息投递到自己的任务系统里。只要记住一件事:事件闭包会在 GUI 事件处理流程中执行,不要在里面做长时间阻塞的工作。
任何 View 都可以挂事件
EventBuilder 是给所有实现了 ViewIdentity 的类型实现的,所以事件不是按钮专属。一个标签、一个容器、一个自定义控件,只要能提供 ViewId,都可以挂外置事件。函数返回的 impl IntoView 也可以继续链式绑定事件:
完整参数里的第一个参数是触发事件的 ViewId。它可以用来访问控件状态、请求重绘、设置焦点、捕获鼠标等。普通业务代码不一定需要它,可以直接省略;当你要把事件和具体控件关联起来时,它就是最直接的入口。
鼠标位置和按键状态
鼠标类事件大多使用同一组参数:
key_state 记录事件发生时的鼠标按钮和修饰键状态;mouse_position 保存坐标。对 on_mouse_move、on_button_down、on_button_up、on_click、on_double_click 这类鼠标命中事件来说,坐标会被转换成目标控件的局部坐标,(0, 0) 表示控件左上角。
点击事件是合成事件:左键按下和松开都落在同一个命中控件上时,Flor 才会触发 on_click。如果只是想知道按钮什么时候按下或松开,使用 on_button_down 和 on_button_up。
右键也有对应事件:
中键当前暴露了 on_middle_button_down、on_middle_button_up 和 on_middle_button_double_click。源码里有内部的 middle click 合成逻辑,但外置事件 API 当前没有 on_middle_button_click。
键盘事件要先拿到焦点
键盘事件派发给当前获得焦点的控件。普通控件如果想参与 Tab 焦点顺序,需要配合 focus_index。更完整的焦点说明见 焦点机制。
on_key_down 和 on_key_up 都要返回 HandleResult。键盘事件可以使用完整参数 |view_id, code, is_alt, is_ctrl, is_shift|,也可以像上面一样省略 ViewId:
Flor 内部控件逻辑和外置 handler 都可能返回 Handled。只要其中任意一边返回 Handled,最终结果就是 Handled。
焦点变化
on_focus 和 on_blur 用于监听焦点进入和离开。闭包收到 ViewId 和一个 u16 类型的虚拟焦点序号。
大多数控件只有一个焦点点位,这个序号通常是 0。少数复合控件可以通过 View::on_focus_count 暴露多个虚拟焦点点位,这时同一个控件内部可以区分不同焦点位置。
创建事件
on_create 会在控件树挂载到窗口以后触发。它适合做依赖 ViewId 已经进入控件树的初始化工作。
如果只是初始化普通 Rust 数据,通常直接在创建控件前完成即可。只有当逻辑依赖控件已经注册、已经有 ViewId 和窗口关系时,再考虑 on_create。
滚轮事件
当前滚轮外置事件的 API 名称是 on_wheel_settings_changed。它在鼠标滚轮消息路径中触发,闭包收到滚动方向、滚动量、按键状态和鼠标位置。
滚轮派发会从命中的控件向上寻找可滚动祖先;如果没有找到,就投递给窗口根控件。当前这条路径传入的是窗口客户区坐标,而不是目标控件局部坐标。
拖放事件
拖放事件需要启用 drag-drop feature。常见流程是:
on_drag_enter判断拖入的数据格式,设置DropEffect;on_drag_over在拖拽移动时持续更新效果;on_drop读取真正的数据并完成业务处理;on_drag_leave清理悬停状态。
on_drag_enter 和 on_drag_over 收到的是可用格式列表,还没有真正取出数据;on_drop 才会收到 DragData。和滚轮一样,当前拖放路径传入的是窗口客户区坐标。
使用建议
外置事件适合应用层逻辑:点击按钮修改 signal、按快捷键触发命令、拖放文件到面板、在鼠标移动时更新预览状态。
如果你是在写控件库,并且事件逻辑是控件本身不可分割的一部分,优先实现 View 的内部 on_* 方法。比如一个输入框如何处理 IME、光标、选区和文本编辑,应该属于控件内部逻辑;应用侧只需要绑定更高层的外置事件或控件自己暴露的业务回调。
事件闭包需要满足 Send + Sync + 'static。常见做法是捕获 signal 句柄、Arc、通道发送端,或者其他能安全长期保存的数据。不要捕获临时引用。
机制说明
这一节用于理解外置事件在 Flor 里的位置。普通使用不需要先读这里。
每个 ViewId 创建时,Flor 会在 VIEW_STORAGE.handlers 里放入一个默认的 ViewHandler。ViewHandler 是一组可选 handler 槽位,例如 on_click_handler、on_key_down_handler、on_focus_handler。
当你调用:
EventBuilder 会取出这个 view 的 handler 存储,把闭包通过 IntoEventHandler 转换成对应的 handler 包装类型,并写入 ViewHandler 的 on_click_handler 槽位。方法返回原来的 self,所以它可以自然地继续链式调用。
这个转换是 trait 驱动的:完整参数签名先走 handler 包装类型的 From<F> 实现,简化签名再由 IntoEventHandler<T, Args> 的其它实现补齐被省略的参数。Args 是给 Rust 做 trait 推导的标记类型,调用事件方法时一般不需要手写;当你自己封装一个接收事件 handler 的函数时,才需要把这个泛型带出去,写法见 框架 DSL 的方法组合控件。
事件从平台层进入 Flor 后,大致路径是:
- 平台窗口过程把系统消息转换成
Message; WindowsProcHandler把消息交给windows::event;- 事件总线按事件类型处理:鼠标事件先命中测试,键盘事件找当前焦点,拖放事件维护当前拖放目标;
- 找到目标
ViewId后调用对应的call_*方法; call_*先执行控件内部的View::on_*,再执行外置 handler。
大多数事件都是“内部逻辑 + 外置逻辑”都会执行。键盘事件会把两边的 HandleResult 合并:任意一边返回 Handled,最终就是 Handled。拖放事件会先用内部 on_drag_* 的返回值初始化 DropEffect,再把 &mut DropEffect 交给外置 handler 修改。
Tooltip 是一个内部特例:它的 call_tooltip_show 和 call_tooltip_hide 使用覆盖模式,如果存在外置 handler,就只执行外置 handler;否则执行控件内部方法。不过当前 EventBuilder 没有公开 tooltip 对应的链式方法,普通用户暂时不需要依赖这个细节。
还有一些 handler 槽位已经在 API 中存在,但当前事件总线没有完整派发到外置 handler,例如 on_context_menu、on_destroy、on_resize、on_close_requested、on_work_area_changed、on_dpi_change 和 on_theme_changed。查询具体状态时,以 Handler API 的“当前派发状态”为准。

