应用菜单
本教程共 48 篇 · 第 29 篇 · 更新于 2026-08-09 · 约 7 分钟阅读
本节目标:读完你能用 Rust 给桌面应用搭出窗口菜单栏、右键菜单,并给菜单项绑定像
CmdOrCtrl+Q这样的快捷键。
你用过的桌面软件几乎都有顶部的菜单栏:文件、编辑、视图……点右键还会弹出「复制、粘贴、删除」一类的快捷菜单。这些不是网页里写的 <div>,而是操作系统提供的原生菜单(native menu)。Tauri 让你直接用 Rust 把这些菜单建出来,外观和行为完全跟着系统走,用户看起来就和本机软件一模一样。
菜单只在桌面平台(Windows、macOS、Linux)可用,移动端没有这套概念。
菜单到底是什么:原生控件,不是网页
网页里想做菜单,通常是用 HTML 拼一个浮层,再用 CSS 调样式。问题是它永远带着「网页感」:字体、间距、深色模式、快捷键提示都和系统的不统一,跨平台还会各长各的。
Tauri 的做法相反:它直接调用各系统的菜单接口来画菜单。你在 Rust 里描述「要哪些项、怎么分组、点了干什么」,剩下的渲染交给操作系统。这也是为什么用 Tauri 做出来的应用,菜单长得和系统自带应用几乎分不出来。
代价是:菜单得在核心进程(Rust)里构建,不能在前端 JS 里随手写。后文会给你完整的 Rust 示例。
菜单三件套:Menu、Submenu、MenuItem
Tauri 的菜单由三层对象拼出来,理解这三个词是看懂所有代码的前提:
- Menu(菜单):最外层的容器,是一个菜单的「根」。你把它挂到应用或窗口上,它就变成了你能看到的菜单栏。
- Submenu(子菜单):挂在 Menu 下的一组分组的入口,比如「文件(File)」「编辑(Edit)」。点开会展开一组选项。
- MenuItem(菜单项):真正能被点的那一行,比如「打开」「保存」「退出」。
除了普通 MenuItem,还有几种特殊的项:
- CheckMenuItem(勾选项):左边带一个勾,表示开/关两种状态,比如「显示侧边栏」。
- IconMenuItem(图标项):文字左边带一个小图标。
- PredefinedMenuItem(预定义项):系统已经定义好行为的项,比如「复制」「撤销」「退出」,连功能都帮你实现好了。
- Separator(分隔线):一条横线,纯粹用来把菜单项分组,不可点击。
把这几样东西按「Menu 包 Submenu,Submenu 包 MenuItem」层层套起来,就是完整的菜单。
用 Rust 搭一个基础窗口菜单
最简单的做法是用 MenuBuilder。下面这段代码在程序启动时(setup 阶段)建了一个包含两个项的菜单,并挂到整个应用上:
// src-tauri/src/lib.rs
use tauri::menu::MenuBuilder;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
// 用 MenuBuilder 逐条添加菜单项
let menu = MenuBuilder::new(app)
.text("open", "打开")
.text("close", "关闭")
.separator() // 加一条分隔线
.text("quit", "退出")
.build()?;
// 把菜单挂到应用,所有没单独设菜单的窗口都会用这一套
app.set_menu(menu)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("运行 Tauri 应用时出错");
}
.text(id, text) 里的第一个参数是这个菜单项的标识(id),后面监听点击事件时靠它来区分用户点了哪一项;第二个参数是显示在界面上的文字。.separator() 加一条分隔线。最后 build() 把零散的项组装成真正的 Menu,再 app.set_menu(menu) 挂上去。
如果某个窗口在创建时没指定自己的菜单,就会自动采用这里设置的这一套。
多级菜单:用 SubmenuBuilder 分组
真实软件的菜单是分组的:点「文件」展开「新建、打开、保存」,点「编辑」展开「撤销、复制、粘贴」。这要用 SubmenuBuilder 先各自建好子菜单,再塞进 MenuBuilder:
// src-tauri/src/lib.rs
use tauri::menu::{MenuBuilder, SubmenuBuilder};
// 在 .setup(|app| { ... }) 内:
let file_menu = SubmenuBuilder::new(app, "文件")
.text("new", "新建")
.text("open", "打开...")
.text("save", "保存")
.build()?;
let edit_menu = SubmenuBuilder::new(app, "编辑")
.undo()
.redo()
.separator()
.cut()
.copy()
.paste()
.build()?;
let menu = MenuBuilder::new(app)
.items(&[&file_menu, &edit_menu])
.build()?;
app.set_menu(menu)?;
注意 SubmenuBuilder::new(app, "文件") 的第二个参数是子菜单显示的标题。edit_menu 里用了 .undo()、.copy() 这种简写,它们是预定义项的快捷方法,等于单独创建一个「撤销」「复制」菜单项。最后用 MenuBuilder::items(&[...]) 把两个子菜单装进去。
监听菜单点击事件
菜单项自己不会干活,点了之后干什么,得你在 Rust 里用 on_menu_event 监听。它会在任意菜单项被点击时触发,给你一个事件对象,你从中取出 id 来判断点了哪一项:
// 接在 app.set_menu(menu)?; 之后
app.on_menu_event(|app, event| {
match event.id().as_ref() {
"new" => println!("点了新建"),
"open" => println!("点了打开"),
"save" => println!("点了保存"),
"quit" => app.exit(0), // 退出整个应用
_ => println!("其它菜单项"),
}
});
event.id() 返回的是菜单项的标识。quit 这一分支里调用 app.exit(0) 直接退出程序,这是「退出」项最常见的处理。
Tip想在点击菜单项后让前端页面跳转?可以在
on_menu_event里用app.emit("菜单事件名", 数据)向前端发事件,前端用listen接收后再操作路由。菜单只负责「通知」,具体业务逻辑放前端更灵活。
右键上下文菜单
除了顶部菜单栏,你还可以在用户右键点击窗口时弹出菜单。这种「上下文菜单」用 popup_menu 显示,而不是 set_menu。常见做法是在一个命令里构建菜单,再弹在窗口当前位置:
// src-tauri/src/lib.rs
use tauri::menu::{MenuBuilder, MenuItem};
use tauri::Manager;
#[tauri::command]
fn show_context_menu(window: tauri::WebviewWindow) {
let menu = MenuBuilder::new(window.manager())
.item(&MenuItem::with_id(
window.manager(),
"copy",
"复制",
true,
Some("CmdOrCtrl+C"),
).unwrap())
.item(&MenuItem::with_id(
window.manager(),
"paste",
"粘贴",
true,
Some("CmdOrCtrl+V"),
).unwrap())
.build()
.unwrap();
// 在窗口内弹出这个菜单(跟随鼠标位置)
window.popup_menu(&menu).unwrap();
}
MenuItem::with_id(app, id, text, enabled, accelerator) 一次把「标识、文字、是否可用、快捷键」都给定了。前端在元素上监听 contextmenu 事件,调用这个命令即可:
import { invoke } from "@tauri-apps/api/core";
document.addEventListener("contextmenu", async (e) => {
e.preventDefault(); // 挡掉浏览器自带的右键菜单
await invoke("show_context_menu");
});
快捷键绑定:accelerator
菜单项可以带一个快捷键(accelerator),用户不用点菜单,直接按组合键就能触发同一项。写法就是一串字符串,比如 "CmdOrCtrl+Q"、"CmdOrCtrl+S":
use tauri::menu::MenuItemBuilder;
// 在 .setup(|app| { ... }) 内构建带快捷键的项
let quit = MenuItemBuilder::with_id("quit", "退出")
.accelerator("CmdOrCtrl+Q")
.build(app)?;
let save = MenuItemBuilder::with_id("save", "保存")
.accelerator("CmdOrCtrl+S")
.build(app)?;
CmdOrCtrl 是 Tauri 的贴心写法:在 macOS 上自动变成 Cmd(⌘),在 Windows 和 Linux 上自动变成 Ctrl。你不用为不同系统写两套。其它常见组合还有 Ctrl+Shift+S、Alt+F4 等。
加了快捷键后,菜单项右边会自动显示对应的按键提示,和系统原生应用一致。
Warning快捷键不要和系统、其它应用的保留组合冲突,比如 macOS 上
Cmd+Q是退出的标准键,Cmd+W是关窗口。照搬这些标准键最不容易让用户困惑;自己发明怪异组合反而显得不专业。
别忘了 capabilities 里的 core:menu 权限
菜单属于 Tauri 的核心能力,不是装个插件就自动能用。它受 capability 权限控制,必须在 src-tauri/capabilities 里声明 core:menu:default,否则菜单相关 API 在运行时会被拦下:
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "主窗口的权限",
"windows": ["main"],
"permissions": [
"core:default",
"core:menu:default"
]
}
core:menu:default 已经涵盖了创建菜单、绑定事件、弹上下文菜单所需的基础权限。如果你还用了系统托盘菜单,则另外需要 core:tray:default。
小结
Tauri 的菜单分三层:Menu 是根容器,Submenu 负责分组(文件、编辑),MenuItem 才是能被点的那一行。用 MenuBuilder / SubmenuBuilder 在 Rust 的 setup 阶段组装,再用 app.set_menu 挂成菜单栏、用 window.popup_menu 弹成右键菜单。点击行为靠 on_menu_event 按 id 分发,快捷键用 accelerator("CmdOrCtrl+Q") 声明,macOS 与 Win/Linux 自动切换。最后记得在 capability 里加 core:menu:default,菜单才能真正跑起来。