第 14 章 窗口、菜单与系统托盘
本章目标
- 动态创建、关闭、定位、隐藏窗口。
- 做一个跨平台的「最小化到托盘」行为。
- 搭建原生菜单(macOS 应用菜单、Windows/Linux 右键菜单)。
一、窗口 API
从 config 创建(静态)
tauri.conf.json:
"app": {
"windows": [
{
"label": "main",
"title": "CloudTone",
"width": 1200, "height": 780, "minWidth": 960, "minHeight": 600,
"center": true, "decorations": true, "resizable": true,
"titleBarStyle": "Overlay"
}
]
}
label 是窗口唯一 ID,代码里通过它拿到窗口实例。
运行时创建(动态)
use tauri::{WebviewUrl, WebviewWindowBuilder};
fn open_mini_player(app: &tauri::AppHandle) -> tauri::Result<()> {
WebviewWindowBuilder::new(app, "mini", WebviewUrl::App("mini.html".into()))
.title("CloudTone Mini")
.inner_size(340.0, 140.0)
.always_on_top(true)
.decorations(false)
.skip_taskbar(true)
.transparent(true)
.build()?;
Ok(())
}
常用选项:
inner_size/min_inner_size/max_inner_sizeposition(x, y)/center()decorations(false)去掉系统边框transparent(true)透明窗always_on_top(true)置顶skip_taskbar(true)不在任务栏显示visible(false)创建时先不显示
获取已有窗口
use tauri::Manager;
let main = app.get_webview_window("main").unwrap();
main.hide()?;
main.show()?;
main.set_focus()?;
main.set_title("新标题")?;
main.set_size(tauri::PhysicalSize::new(800, 600))?;
前端调用:
import { getCurrentWindow } from "@tauri-apps/api/window";
const win = getCurrentWindow();
await win.setTitle("Hello");
await win.minimize();
拖拽区域(Drag Region)
无边框窗口需要你自己指定哪里可以拖:
<div data-tauri-drag-region className="h-10 bg-zinc-900 flex items-center px-3">
CloudTone
</div>
CSS 层面可用 app-region: drag;,但 Tauri 推荐用 data-tauri-drag-region 属性,兼容好。
二、窗口事件
use tauri::WindowEvent;
window.on_window_event(move |ev| match ev {
WindowEvent::CloseRequested { api, .. } => {
api.prevent_close();
window_clone.hide().ok(); // 改为隐藏
}
WindowEvent::Focused(true) => { /* ... */ }
_ => {}
});
CloudTone 的 "关闭即隐藏到托盘" 就靠 prevent_close + 托盘菜单呼出。
前端:
import { getCurrentWindow } from "@tauri-apps/api/window";
const unlisten = await getCurrentWindow().onResized(e => console.log(e.payload));
三、系统托盘(System Tray)
Tauri 2.x 托盘是一等公民。示例:
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem, MenuBuilder},
tray::{TrayIconBuilder, TrayIconEvent, MouseButton, MouseButtonState},
Manager,
};
fn setup_tray(app: &tauri::AppHandle) -> tauri::Result<()> {
let show = MenuItem::with_id(app, "show", "显示主界面", true, None::<&str>)?;
let play = MenuItem::with_id(app, "play", "播放/暂停", true, Some("Space"))?;
let next = MenuItem::with_id(app, "next", "下一首", true, None::<&str>)?;
let prev = MenuItem::with_id(app, "prev", "上一首", true, None::<&str>)?;
let quit = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
let sep = PredefinedMenuItem::separator(app)?;
let menu = Menu::with_items(app, &[&show, &sep, &prev, &play, &next, &sep, &quit])?;
TrayIconBuilder::with_id("main-tray")
.icon(app.default_window_icon().cloned().unwrap())
.menu(&menu)
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => { let _ = app.get_webview_window("main").unwrap().show(); }
"play" => { app.emit("player:toggle", ()).ok(); }
"next" => { app.emit("player:next", ()).ok(); }
"prev" => { app.emit("player:prev", ()).ok(); }
"quit" => { app.exit(0); }
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event {
let app = tray.app_handle();
if let Some(w) = app.get_webview_window("main") {
let _ = w.show(); let _ = w.set_focus();
}
}
})
.build(app)?;
Ok(())
}
在 setup 里调用:
.setup(|app| {
setup_tray(app.handle())?;
Ok(())
})
Linux 提示:托盘图标需要桌面环境支持 StatusNotifierItem。GNOME 需要装
AppIndicator扩展,KDE/Ubuntu 默认 ok。
四、窗口菜单(macOS 顶部菜单栏 / 菜单键)
use tauri::menu::{MenuBuilder, SubmenuBuilder};
fn build_menu(app: &tauri::AppHandle) -> tauri::Result<tauri::menu::Menu<tauri::Wry>> {
let app_menu = SubmenuBuilder::new(app, "CloudTone")
.about(Some(Default::default()))
.separator()
.services()
.separator()
.hide()
.hide_others()
.show_all()
.separator()
.quit()
.build()?;
let file_menu = SubmenuBuilder::new(app, "文件")
.text("new-playlist", "新建歌单")
.text("import", "导入音乐...")
.separator()
.close_window()
.build()?;
let menu = MenuBuilder::new(app).items(&[&app_menu, &file_menu]).build()?;
Ok(menu)
}
// setup 时
let menu = build_menu(app.handle())?;
app.set_menu(menu)?;
菜单事件绑定:
.on_menu_event(|app, event| {
match event.id.as_ref() {
"new-playlist" => app.emit("ui:new-playlist", ()).ok(),
"import" => app.emit("ui:import", ()).ok(),
_ => None,
};
})
五、右键上下文菜单(前端自实现)
原生 ContextMenu 目前 Tauri 只在桌面有限支持。推荐前端用 Radix/shadcn 的 ContextMenu 组件,体验更一致。除非你确实需要系统菜单(比如 macOS 的 Emoji & Symbols),那再考虑原生。
六、多窗口协作:CloudTone 的三窗口布局
main (1200x780)
mini-player (340x140, always_on_top, transparent)
lyric-overlay (全屏宽, 固定高, always_on_top, ignore_cursor_events)
lyric-overlay 有个难点:鼠标穿透。让它不挡住后面的 UI:
lyric_window.set_ignore_cursor_events(true)?;
再加个 "显示/隐藏桌面歌词" 快捷键(下一章讲)。
常见陷阱
1.
hide()后show()不显示macOS 有时需要先
set_focus()或show后等一帧。Linux 上 hide 真正关闭子渲染进程,show 会重新创建。
2. 无边框窗口在 macOS 有黑边
titleBarStyle: "Overlay"+hiddenTitle: true组合可达 native macOS 风格。
3. 托盘点击没反应
Linux 下左键可能被发行版当成弹菜单。右键弹菜单是最稳的行为。
4. 前端
getCurrentWindow()拿到了别的窗口每个窗口的
getCurrentWindow都返回自己的实例,但Window.getByLabel("main")可能跨窗口。注意心智模型。
本章小结
- 窗口 =
WebviewWindow,可以 config 也可以 runtime 创建。 - 托盘 + 菜单靠
tray/menu模块。 - CloudTone 规划三个窗口:主、迷你、桌面歌词。
动手时刻
在 hello 项目里:
- 点击关闭按钮不退出,而是隐藏到托盘。
- 托盘右键菜单「显示主界面」「退出」。
-
新建一个无边框、置顶、透明的
mini窗口。
下一章,全局快捷键与单实例。