返回博客

Tauri macOS 隐藏到托盘后 Dock 图标失效:必须处理 RunEvent::Reopen

做 Tauri 2 桌面应用时,一个常见需求是「关闭窗口不退出,藏到系统托盘让后台进程继续跑」。实现起来也不复杂:拦截 WindowEvent::CloseRequested,调用 window.hide()api.prevent_close() 即可。托盘菜单里放一个「打开」按钮,调 show() 恢复。

但等用户测试的时候,你会发现一个让人抓狂的 bug:点击 Dock 图标,什么都没发生。窗口不出来,进程还活着,只有托盘菜单的「打开」能救场。macOS 用户对 Dock 有肌肉记忆,这读起来完全就是应用坏了。

现象

  1. 应用正常启动,主窗口显示。
  2. 点红色关闭按钮 → 窗口消失,托盘图标还在,后台进程继续运行(符合预期)。
  3. 点击 Dock 图标 → 毫无反应,窗口不出现。
  4. 右键托盘图标 → 点「打开」→ 窗口正常恢复。

根因

这是一个「写法合法、逻辑缺失」的典型陷阱,分三步拆解:

第一步window.hide() 只是把窗口藏起来,进程仍在运行,没有可见窗口。

第二步:macOS 对「点击一个已在运行的应用的 Dock 图标」有明确的语义——触发 AppKit 的 applicationShouldHandleReopen:hasVisibleWindows:。Tauri 把这个系统事件映射成了 RunEvent::Reopen

第三步:Tauri 项目里最常见的启动收尾写法是:

tauri::Builder::default()
    // ...
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

这个 .run() 的签名接受一个事件回调,但上面这种「一行式」写法把回调省掉了。于是 RunEvent::Reopen 被触发之后,没有任何代码接住它,直接静默丢弃。托盘菜单的「打开」之所以还能用,是因为它走的是一条完全独立的代码路径(手动调 show()),跟 Reopen 事件毫无关系。

坑:省略 run() 回调,Reopen 事件静默丢弃

.run(generate_context!()) 是合法的 Rust,编译通过,运行时也不报错。但它把所有应用级事件(包括 RunEvent::Reopen)都丢进了黑洞。在隐藏到托盘的场景下,后果就是 Dock 图标彻底失效。

正解

把末尾的 .run(ctx) 换成 .build(ctx).run(closure),在闭包里处理 RunEvent::Reopen,把隐藏的主窗口重新 show 出来并 set_focus:

use tauri::Manager; // get_webview_window 需要这个 trait
 
tauri::Builder::default()
    // ...setup、tray、on_window_event、invoke_handler 等配置不变...
    .build(tauri::generate_context!())
    .expect("error while building app")
    .run(|app_handle, event| {
        // macOS:点 Dock 图标唤起已隐藏到托盘的窗口
        if let tauri::RunEvent::Reopen { .. } = event {
            if let Some(w) = app_handle.get_webview_window("main") {
                let _ = w.show();
                let _ = w.set_focus();
            }
        }
    });

几个要点:

  • "main" 必须和 tauri.conf.jsonapp.windows[].label 一致。
  • RunEvent::Reopen 是公开枚举变体,不需要 #[cfg(target_os = "macos")]——它只在 macOS 上被触发,其他平台上这个 if let 永远不命中,代码跨平台编译没问题。
  • set_focus() 紧跟在 show() 之后,确保窗口弹到前台,而不是默默在后台出现。
  • 托盘菜单的 show() 调用保留,两条路径都要有。
正解:.build().run(closure) 显式接管 Reopen 事件

.run(ctx) 换到 .build(ctx).run(|handle, event| { ... }) 是零破坏性改动——其余所有 RunEvent 变体不处理就直接 fall through,行为不变。只需加三行即可让 Dock 图标恢复预期语义。

验证

  1. 重新构建并启动应用,确认主窗口正常显示。
  2. 点击窗口红色关闭按钮 → 窗口消失,托盘图标仍在,后台进程保持运行。
  3. 点击 Dock 图标 → 窗口重新出现并获得焦点。(修复前:无反应。)
  4. 托盘菜单「打开」→ 同样有效(保留原路径)。

一句话外卖

Tauri 的隐藏到托盘模式在 macOS 上必须显式处理 RunEvent::Reopen,否则 Dock 图标点击永远静默丢弃——.run(ctx) 省略回调是合法写法,但代价是整条 AppKit reopen 语义链路断掉。