同じアプリでも、メニューの並べ方は OS ごとに慣習が違います。macOS ではメニューバーが画面上部に 1 つだけで、先頭のサブメニューは名前に関係なくアプリ名のメニューになり、「このアプリについて」「設定」「終了」はそこに置きます。Windows と Linux ではメニューバーがウィンドウごとに付き、「ファイル」から始めて「終了」はその末尾、「バージョン情報」は「ヘルプ」に置くのが普通です。ここでは OS の判定方法と、項目を OS ごとに振り分ける組み立て方を説明します。メニュー自体の作り方は menu-001、標準項目の一覧は menu-007 を参照してください。
前提条件
Rust で組み立てるなら、OS はビルド時に決まるのでプラグインは不要です。JS で組み立てる場合は、OS の判定に os プラグインを使います。
npm run tauri add os
このコマンドで lib.rs に .plugin(tauri_plugin_os::init()) が追加されます。capability に os:default が無ければ足します(platform() の権限 os:allow-platform はこれに含まれます)。メニューの権限は core:default に含まれています。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"os:default"
]
}
主な項目の置き場所は次のとおりです。Tauri の既定のメニュー(Menu.default())も、「このアプリについて」を macOS ではアプリ名のメニュー、Windows と Linux では「Help」に入れています。
| 項目 | macOS | Windows / Linux |
|---|---|---|
| このアプリについて | アプリ名のメニューの先頭 | 「ヘルプ」の末尾 |
| 設定 | アプリ名のメニュー(Cmd+,) | 「ファイル」や「ツール」 |
| 終了 | アプリ名のメニューの末尾(Cmd+Q) | 「ファイル」の末尾(Linux は標準項目の Quit が使えない) |
| 「ウィンドウ」メニュー | 置く(開いているウィンドウの一覧を OS が足す) | 置かないことが多い |
アクセスキー (&F) | 付けない(macOS のメニューにはアクセスキーが無い) | 付ける |
1. フロントエンドから実装する (TypeScript)
os プラグインの platform() は、await なしで 'macos'・'windows'・'linux' などを返します。値はビルド時に決まっていて、呼ぶたびに問い合わせることはありません。OS ごとに別のメニューを丸ごと書くより、置き場所の変わる項目を先に 1 回だけ定義し、どこに入れるかだけを分岐させると、項目を足したときの書き忘れが減ります。
import { Menu, Submenu, type MenuItemOptions, type PredefinedMenuItemOptions } from '@tauri-apps/api/menu';
import { platform } from '@tauri-apps/plugin-os';
const os = platform(); // 'macos' | 'windows' | 'linux' など
const isMac = os === 'macos';
const APP = 'My Editor';
// macOS ではアクセスキーの「(&F)」を付けない
const label = (text: string, key: string) => (isMac ? text : `${text}(&${key})`);
const run = (id: string) => console.log('menu:', id); // 振り分けは menu-005
// OS によって置き場所が変わる項目は、先に 1 回だけ定義する
const about: PredefinedMenuItemOptions = {
item: { About: { name: APP, version: '1.0.0' } },
text: isMac ? `${APP} について` : 'バージョン情報(&A)',
};
const settings: MenuItemOptions = { id: 'app:settings', text: `${label('設定', 'O')}...`, accelerator: 'CmdOrCtrl+,', action: run };
const separator: PredefinedMenuItemOptions = { item: 'Separator' };
const fileItems: Array<MenuItemOptions | PredefinedMenuItemOptions> = [
{ id: 'file:new', text: label('新規作成', 'N'), accelerator: 'CmdOrCtrl+N', action: run },
];
if (!isMac) fileItems.push(separator, settings);
if (os === 'windows') fileItems.push(separator, { item: 'Quit', text: '終了(&X)' }); // Linux は Quit が非対応
const submenus: Submenu[] = [];
if (isMac) {
// 先頭のサブメニューは、text に関係なくアプリ名のメニューになる
submenus.push(await Submenu.new({
text: APP,
items: [about, separator, settings, separator, { item: 'Quit', text: `${APP} を終了` }],
}));
}
submenus.push(await Submenu.new({ text: label('ファイル', 'F'), items: fileItems }));
submenus.push(await Submenu.new({
text: label('編集', 'E'), // macOS でコピーなどのキーを効かせるにも必要(menu-007)
items: [
{ item: 'Cut', text: label('切り取り', 'T') },
{ item: 'Copy', text: label('コピー', 'C') },
{ item: 'Paste', text: label('貼り付け', 'P') },
],
}));
const windowMenu = isMac
? await Submenu.new({ text: 'ウィンドウ', items: [{ item: 'Minimize', text: 'しまう' }, { item: 'Maximize', text: '拡大/縮小' }] })
: null;
if (windowMenu) submenus.push(windowMenu);
const helpMenu = await Submenu.new({
text: label('ヘルプ', 'H'),
items: isMac ? [{ id: 'help:docs', text: `${APP} ヘルプ`, action: run }] : [{ id: 'help:docs', text: 'ヘルプを表示(&D)', action: run }, separator, about],
});
submenus.push(helpMenu);
await (await Menu.new({ items: submenus })).setAsAppMenu();
if (isMac) {
await windowMenu?.setAsWindowsMenuForNSApp(); // ウィンドウの一覧を OS が足す
await helpMenu.setAsHelpMenuForNSApp(); // ヘルプに検索欄が付く
}
setAsWindowsMenuForNSApp() と setAsHelpMenuForNSApp() は Windows と Linux では非対応で、呼んでも何も変わりません。
2. バックエンドから実装する (Rust)
Rust では cfg!(target_os = "macos") で分岐できます。cfg!() はどの OS でも両方の分岐をコンパイルするので書き間違いに気付きやすい一方、macOS にしかないメソッド(Submenu::set_as_windows_menu_for_nsapp() など)を分岐の中で呼ぶと、Windows でビルドが通りません。そうしたメソッドは #[cfg(target_os = "macos")] を付けたブロックで呼ぶか、下の例のようにサブメニューへ特別な id(WINDOW_SUBMENU_ID / HELP_SUBMENU_ID)を付けます。この id のサブメニューをメニューの最上位に置くと、macOS では「ウィンドウ」「ヘルプ」メニューとして扱われ、ほかの OS では普通のサブメニューになります。
use tauri::menu::{AboutMetadata, Menu, MenuBuilder, MenuItemBuilder, SubmenuBuilder, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID};
use tauri::{AppHandle, Runtime};
/// macOS ではアクセスキーの「(&F)」を付けない
fn label(text: &str, key: char) -> String {
if cfg!(target_os = "macos") {
text.to_string()
} else {
format!("{text}(&{key})")
}
}
fn os_menu<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
let mac = cfg!(target_os = "macos");
let name = app.package_info().name.clone();
let about = AboutMetadata {
name: Some(name.clone()),
version: Some(app.package_info().version.to_string()),
..Default::default()
};
// 置き場所が OS で変わる「設定」は 1 回だけ作る
let settings = MenuItemBuilder::with_id("app:settings", format!("{}...", label("設定", 'O')))
.accelerator("CmdOrCtrl+,")
.build(app)?;
let mut menu = MenuBuilder::new(app);
if mac {
let app_menu = SubmenuBuilder::new(app, &name) // 先頭なのでアプリ名のメニューになる
.about_with_text(format!("{name} について"), Some(about.clone()))
.separator()
.item(&settings)
.separator()
.quit_with_text(format!("{name} を終了"))
.build()?;
menu = menu.item(&app_menu);
}
let mut file = SubmenuBuilder::new(app, label("ファイル", 'F')).text("file:new", label("新規作成", 'N'));
if !mac {
file = file.separator().item(&settings).separator();
file = if cfg!(target_os = "linux") {
file.text("app:quit", "終了(&X)") // Linux は標準の Quit が使えないので自前の項目にする
} else {
file.quit_with_text("終了(&X)")
};
}
let edit = SubmenuBuilder::new(app, label("編集", 'E'))
.cut_with_text(label("切り取り", 'T'))
.copy_with_text(label("コピー", 'C'))
.paste_with_text(label("貼り付け", 'P'))
.build()?;
menu = menu.item(&file.build()?).item(&edit);
if mac {
let window = SubmenuBuilder::with_id(app, WINDOW_SUBMENU_ID, "ウィンドウ")
.minimize_with_text("しまう")
.maximize_with_text("拡大/縮小")
.build()?;
menu = menu.item(&window);
}
let mut help = SubmenuBuilder::with_id(app, HELP_SUBMENU_ID, label("ヘルプ", 'H')).text("help:docs", label("ヘルプを表示", 'D'));
if !mac {
help = help.separator().about_with_text("バージョン情報(&A)", Some(about));
}
menu.item(&help.build()?).build()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.menu(|app| os_menu(app))
.on_menu_event(|app, event| match event.id().as_ref() {
"app:quit" => app.exit(0), // Linux 用の自前の「終了」
id => println!("menu: {id}"),
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
動作確認
npm run tauri dev で起動します。
- Windows: タイトルバーの下に「ファイル(F)」「編集(E)」「ヘルプ(H)」が並びます。「ファイル」の末尾に「設定(O)...」と「終了(X)」、「ヘルプ」の末尾に「バージョン情報(A)」が入り、Alt+F で「ファイル」が開きます。
- macOS: 画面上部にアプリ名・「ファイル」・「編集」・「ウィンドウ」・「ヘルプ」が並びます。アプリ名のメニューに「My Editor について」「設定...」「My Editor を終了」が入り、「ウィンドウ」の末尾には開いているウィンドウの名前が、「ヘルプ」には検索欄が出ます。
- Linux: Windows と同じ並びで、Rust 版の「終了」を選ぶとアプリが終わります。
Rust 版で「設定...」を選ぶと、ターミナルに次のように出ます。
menu: app:settings
よくあるエラーと対処法
- macOS で「ファイル」の中身がアプリ名のメニューに入ってしまう: 先頭のサブメニューは名前に関係なくアプリ名のメニューになります。macOS ではアプリ用のサブメニューを先頭に足します。
- Windows のビルドで
set_as_windows_menu_for_nsappが見つからないという趣旨のエラー: macOS にしかないメソッドをcfg!()の分岐で呼んでいます。#[cfg(target_os = "macos")]のブロックに移すか、WINDOW_SUBMENU_IDを使います。 platform()で undefined のプロパティを読もうとした趣旨の TypeError: os プラグインが Rust 側に登録されていません。lib.rsに.plugin(tauri_plugin_os::init())があるか確かめます。- Linux で「終了」が機能しない: 標準項目の
Quitは Linux では非対応です。自前の項目にしてapp.exit(0)を呼びます。 - macOS の「ウィンドウ」にウィンドウの一覧が出ない:
setAsWindowsMenuForNSApp()を呼んでいないか、Rust でWINDOW_SUBMENU_IDのサブメニューを最上位ではなく別のサブメニューの中に置いています。
OS ごとの違いと注意点
- macOS: メニューはアプリで 1 つで、最上位にはサブメニューしか置けません。ウィンドウごとのメニュー(
setAsWindowMenu())は使えません。ヘルプメニューを指定しなければ、表示言語で「ヘルプ」に当たる名前のサブメニューが自動で使われます。 - Windows: 標準項目の
Undo・Redoは非対応です。「元に戻す」が必要なら自前の項目にします。 - Linux:
QuitのほかCloseWindow・Minimizeなども非対応で、切り取りなどにはlinux-libxdoが必要です(menu-007)。 - キーの慣習: Command と Ctrl の違いは
CmdOrCtrlで吸収できます(menu-004)。「やり直し」が macOS では Cmd+Shift+Z、Windows では Ctrl+Y であるような慣習の違いは、OS ごとにacceleratorを分けます。 - iOS / Android: メニューの API はありません。OS の判定だけなら OS の種類(Windows/Mac/Linux)を取得する を参照してください。
