首页 / Tauri 2 入门教程 / 应用菜单

Tauri 2 入门教程

应用菜单

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

TauriTauri 2 入门教程应用菜单MenuSubmenu快捷键accelerator

本节目标:读完你能用 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+SAlt+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,菜单才能真正跑起来。