首页 / Tauri 2 入门教程 / 系统托盘

Tauri 2 入门教程

系统托盘

本教程共 48 篇 · 第 37 篇 · 更新于 2026-08-09 · 约 7 分钟阅读

TauriTauri 2 入门教程系统托盘system trayTrayIconBuilder托盘菜单托盘事件

本节目标:读完你能在 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 都找不到。

Note

Tauri 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.jsonicon 字段指定的那张图。你不用额外准备图片,直接拿来用就行。

Tip

如果你想用自定义图标,可以用 IconKittauri::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 对托盘鼠标事件的支持有限。EnterMoveLeave 事件在 Linux 上不触发,但 ClickDoubleClick 正常工作。如果你的应用要跨平台,别把关键逻辑绑定在 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_taskbarActivationPolicy::Accessory 隐藏任务栏/Dock 图标。