6 窗口菜单

原文链接: https://tauri.app/learn/window-menu/

原生应用菜单既可以附加到窗口,也可以附加到系统托盘。仅桌面端可用。

创建基础菜单

要创建一个基础的原生窗口菜单并把它附加到窗口,你可以创建各种类型的菜单项,包括基础项、勾选项和分隔线:

语言

使用 Menu.new 静态函数创建窗口菜单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { Menu } from '@tauri-apps/api/menu';

const menu = await Menu.new({
  items: [
    {
      id: 'quit',
      text: 'Quit',
      action: () => {
        console.log('quit pressed');
      },
    },
    {
      id: 'check_item',
      text: 'Check Item',
      checked: true,
    },
    {
      item: 'Separator',
    },
    {
      id: 'disabled_item',
      text: 'Disabled Item',
      enabled: false,
    },
    {
      id: 'status',
      text: 'Status: Processing...',
    },
  ],
});

// 如果某个窗口在创建时没有指定菜单,或从未显式设置过菜单,
// 该菜单会被分配给它。
menu.setAsAppMenu().then(async (res) => {
  console.log('menu set success', res);

  // 更新单个菜单项的文本
  const statusItem = await menu.get('status');
  if (statusItem) {
    await statusItem.setText('Status: Ready');
  }
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let menu = MenuBuilder::new(app)
                .text("open", "Open")
                .text("close", "Close")
                .check("check_item", "Check Item")
                .separator()
                .text("disabled_item", "Disabled Item")
                .text("status", "Status: Processing...")
                .build()?;

            app.set_menu(menu.clone())?;

            // 更新单个菜单项的文本
            menu
                .get("status")
                .unwrap()
                .as_menuitem_unchecked()
                .set_text("Status: Ready")?;

            Ok(())
        })
        .run(tauri::generate_context!());
}

监听自定义菜单项上的事件

每个自定义菜单项在被点击时都会触发一个事件。使用 on_menu_event API 处理它们。

语言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Menu } from '@tauri-apps/api/menu';

const menu = await Menu.new({
  items: [
    {
      id: 'Open',
      text: 'open',
      action: () => {
        console.log('open pressed');
      },
    },
    {
      id: 'Close',
      text: 'close',
      action: () => {
        console.log('close pressed');
      },
    },
  ],
});

await menu.setAsAppMenu();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use tauri::menu::{MenuBuilder};

fn main() {
  tauri::Builder::default()
        .setup(|app| {
            let menu = MenuBuilder::new(app)
                .text("open", "Open")
                .text("close", "Close")
                .build()?;

            app.set_menu(menu)?;

            app.on_menu_event(move |app_handle: &tauri::AppHandle, event| {

                println!("menu event: {:?}", event.id());

                match event.id().0.as_str() {
                    "open" => {
                        println!("open event");
                    }
                    "close" => {
                        println!("close event");
                    }
                    _ => {
                        println!("unexpected menu event");
                    }
                }
            });

            Ok(())
        })
}

创建多级菜单

多级菜单让你可以把菜单项归组到 “File”“Edit” 之类的分类下。在 Windows 或 Linux 上,它们会作为应用窗口的一部分出现;在 macOS 上则出现在菜单栏中。

注意: 在 macOS 上使用子菜单时,所有项都必须归组到某个子菜单下。顶层项会被忽略。此外,第一个子菜单默认会被放在应用的“关于”菜单之下,与它的 text 标签无关。你应当把某个子菜单作为第一项(比如一个 “About” 子菜单)来填充这个位置。

语言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { Menu, MenuItem, Submenu } from '@tauri-apps/api/menu';

// 在 macOS 上会成为应用的子菜单
const aboutSubmenu = await Submenu.new({
  text: 'About',
  items: [
    await MenuItem.new({
      id: 'quit',
      text: 'Quit',
      action: () => {
        console.log('Quit pressed');
      },
    }),
  ],
});

const fileSubmenu = await Submenu.new({
  text: 'File',
  icon: 'folder', // 可选:为子菜单添加图标
  items: [
    await MenuItem.new({
      id: 'new',
      text: 'New',
      action: () => {
        console.log('New clicked');
      },
    }),
    await MenuItem.new({
      id: 'open',
      text: 'Open',
      action: () => {
        console.log('Open clicked');
      },
    }),
    await MenuItem.new({
      id: 'save_as',
      text: 'Save As...',
      action: () => {
        console.log('Save As clicked');
      },
    }),
  ],
});

const editSubmenu = await Submenu.new({
  text: 'Edit',
  items: [
    await MenuItem.new({
      id: 'undo',
      text: 'Undo',
      action: () => {
        console.log('Undo clicked');
      },
    }),
    await MenuItem.new({
      id: 'redo',
      text: 'Redo',
      action: () => {
        console.log('Redo clicked');
      },
    }),
  ],
});

const menu = await Menu.new({
  items: [aboutSubmenu, fileSubmenu, editSubmenu],
});

menu.setAsAppMenu();

// 你也可以动态更新子菜单图标
fileSubmenu.setIcon('document');
// 或者设置原生图标(每个平台只有一种类型生效)
fileSubmenu.setNativeIcon('NSFolder');
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
use tauri::{
    image::Image,
    menu::{CheckMenuItemBuilder, IconMenuItemBuilder, MenuBuilder, SubmenuBuilder},
};

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let menu_image = Image::from_bytes(include_bytes!("../icons/menu.png")).unwrap();
            let file_menu = SubmenuBuilder::new(app, "File")
                .submenu_icon(menu_image) // 可选:为子菜单添加图标
                .text("open", "Open")
                .text("quit", "Quit")
                .build()?;

            let lang_str = "en";
            let check_sub_item_1 = CheckMenuItemBuilder::new("English")
                .id("en")
                .checked(lang_str == "en")
                .build(app)?;

            let check_sub_item_2 = CheckMenuItemBuilder::new("Chinese")
                .id("zh")
                .checked(lang_str == "zh")
                .enabled(false)
                .build(app)?;

            // 从路径加载图标
            let icon_image = Image::from_bytes(include_bytes!("../icons/icon.png")).unwrap();

            let icon_item = IconMenuItemBuilder::new("icon")
                .icon(icon_image)
                .build(app)?;

            let other_item = SubmenuBuilder::new(app, "language")
                .item(&check_sub_item_1)
                .item(&check_sub_item_2)
                .build()?;

            let menu = MenuBuilder::new(app)
                .items(&[&file_menu, &other_item, &icon_item])
                .build()?;

            app.set_menu(menu)?;

            let menu_image_update =
                Image::from_bytes(include_bytes!("../icons/menu_update.png")).unwrap();
            // 你也可以动态更新子菜单图标
            file_menu.set_icon(Some(menu_image_update))?;
            // 或者设置原生图标(每个平台只有一种类型生效)
            file_menu.set_native_icon(Some(tauri::menu::NativeIcon::Folder))?;

            Ok(())
        })
        .run(tauri::generate_context!());
}

注意你需要启用 image-ico 或 image-png 特性才能使用这个 API:

1
tauri = { version = "...", features = ["...", "image-png"] }

创建预定义菜单

要使用由操作系统或 Tauri 预定义行为的内置(原生)菜单项:

语言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { Menu, PredefinedMenuItem } from '@tauri-apps/api/menu';

const copy = await PredefinedMenuItem.new({
  text: 'copy-text',
  item: 'Copy',
});

const separator = await PredefinedMenuItem.new({
  text: 'separator-text',
  item: 'Separator',
});

const undo = await PredefinedMenuItem.new({
  text: 'undo-text',
  item: 'Undo',
});

const redo = await PredefinedMenuItem.new({
  text: 'redo-text',
  item: 'Redo',
});

const cut = await PredefinedMenuItem.new({
  text: 'cut-text',
  item: 'Cut',
});

const paste = await PredefinedMenuItem.new({
  text: 'paste-text',
  item: 'Paste',
});

const select_all = await PredefinedMenuItem.new({
  text: 'select_all-text',
  item: 'SelectAll',
});

const menu = await Menu.new({
  items: [copy, separator, undo, redo, cut, paste, select_all],
});

await menu.setAsAppMenu();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use tauri::menu::{MenuBuilder, PredefinedMenuItem};

fn main() {
  tauri::Builder::default()
        .setup(|app| {
      let menu = MenuBuilder::new(app)
                .copy()
                .separator()
                .undo()
                .redo()
                .cut()
                .paste()
                .select_all()
                .item(&PredefinedMenuItem::copy(app, Some("custom text"))?)
                .build()?;
            app.set_menu(menu)?;

            Ok(())
        })
}

关于更多预置能力,请参阅 PredefinedMenuItem 文档。

修改菜单状态

如果你想修改菜单的状态,例如文本、图标或勾选状态,可以再次调用 set_menu:

语言

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import {
  Menu,
  CheckMenuItem,
  IconMenuItem,
  MenuItem,
} from '@tauri-apps/api/menu';
import { Image } from '@tauri-apps/api/image';

let currentLanguage = 'en';

const check_sub_item_en = await CheckMenuItem.new({
  id: 'en',
  text: 'English',
  checked: currentLanguage === 'en',
  action: () => {
    currentLanguage = 'en';
    check_sub_item_en.setChecked(currentLanguage === 'en');
    check_sub_item_zh.setChecked(currentLanguage === 'cn');
    console.log('English pressed');
  },
});

const check_sub_item_zh = await CheckMenuItem.new({
  id: 'zh',
  text: 'Chinese',
  checked: currentLanguage === 'zh',
  action: () => {
    currentLanguage = 'zh';
    check_sub_item_en.setChecked(currentLanguage === 'en');
    check_sub_item_zh.setChecked(currentLanguage === 'zh');
    check_sub_item_zh.setAccelerator('Ctrl+L');
    console.log('Chinese pressed');
  },
});

// 从路径加载图标
const icon = await Image.fromPath('../src/icon.png');
const icon2 = await Image.fromPath('../src/icon-2.png');

const icon_item = await IconMenuItem.new({
  id: 'icon_item',
  text: 'Icon Item',
  icon: icon,
  action: () => {
    icon_item.setIcon(icon2);
    console.log('icon pressed');
  },
});

const text_item = await MenuItem.new({
  id: 'text_item',
  text: 'Text Item',
  action: () => {
    text_item.setText('Text Item Changed');
    console.log('text pressed');
  },
});

const menu = await Menu.new({
  items: [
    {
      id: 'change menu',
      text: 'change_menu',
      items: [text_item, check_sub_item_en, check_sub_item_zh, icon_item],
    },
  ],
});

await menu.setAsAppMenu();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use tauri::{
    image::Image,
    menu::{CheckMenuItemBuilder, IconMenuItem, MenuBuilder, MenuItem, SubmenuBuilder},
};

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let check_sub_item_en = CheckMenuItemBuilder::with_id("en", "EN")
                .checked(true)
                .build(app)?;

            let check_sub_item_zh = CheckMenuItemBuilder::with_id("zh", "ZH")
                .checked(false)
                .build(app)?;

            let text_menu = MenuItem::with_id(
                app,
                "change_text",
                &"Change menu".to_string(),
                true,
                Some("Ctrl+Z"),
            )
            .unwrap();

            let icon_menu = IconMenuItem::with_id(
                app,
                "change_icon",
                &"Change icon menu",
                true,
                Some(Image::from_bytes(include_bytes!("../icons/icon.png")).unwrap()),
                Some("Ctrl+F"),
            )
            .unwrap();

            let menu_item = SubmenuBuilder::new(app, "Change menu")
                .item(&text_menu)
                .item(&icon_menu)
                .items(&[&check_sub_item_en, &check_sub_item_zh])
                .build()?;
            let menu = MenuBuilder::new(app).items(&[&menu_item]).build()?;
            app.set_menu(menu)?;
            app.on_menu_event(move |_app_handle: &tauri::AppHandle, event| {
                match event.id().0.as_str() {
                    "change_text" => {
                        text_menu
                            .set_text("changed menu text")
                            .expect("Change text error");

                        text_menu
                            .set_text("changed menu text")
                            .expect("Change text error");
                    }
                    "change_icon" => {
                        icon_menu
                            .set_text("changed menu-icon text")
                            .expect("Change text error");
                        icon_menu
                            .set_icon(Some(
                                Image::from_bytes(include_bytes!("../icons/icon-2.png")).unwrap(),
                            ))
                            .expect("Change icon error");
                    }

                    "en" | "zh" => {
                        check_sub_item_en
                            .set_checked(event.id().0.as_str() == "en")
                            .expect("Change check error");
                        check_sub_item_zh
                            .set_checked(event.id().0.as_str() == "zh")
                            .expect("Change check error");
                        check_sub_item_zh.set_accelerator(Some("Ctrl+L"))
                        .expect("Change accelerator error");
                    }
                    _ => {
                        println!("unexpected menu event");
                    }
                }
            });

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
最后修改 September 25, 2026: 更新 (4c0ee2db0)