系统托盘、菜单与全局快捷键

效率类桌面应用的三件套:系统托盘让应用常驻后台、原生菜单提供标准操作入口、全局快捷键随时唤起。Tauri 2.x 把这些能力统一进核心 API,本篇给出托盘图标、应用菜单与全局快捷键的实现方式与联动设计。

一、系统托盘

托盘图标在 Tauri 2 中由 TrayIconBuilder 创建,并挂到 Builder 上:

use tauri::{
    menu::{Menu, MenuItem},
    tray::TrayIconBuilder,
    Manager,
};

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let quit = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
            let show = MenuItem::with_id(app, "show", "显示主窗口", true, None::<&str>)?;
            let menu = Menu::with_items(app, &[&show, &quit])?;

            TrayIconBuilder::new()
                .icon(app.default_window_icon().unwrap().clone())
                .menu(&menu)
                .on_menu_event(|app, event| match event.id.as_ref() {
                    "quit" => app.exit(0),
                    "show" => {
                        if let Some(window) = app.get_webview_window("main") {
                            let _ = window.show();
                            let _ = window.set_focus();
                        }
                    }
                    _ => {}
                })
                .build(app)?;
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

托盘图标资源放在 src-tauri/icons 下,图标路径与多尺寸格式见「图标、资源与本地化」。API 形态以官方文档为准。

常见交互 实现要点
点击托盘显示窗口 on_tray_icon_event 中 show 加 set_focus
关闭按钮只隐藏 拦截 CloseRequested 事件,隐藏而非退出
右键菜单 Menu 挂到 TrayIconBuilder
动态更新菜单 持有 menu 引用并 rebuild

窗口拦截关闭的写法见「窗口管理:创建、多窗口与装饰」。

二、应用菜单

应用菜单(菜单栏)用 Menu 与 MenuItem 组合,可以为单个窗口设置,也可以设为全局应用菜单:

use tauri::menu::{Menu, MenuItem, Submenu};

fn main() {
    tauri::Builder::default()
        .menu(|app| {
            let new_item = MenuItem::with_id(app, "new", "新建", true, Some("CmdOrCtrl+N"))?;
            let file_menu = Submenu::with_items(app, "文件", true, &[&new_item])?;
            Menu::with_items(app, &[&file_menu])
        })
        .on_menu_event(|app, event| {
            if event.id.as_ref() == "new" {
                let _ = app.emit("menu-new-file", ());
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

菜单事件推给前端用事件系统最顺手,前端订阅 menu-new-file 即可响应,事件写法见「事件系统:Rust 主动通知前端」。macOS 上应用菜单是系统级行为,Electron 用户对此熟悉,差异对比见「Tauri 是什么:与 Electron 的架构对比」。

三、全局快捷键

全局快捷键(系统范围内生效)由 tauri-plugin-global-shortcut 提供:

use tauri_plugin_global_shortcut::{Code, Modifiers, Shortcut, ShortcutState};

fn main() {
    tauri::Builder::default()
        .plugin(
            tauri_plugin_global_shortcut::Builder::new()
                .with_shortcut("CmdOrCtrl+Shift+P")?
                .with_handler(|app, _shortcut, event| {
                    if event.state == ShortcutState::Pressed {
                        if let Some(window) = app.get_webview_window("main") {
                            let _ = window.show();
                            let _ = window.set_focus();
                        }
                    }
                })
                .build(),
        )
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
注意点 说明
快捷键冲突 与系统或输入法冲突时不生效
注册时机 应用启动后注册,退出时自动注销
动态注册 支持运行时增删,需管理映射
权限 在 Capability 中声明插件权限

快捷键设置页是常见需求:把用户自定义的快捷键存数据库或配置文件,启动时读取注册,存储方案见「全局状态:tauri::State 与共享数据」。

四、三者联动设计

典型组合:托盘常驻加全局快捷键唤起加菜单提供标准操作。设计建议用一张表梳理数据流:

触发源 Rust 响应 前端响应
托盘菜单项 窗口管理 emit 事件
应用菜单 emit 事件 监听后执行操作
全局快捷键 显示聚焦窗口 路由跳转到对应页面

统一原则:Rust 负责系统能力与窗口,前端负责业务响应,中间用事件解耦。这样菜单项文案与页面状态完全由前端管理,多语言时尤其方便。

托盘与菜单 API 在 2.x 中变动较频繁,写代码时以官方文档为准,遇到 Manager、AppHandle 等类型不熟悉可以翻 Rust 入门;样板代码生成可参考 Claude Code 指南;批量调试日志中的事件名时 正则 mastery 是好帮手。

六、托盘应用的设计陷阱

常驻类应用看似简单,实际上有一堆细节:托盘菜单的国际化、图标的深浅色适配、macOS 上应用退出与窗口隐藏的行为差异。重点问题列表:

陷阱 说明
macOS 退出习惯 Dock 右键退出与 Cmd 加 Q 的处理
单实例感知 已运行时再启动应聚焦而不是开第二个
图标模板化 macOS 托盘图标应为模板图自动适配深浅色
菜单文案换行 不同语言长度差异
快捷键失效反馈 冲突时静默失败,需要可视化提示

全局快捷键冲突检测的建议做法:维护一份常见系统快捷键黑名单文档,注册失败时给出明确提示而不是忽略错误。用户自己挑快捷键时,提供输入框按下即录入的交互,比手工填字符串友好得多。

把托盘、菜单、快捷键三件套视为「应用的系统侧门面」,它们是用户与应用建立肌肉记忆的地方,值得在设计稿里同主界面一起打磨。

延伸阅读

  • 窗口管理:创建、多窗口与装饰——托盘联动的窗口侧
  • 事件系统:Rust 主动通知前端——菜单事件到前端的桥梁
  • 自动更新与版本分发——常驻后台应用的升级方式