
egui 关闭确认对话框实战用 close_requested 与 CancelClose 拦截窗口退出【免费下载链接】eguiegui: an easy-to-use immediate mode GUI in Rust that runs on both web and native项目地址: https://gitcode.com/GitHub_Trending/eg/egui导读在使用 Rust egui/eframe 开发桌面应用时常常需要在用户点击窗口关闭按钮后弹出确认框避免误关导致未保存数据丢失。本篇文章以仓库中 examples/confirm_exit 官方示例为主体完整讲解如何监听视口viewport的关闭请求、通过ViewportCommand::CancelClose取消关闭并弹窗询问、再通过ViewportCommand::Close真正退出这一完整流程并深入到 eframe 与 egui-winit 源码说明这套机制在底层是如何被处理的。读完本文你将能独立实现关闭前确认未保存工作提醒等常见交互。示例概览与运行方式该示例位于 examples/confirm_exit核心文件为 src/main.rs。它实现了一个 320×240 的小窗口标题为 Confirm exit窗口中只有一行文字 Try to close the window当你点击窗口右上角的关闭按钮时不会立刻退出而是弹出 Do you want to quit? 对话框提供 No 与 Yes 两个按钮。运行方式非常简单在仓库根目录执行cargo run -p confirm_exit示例的 Cargo.toml 显示它依赖工作区中的eframe启用默认特性并额外启用了__screenshot特性用于 CI 中通过EFRAME_SCREENSHOT_TO环境变量导出截图仓库中的 screenshot.png 即由此生成。核心机制一次关闭被拆成两半要理解这个示例关键是明白 egui 对关闭窗口的处理方式关闭请求close request与真正关闭close是分离的两件事。用户点击窗口关闭按钮时窗口系统发出关闭请求egui 将这一请求记录为视口信息中的close_requested标志应用可以在每一帧读取它应用可以选择取消这次关闭发送ViewportCommand::CancelClose也可以选择放行什么都不做或发送ViewportCommand::Close只有未被取消的关闭请求才会真正导致窗口销毁、程序退出。在 crates/egui/src/data/input/viewport_info.rs 中close_requested()的实现如下/// This viewport has been told to close. /// /// If this is the root viewport, the application will exit /// after this frame unless you send a /// [crate::ViewportCommand::CancelClose] command. /// /// If this is not the root viewport, /// it is up to the user to hide this viewport the next frame. pub fn close_requested(self) - bool { self.events.contains(ViewportEvent::Close) }注意注释中强调的两点对于根视口即主窗口应用会在该帧结束后退出除非发送了CancelClose对于子视口额外的原生窗口则需要用户自行决定是否隐藏它。对应地crates/egui/src/viewport.rs 中定义了两个方向相反的视口命令pub enum ViewportCommand { /// Request this viewport to be closed. /// /// For the root viewport, this usually results in the application shutting down. /// For other viewports, the [crate::ViewportInfo::close_requested] flag will be set. Close, /// Cancel the closing that was signaled by [crate::ViewportInfo::close_requested]. CancelClose, // ... }ViewportCommand是 egui 输出给后端eframe/winit的命令应用通过Context::send_viewport_cmd发送见 crates/egui/src/viewport.rs 的说明。逐步拆解示例代码1. 应用状态与入口#[derive(Default)] struct MyApp { show_confirmation_dialog: bool, allowed_to_close: bool, }示例用两个布尔值管理状态show_confirmation_dialog控制确认弹窗是否可见allowed_to_close记录用户是否已经明确同意关闭即点击过 Yes。main函数中通过eframe::NativeOptions设置初始窗口大小并调用eframe::run_native启动应用fn main() - eframe::Result { env_logger::init(); // Log to stderr (if you run with RUST_LOGdebug). let options eframe::NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]), ..Default::default() }; eframe::run_native( Confirm exit, options, Box::new(|_cc| Ok(Box::MyApp::default())), ) }2. 在每一帧检查关闭请求ui回调中在绘制完主面板后立即检查用户是否请求关闭if ui.input(|i| i.viewport().close_requested()) { if self.allowed_to_close { // do nothing - we will close } else { ui.send_viewport_cmd(egui::ViewportCommand::CancelClose); self.show_confirmation_dialog true; } }这里的逻辑是一旦检测到close_requested()为真说明用户点了关闭按钮如果allowed_to_close为真说明此前已确认过什么都不做关闭请求会被放行程序正常退出否则通过send_viewport_cmd(egui::ViewportCommand::CancelClose)取消本次关闭并置show_confirmation_dialog true弹出确认对话框。ui.input(...)是 egui 中安全读取当前帧输入的方式其中i.viewport()返回 ViewportInfoclose_requested()即为上文所述的关闭标志。3. 弹出确认对话框if self.show_confirmation_dialog { egui::Window::new(Do you want to quit?) .collapsible(false) .resizable(false) .show(ui.ctx(), |ui| { ui.horizontal(|ui| { if ui.button(No).clicked() { self.show_confirmation_dialog false; self.allowed_to_close false; } if ui.button(Yes).clicked() { self.show_confirmation_dialog false; self.allowed_to_close true; ui.send_viewport_cmd(egui::ViewportCommand::Close); } }); }); }点击No关闭对话框show_confirmation_dialog false并重置allowed_to_close false让下次关闭仍然需要确认点击Yes关闭对话框、置allowed_to_close true然后主动发送ViewportCommand::Close再次发起关闭请求。由于此时allowed_to_close已经为真下一帧检查到关闭请求时会放行程序随之退出。之所以点击 Yes 后要重新发送ViewportCommand::Close是因为用户第一次点关闭按钮时发出的请求已经被CancelClose取消了必须再发一次才能让窗口真正关闭。这正是该示例最精妙也最容易被忽略的一点。源码级原理eframe 如何处理关闭请求理解了应用层代码我们再深入 eframe 与 egui-winit 看这套机制如何落地。eframe 的 update 流程在 crates/eframe/src/native/epi_integration.rs 的update方法中eframe 先读取本次输入的关闭标志再运行 egui UI最后检查输出中是否包含取消命令let close_requested raw_input.viewport().close_requested(); // ... let full_output self.egui_ctx.run_ui(raw_input, |ui| { /* app 的 logic 与 ui */ }); if is_root_viewport close_requested { let canceled full_output.viewport_output[ViewportId::ROOT] .commands .contains(egui::ViewportCommand::CancelClose); self.handle_close_request(canceled); }handle_close_request的实现在同一文件的 L403-L409fn handle_close_request(mut self, canceled: bool) { if canceled { log::debug!(Closing of root viewport canceled with ViewportCommand::CancelClose); } else { log::debug!(Closing root viewport (ViewportCommand::CancelClose was not sent)); self.close true; } }可以看到决定窗口是否关闭的唯一依据就是本帧输出到根视口的命令里是否包含ViewportCommand::CancelClose。如果包含关闭被取消否则设置内部标志close true驱动主循环退出。也就是说即使示例代码不发送CancelClose只要应用在收到关闭请求的那一帧什么都不做窗口照样会关闭——取消关闭是主动动作必须显式发送命令。egui-winit 侧的事件来源关闭请求最初来自窗口系统。在 crates/egui-winit/src/lib.rs 附近winit 的WindowEvent::CloseRequested被转换为 egui 的视口事件最终体现为ViewportInfo中的ViewportEvent::Close从而被close_requested()读取。而ViewportCommand::Close与CancelClose这两个命令在 crates/egui-winit/src/lib.rs 的处理也值得注意ViewportCommand::Close { info.events.push(egui::ViewportEvent::Close); } ViewportCommand::CancelClose { // Need to be handled elsewhere }Close会把关闭事件重新推入视口事件列表再次触发close_requested而CancelClose在此处无需处理——它由上一节介绍的 eframeupdate流程在更高层看到并决定是否放行。这正是示例中点击 Yes 后再次发送 Close能被正常响应、且allowed_to_close标志能在下一帧生效的原因。与旧 API 的对比在 crates/eframe/CHANGELOG.md 中记录了本机制的演进App::on_close_eventhas been replaced withctx.input(|i| i.viewport().close_requested())andctx.send_viewport_cmd(ViewportCommand::CancelClose).同时crates/eframe/src/epi.rs 中App::on_exit的文档也明确提示If you need to abort an exit checkctx.input(|i| i.viewport().close_requested())and respond withegui::ViewportCommand::CancelClose.也就是说on_exit是程序确定退出前的最后一个钩子通常用于保存状态、释放资源而关闭拦截要放在每一帧的 UI 回调中完成两者职责不同可以组合使用用close_requestedCancelClose拦截并询问确认退出后由on_exit做收尾保存。实战扩展从确认退出到未保存工作提醒示例展示的是最简形态你可以在此基础上演化出更实用的场景1. 未保存数据检测。把allowed_to_close换成是否有未保存修改的判断例如if ui.input(|i| i.viewport().close_requested()) { if self.document.is_saved() { // 放行 } else { ui.send_viewport_cmd(egui::ViewportCommand::CancelClose); self.show_save_dialog true; } }2. 多视口应用。对于通过Context::show_viewport创建的子视口关闭请求同样体现在其close_requested标志上由于子视口关闭不会自动退出整个程序处理方式可以是确认后隐藏子视口见 crates/egui/src/context.rs 中关于子视口关闭标志的说明。3. 键盘快捷键触发关闭。也可以主动向视口发送ViewportCommand::Close来模拟关闭请求如响应CtrlW走完全相同的确认流程实现方式与示例中 Yes 按钮的代码一致。注意事项与平台差异必须在发出关闭请求的同一帧内决定是否取消egui 是立即模式immediate modeCancelClose的判定以该帧输出为准因此不能异步延时后才决定如果弹出的是模态确认框通常要保证在用户做出选择前后续每一帧都检查并CancelClose直到用户点击按钮。allowed_to_close标志的生命周期示例在用户点 Yes 后先置标志再发Close命令依赖下一帧的检查来放行。若你的确认流程跨越多帧例如确认框异步等待需要确保每次收到close_requested且尚未确认时都发送CancelClose否则窗口会提前退出。Web 端行为本示例针对桌面原生窗口eframe native。Web 端浏览器标签页的关闭由浏览器接管egui 无法可靠拦截close_requested机制主要适用于原生视口场景。调试日志示例启用了env_logger用RUST_LOGdebug cargo run -p confirm_exit运行时可以在 stderr 中看到 eframe 输出的 Closing of root viewport canceled ... / Closing root viewport ... 日志crates/eframe/src/native/epi_integration.rs有助于排查关闭流程是否按预期工作。小结confirm_exit示例虽然只有几十行代码却完整演示了 egui 视口关闭机制的三个关键环节读取关闭请求close_requested()→ 取消关闭ViewportCommand::CancelClose→ 确认后重新发起ViewportCommand::Close。从源码看eframe 在每一帧结束后根据根视口命令中是否含CancelClose决定是否真正退出epi_integration.rs这既是该示例正确运行的基础也是你在自己应用中实现关闭拦截、未保存提醒等功能的底层依据。相关完整源码与运行说明可继续查看 examples/confirm_exit以及 crates/eframe/src/epi.rs 中Apptrait 的接口文档。【免费下载链接】eguiegui: an easy-to-use immediate mode GUI in Rust that runs on both web and native项目地址: https://gitcode.com/GitHub_Trending/eg/egui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考