窗口交互规范直接影响用户对应用的“第一印象”——比如点击关闭按钮是直接退出,还是最小化到系统托盘?打开新窗口时是独立窗口还是子窗口?这些细节虽然小,但处理不好会显得很业余。本节梳理几个常见的交互模式及其在 Tauri 中的实现思路,以“实用、可靠”为目标。
1. 关闭到托盘(而不是直接退出)
很多工具类应用(如笔记、通讯、下载器)希望用户点击窗口的 ✕ 按钮时,应用最小化到系统托盘继续运行,而不是完全退出。在 Tauri 中可以把“关闭”事件拦截下来,改为隐藏窗口并显示托盘图标。
典型实现步骤
- 关闭默认行为
在窗口配置中禁用关闭按钮的默认退出行为(或直接在 Rust 侧监听窗口事件)。
- 拦截关闭事件
使用 window.on_close_requested 来阻止窗口关闭并隐藏窗口。
- 托盘图标
在应用启动时(setup 或 build 阶段)创建系统托盘,并绑定菜单(如“显示窗口”、“退出”)。
- 退出逻辑
只有用户通过托盘菜单点击“退出”时,才真正调用 app.exit()。
Rust 侧核心代码片段示意
// 在 main 或 build 里设置
let window = /* ... */;
let window_clone = window.clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
// 阻止默认关闭
api.prevent_close();
// 隐藏窗口(最小化到托盘)
window_clone.hide().ok();
}
});
// 创建托盘
use tauri::tray::{TrayIconBuilder, MouseButton, MouseButtonState, TrayIconEvent};
use tauri::menu::{MenuBuilder, MenuItemBuilder};
let show_item = MenuItemBuilder::with_id("show", "显示窗口").build(app)?;
let quit_item = MenuItemBuilder::with_id("quit", "退出").build(app)?;
let menu = MenuBuilder::new(app).items(&[&show_item, &quit_item]).build()?;
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.on_menu_event(|app, event| {
match event.id().as_ref() {
"show" => {
// 获取窗口并显示
app.get_webview_window("main")
.expect("no main window")
.show()
.ok();
}
"quit" => {
app.exit(0);
}
_ => {}
}
})
.build(app)?;
注意事项
- 第一次启动时窗口一般直接显示;而退出再重启时,如果上次是“隐藏到托盘”,恢复时通常会直接展示窗口,避免用户困惑。
- macOS 中窗口全关闭但应用不退出的情况很常见,可以用托盘或 Dock 菜单保持一致。
2. 最小化逻辑
最小化是系统标准行为,通常无需特殊处理。但如果你想增强体验,可以在最小化时顺便“模糊窗口内容”以保护隐私,或者当窗口最小化后如果有新通知,自动恢复窗口。
- 隐私遮挡:监听
WindowEvent::Minimize和WindowEvent::Restore,在前端执行页面模糊 CSS 或替换截图。 - 自动恢复:在前端或 Rust 侧收到新消息时,调用
window.unminimize()和window.set_focus()。 - 减少内存:WebView 在后台时系统会自动回收部分资源,Tauri 本身已经很轻,通常无需主动干预。
3. 多窗口协作
有些应用需要打开多个窗口(如主窗口 + 设置窗口、主窗口 + 预览窗口),它们之间可能需要通信或保持某种约束。
创建新窗口
通过前端调用 tauri::WebviewWindowBuilder 或者在 Rust 中预先注册多个窗口(推荐在 tauri.conf.json 预定义,因为这样可以提前管理生命周期)。
// 在命令中创建新窗口
use tauri::WebviewWindowBuilder;
#[tauri::command]
fn open_settings(app: tauri::AppHandle) {
WebviewWindowBuilder::new(
&app,
"settings",
tauri::WebviewUrl::App("settings.html".into())
)
.title("设置")
.inner_size(500.0, 400.0)
.resizable(false)
.build()
.unwrap();
}
窗口间通信
- 事件系统:使用
app.emit("event_name", payload)和前端监听listen来向所有窗口广播,或者指定窗口标签emit_to。 - 状态管理器(State):如果多个窗口需要共享状态,可以在 Rust 中维护一个
Mutex<SharedState>或使用 Tauri 的State管理,前端通过调用命令来读写。 - 直接获取窗口:
app.get_webview_window("label")可以获取任何已存在的窗口实例,然后调用其方法(如set_title、close等)。
约束建议
- 单例窗口:设置窗口、关于窗口等最好在打开前检查是否已存在,避免重复打开。
- 窗口关闭时清理资源:利用
on_window_event处理Destroyed事件。 - 主窗口关闭时,附加窗口可以自动关闭。可以在主窗口的关闭或托盘退出逻辑里遍历所有窗口并关闭。
实际案例
一个 Markdown 编辑器主窗口,点击“导出 PDF”时打开预览窗口,预览窗口可以实时接收主窗口发送的渲染内容,完成后点击关闭自动销毁,不干扰主窗口。这种协作模式用“事件 + 窗口标签”即可轻松实现。
总结
窗口交互不应是“能用就行”,而应该是“用户不用思考”的直觉体验。Tauri 提供的窗口 API 和托盘机制非常灵活,可以很自然地实现关闭到托盘、最小化隐私保护以及多窗口协作等需求,而且代码量很少。在所有平台保持一致性的同时,也可以针对不同平台做一些细节调整(如 macOS 菜单栏)。