アプリケーションのメニューバーを作る

Rust の Builder::menu() か JS の Menu.new() と setAsAppMenu() で「ファイル」「ヘルプ」などのメニューバーを作る。macOS の既定メニューやサブウィンドウへの付き方も示す。

メニュー・トレイ 対象: Tauri 2.x 更新日: 読了目安: 約9分 menu-001
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 2. バックエンドから実装する (Rust)
  4. 動作確認
  5. よくあるエラーと対処法
  6. OS ごとの違いと注意点
  7. 関連レシピ

「ファイル」「ヘルプ」のようなメニューバーは、tauri.conf.json に設定項目が無く、コードで組み立てます。起動時から決まったメニューを出すなら Rust の Builder::menu()、フロントエンドの状態に合わせて作るなら JS の Menu.new() と setAsAppMenu() を使います。何もしなければ、Windows と Linux ではメニューバーが無く、macOS では Tauri の既定のメニューが出ます。ここでは表示するところまでを扱い、選ばれたときの処理は menu-005、キー操作は menu-004 で説明します。

前提条件

プラグインは不要です。JS の Menu.new() や setAsAppMenu() の権限(core:menu:allow-new など)はすべて core:menu:default に含まれ、それが core:default に含まれるので、新規プロジェクトの capability のままで使えます。core:default を外している場合は "core:menu:default" を足します。Rust だけで作るなら権限は関係ありません。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default"
  ]
}

作り方は次のように選びます。

やりたいこと使うもの
起動した瞬間から決まったメニューを出すRust の Builder::menu()
設定などを読んでから組み立てるRust の setup で set_menu()
フロントエンドの状態に合わせて作る・作り直すJS の Menu.new() と setAsAppMenu()
特定のウィンドウだけ別のメニューにするWebviewWindowBuilder::menu() / JS の setAsWindowMenu()
メニューバーを出さない・隠すメニューバーを非表示にする

1. フロントエンドから実装する (TypeScript)

Menu.new() にオブジェクトで階層ごと渡すと、1 回で組み立てられます。text と items を持つものがサブメニュー(「ファイル」などの見出し)、text だけのものが項目、{ item: 'Separator' } が区切り線です。作っただけでは表示されず、setAsAppMenu() を呼んだ時点でアプリのメニューになります。

import { Menu } from '@tauri-apps/api/menu';
import { getCurrentWindow } from '@tauri-apps/api/window';

// どのウィンドウのページでもこのコードが動く構成でも、作るのは main だけにする
if (getCurrentWindow().label === 'main') {
  const menu = await Menu.new({
    items: [
      {
        id: 'file',
        text: 'ファイル(&F)',
        items: [
          { id: 'file:new', text: '新規作成(&N)' },
          { id: 'file:open', text: '開く(&O)...' },
          { item: 'Separator' },
          { id: 'app:quit', text: '終了(&X)' },
        ],
      },
      {
        id: 'help',
        text: 'ヘルプ(&H)',
        items: [{ id: 'help:about', text: 'バージョン情報(&A)' }],
      },
    ],
  });
  const previous = await menu.setAsAppMenu(); // 直前のアプリのメニュー(無ければ null)が返る
  console.log('previous menu:', previous?.id ?? 'none');
}

書くときのポイントは 3 つです。

  • 一番上の階層にはサブメニューだけを置く: macOS ではメニューバーの直下に項目を置けません。全 OS で同じ構成にするならサブメニューで包みます。
  • & はアクセスキーの印: & の次の文字が、キーボードでメニューを開くためのアクセスキーになります。& 自体を表示したいときは && と書きます。
  • id は自分で付ける: 省略すると自動の値になり、どの項目が選ばれたかを見分けられません。トレイや右クリックメニューと重ならないよう、file: のような接頭辞を付けると安全です。

JS はページの読み込み後に動くので、起動直後の一瞬はメニューの無い状態(macOS では既定のメニュー)が見えます。また、全ウィンドウで同じ main.ts が動く構成だと、ウィンドウを開くたびにメニューが作り直されるため、上の例では main に限っています。後から項目を変えるなら、MenuItem.new() などで部品を作って参照を持っておき、items に渡します。

Windows と Linux では、メニューを指定せずに作ったウィンドウにもアプリのメニューが付きます。設定画面などを別のメニューにするには、そのウィンドウのページで setAsWindowMenu() を呼びます。

import { Menu } from '@tauri-apps/api/menu';

// 設定ウィンドウのページで実行する(macOS は非対応)
const menu = await Menu.new({
  items: [
    {
      id: 'settings-window',
      text: 'ウィンドウ(&W)',
      items: [{ id: 'settings:close', text: '閉じる(&C)' }],
    },
  ],
});
await menu.setAsWindowMenu(); // 引数を省略すると、呼び出したウィンドウが対象

2. バックエンドから実装する (Rust)

Rust では MenuBuilder と SubmenuBuilder を使い、.text(id, 表示名) で項目、.separator() で区切り線を足します。Builder::menu() に渡した関数は設定ファイルのウィンドウが作られる前に呼ばれるので、ウィンドウは最初からメニュー付きで開きます。設定を読んでから組み立てたいなら、setup の中で app.set_menu(app_menu(app.handle())?)? と呼びます。別のメニューにしたいウィンドウには、作るときに WebviewWindowBuilder::menu() で渡します。

use tauri::menu::{Menu, MenuBuilder, SubmenuBuilder};
use tauri::{AppHandle, Runtime, WebviewUrl, WebviewWindowBuilder};

/// アプリ全体のメニュー。Builder::menu() からも set_menu() からも呼べる形にしておく
fn app_menu<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
    let file = SubmenuBuilder::with_id(app, "file", "ファイル(&F)")
        .text("file:new", "新規作成(&N)")
        .text("file:open", "開く(&O)...")
        .separator()
        .text("app:quit", "終了(&X)")
        .build()?;
    let help = SubmenuBuilder::with_id(app, "help", "ヘルプ(&H)")
        .text("help:about", "バージョン情報(&A)")
        .build()?;
    MenuBuilder::new(app).items(&[&file, &help]).build()
}

/// 設定ウィンドウを開く。アプリのメニューではなく専用のメニューを付ける
#[tauri::command]
async fn open_settings(app: AppHandle) -> Result<(), String> {
    let window_menu = SubmenuBuilder::new(&app, "ウィンドウ(&W)")
        .text("settings:close", "閉じる(&C)")
        .build()
        .map_err(|e| e.to_string())?;
    let menu = MenuBuilder::new(&app)
        .item(&window_menu)
        .build()
        .map_err(|e| e.to_string())?;
    WebviewWindowBuilder::new(&app, "settings", WebviewUrl::App("settings.html".into()))
        .title("設定")
        .menu(menu) // 指定しなければアプリのメニューが付く(Windows / Linux)
        .build()
        .map_err(|e| e.to_string())?;
    Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        // 設定ファイルのウィンドウより先に登録されるので、最初からメニュー付きで開く
        .menu(|app| app_menu(app))
        .invoke_handler(tauri::generate_handler![open_settings])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

動作確認

npm run tauri dev で起動すると、Windows と Linux ではタイトルバーの下に「ファイル(F)」「ヘルプ(H)」が並び、Windows では Alt+F で「ファイル」が開きます。macOS では画面上部のメニューバーに出ます。項目を選んでもまだ何も起きません。JS 版のコンソールには、Windows と Linux では previous menu: none、macOS では既定のメニューの id が出ます。

次のように設定ウィンドウを開くと、Windows と Linux では「ウィンドウ(W)」だけのメニューが付き、メインウィンドウのメニューはそのままです。

import { invoke } from '@tauri-apps/api/core';

await invoke('open_settings');

よくあるエラーと対処法

  • macOS でだけメニューが出ない・一部が欠ける: 一番上の階層に項目を直接置いています。サブメニューで包みます。
  • 「menu.new not allowed. Permissions associated with this command: core:menu:allow-new, core:menu:default」: capability に core:default も core:menu:default もありません(リリースビルドでは「Command plugin:menu|new not allowed by ACL」)。メイン以外のウィンドウのページで作るなら、そのラベルも windows に入れます。
  • サブウィンドウにも同じメニューバーが出る: Windows と Linux の仕様です。別のメニューを渡すか、メニューバーを非表示にする の方法で隠します。
  • 自前のメニューにしたら、macOS でコピー・貼り付けのキーが効かない: 既定のメニューを置き換えると「編集」メニューも無くなります。コピー・貼り付けなどの標準メニューを入れる で足します。
  • モバイル向けのビルドで tauri::menu が見つからないという趣旨のエラー: メニューの API はデスクトップ向けにしかありません。メニューを作るコードを #[cfg(desktop)] で囲みます。

OS ごとの違いと注意点

  • Windows / Linux: メニューバーはウィンドウごとに付き、ウィンドウ単位で変えられます。
  • macOS: メニューは画面上部に 1 つだけで、アプリ全体で共有します。setAsWindowMenu() は非対応で、呼んでも表示は変わりません。何もしなくても既定のメニュー(アプリ名・File・Edit・View・Window・Help)が出て、Rust の Builder::enable_macos_default_menu(false) で止められます。OS ごとの並べ方は Windows と macOS でメニューを出し分ける を参照してください。
  • iOS / Android: メニューバーの API はありません。
  • 項目の無効化やチェックは menu-003 と menu-006、トレイのメニューは menu-010 で扱います。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する