系统托盘
本教程共 48 篇 · 第 37 篇 · 更新于 2026-08-09 · 约 7 分钟阅读
本节目标:读完你能在 Tauri 2 应用里创建系统托盘(system tray)图标,给它挂菜单、绑点击事件,并知道怎么用 Rust 端的
TrayIconBuilder管理托盘生命周期。
很多桌面应用关掉窗口后并没有真的退出,而是缩到屏幕角落的小图标里——这就是系统托盘。比如微信、网易云音乐,点叉号后托盘里还亮着图标,右键能弹出菜单。Tauri 2 内置了完整的托盘 API,不需要装额外插件,只需要开一个 feature flag。
开启 tray-icon 特性
Tauri 2 把托盘功能放在 tray-icon 特性开关后面,默认不开启。你需要在 Cargo.toml 里手动加上:
# src-tauri/Cargo.toml
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
加完后 cargo build 一下,编译能过就说明特性生效了。这一步是前提——不开这个 feature,后面所有托盘 API 都找不到。
NoteTauri 1.x 时代,托盘功能叫
system-tray,配置写在tauri.conf.json里。2.x 改成了tray-icon,并且 API 从配置文件搬到了代码里——你用TrayIconBuilder在 Rust 中创建托盘,更灵活也更符合 Rust 的习惯。
用 TrayIconBuilder 创建托盘
Tauri 2 的托盘创建方式是 Rust 端的 TrayIconBuilder,一般在 setup 钩子里调用。最小可运行示例:
// src-tauri/src/lib.rs
use tauri::tray::TrayIconBuilder;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let _tray = TrayIconBuilder::new()
.tooltip("我的应用")
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("运行 Tauri 应用时出错");
}
跑起来后,托盘区域会出现一个图标。但这里有个问题:我们没设图标,它可能是个空白。设置图标最简单的方式是复用应用的默认窗口图标:
use tauri::tray::TrayIconBuilder;
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.tooltip("我的应用")
.build(app)?;
app.default_window_icon() 返回的是 tauri.conf.json 里 icon 字段指定的那张图。你不用额外准备图片,直接拿来用就行。
Tip如果你想用自定义图标,可以用
IconKit或tauri::image::Image从文件路径加载。但大多数场景下,复用应用图标就够用了。
给托盘挂菜单
光有个图标不够,托盘的精髓在于右键弹出菜单。Tauri 2 的菜单 API 和窗口菜单是同一套,用 Menu::with_items 构建:
use tauri::{
menu::{Menu, MenuItem},
tray::TrayIconBuilder,
};
let quit_i = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&quit_i])?;
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.build(app)?;
MenuItem::with_id 的参数依次是:app 引用、菜单项 ID、显示文本、是否启用、可选快捷键。Menu::with_items 把多个菜单项打包成一个菜单,然后 .menu(&menu) 挂到托盘上。
默认情况下,左键和右键点击托盘都会弹出菜单。如果你只想右键弹菜单、左键做别的事(比如切窗口显隐),可以关掉左键弹菜单:
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.show_menu_on_left_click(false)
.build(app)?;
监听菜单点击事件
菜单挂上去后,还得知道用户点了哪一项。用 on_menu_event 绑定回调:
use tauri::tray::TrayIconBuilder;
TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.on_menu_event(|app, event| match event.id.as_ref() {
"quit" => {
println!("用户点了退出");
app.exit(0);
}
_ => {
println!("未处理的菜单项: {:?}", event.id);
}
})
.build(app)?;
event.id 就是你在 MenuItem::with_id 时设的那个字符串 ID。用 match 分发到不同的处理逻辑就行,写法很直白。
监听托盘鼠标事件
除了菜单点击,托盘图标本身也能响应鼠标事件:单击、双击、鼠标进入、移动、离开。用 on_tray_icon_event 绑定:
use tauri::{
Manager,
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
};
TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.on_tray_icon_event(|tray, event| match event {
TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} => {
println!("左键单击(松开)");
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
TrayIconEvent::DoubleClick {
button: MouseButton::Left,
..
} => {
println!("左键双击");
}
_ => {}
})
.build(app)?;
上面的例子实现了一个常见交互:左键点击托盘图标,把最小化或隐藏的主窗口恢复出来。TrayIconEvent 是个枚举,Click 带了按键和按下/松开状态,DoubleClick 带了按键,Enter/Move/Leave 带了坐标位置。
Note根据社区反馈,Linux 对托盘鼠标事件的支持有限。
Enter、Move、Leave事件在 Linux 上不触发,但Click和DoubleClick正常工作。如果你的应用要跨平台,别把关键逻辑绑定在 hover 类事件上。
前端 JS 创建托盘
除了 Rust 端,前端 JS 也能创建托盘。先装 JS 包:
npm install @tauri-apps/api
然后在前端代码里:
import { TrayIcon, Menu } from "@tauri-apps/api";
const menu = await Menu.new({
items: [
{ id: "quit", text: "退出" },
],
});
const tray = await TrayIcon.new({
tooltip: "我的应用",
menu,
menuOnLeftClick: false,
action: (event) => {
if (event.type === "Click") {
console.log("托盘被点击了");
}
},
});
JS API 和 Rust API 功能上是对等的,选择哪种取决于你的架构偏好。如果托盘逻辑和窗口管理强绑定(比如点托盘切窗口显隐),放 Rust 端更自然;如果托盘菜单内容是动态的、由前端数据驱动,放 JS 端更方便。
Tip实际开发中推荐 Rust 端创建托盘。原因有二:一是托盘通常在应用启动时就存在,放在
setup里最合适;二是退出应用的app.exit(0)只能在 Rust 端调用,JS 端只能发 IPC 命令绕一圈。
纯托盘应用:隐藏任务栏图标
有些应用不需要窗口,只活在托盘里(比如后台监控工具)。做法是把 tauri.conf.json 里的 windows 数组留空:
{
"app": {
"windows": []
}
}
这样应用启动后没有窗口,只有托盘图标。但在 Windows 和 Linux 上,任务栏可能还会显示一个图标。要隐藏它,在创建窗口时加 skip_taskbar(true):
use tauri::WebviewWindowBuilder;
// 需要弹窗口时再创建,并跳过任务栏
let window = WebviewWindowBuilder::new(
app,
"popup",
tauri::WebviewUrl::App("index.html".into()),
)
.inner_size(400.0, 600.0)
.decorations(false)
.skip_taskbar(true)
.always_on_top(true)
.build()?;
macOS 上隐藏 Dock 图标的方式不同,需要设置激活策略:
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
Accessory 策略让应用不出现在 Dock 里,但托盘图标照常显示。
托盘的权限配置
托盘功能虽然是 Tauri 核心自带的,但前端 JS 要操作托盘,仍然需要权限。在 src-tauri/capabilities/default.json 中确保包含 core:tray:default:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"windows": ["main"],
"permissions": [
"core:default",
"core:tray:default"
]
}
如果你只用 Rust 端创建托盘、前端不碰托盘 API,这个权限可以不加。但只要前端 JS 要调用 TrayIcon.new() 或相关方法,就必须授权。
小结
Tauri 2 的系统托盘功能通过 tray-icon 特性开启,核心 API 是 Rust 端的 TrayIconBuilder:用它创建图标、设置 tooltip、挂菜单、绑定事件。菜单用 Menu::with_items 构建,通过 on_menu_event 监听点击。托盘自身的鼠标事件(单击、双击、hover)用 on_tray_icon_event 监听。前端 JS 也有对等的 TrayIcon / Menu API,但实际开发中推荐 Rust 端管理托盘生命周期。纯托盘应用可以清空 windows 数组并配合 skip_taskbar 或 ActivationPolicy::Accessory 隐藏任务栏/Dock 图标。