4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tauri v2 で macOS メニューバー常駐アプリを作る完全ガイド

4
Posted at

はじめに

macOS のメニューバーに常駐するユーティリティアプリ(Bartender、Stats、iStatMenus 的なやつ)を Tauri v2 で作る方法 を解説します。

筆者が実際にリリースしている HiyokoBar(メニューバー常駐型 HUD モニタリングアプリ)の実装をベースにしています。

この記事で作れるもの

  • メニューバーのトレイアイコンをクリックするとウィンドウが表示される
  • ウィンドウ外をクリックすると自動で閉じる(HUD 動作)
  • macOS ネイティブのすりガラス(Vibrancy)UI
  • グローバルショートカットでのトグル表示
  • ログイン時の自動起動

1. プロジェクトのセットアップ

必要な依存関係

# Cargo.toml
[dependencies]
tauri = { version = "2", features = ["macos-private-api", "tray-icon", "image-png"] }
tauri-plugin-positioner = { version = "2", features = ["tray-icon"] }
tauri-plugin-autostart = "2.5.1"
tauri-plugin-global-shortcut = "2.3.1"
window-vibrancy = "0.7.1"

macos-private-api feature は Vibrancy(すりガラスUI)に必須です。
tray-icon feature は Position::TrayCenter 配置に必要です。

tauri.conf.json

メニューバーアプリの設定で 重要なのは5つ :

{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "MyMenuBarApp",
  "version": "1.0.0",
  "identifier": "com.example.menubar",
  "app": {
    "macOSPrivateApi": true,
    "windows": [
      {
        "title": "MyMenuBarApp",
        "width": 360,
        "height": 540,
        "resizable": false,
        "decorations": false,
        "transparent": true,
        "alwaysOnTop": true,
        "visible": false,
        "skipTaskbar": true
      }
    ]
  }
}
設定 理由
macOSPrivateApi true Vibrancy に必須
visible false 起動時はウィンドウを隠す
decorations false タイトルバーを消す
transparent true 背景を透過させる
skipTaskbar true Dock に表示しない

2. コアの実装(lib.rs)

ウィンドウのトグル表示

メニューバーアプリの 心臓部 です。

use tauri::Manager;
use tauri_plugin_positioner::{WindowExt, Position};

fn toggle_window(app: &tauri::AppHandle) {
    if let Some(window) = app.get_webview_window("main") {
        if window.is_visible().unwrap_or(false) {
            let _ = window.hide();
        } else {
            // macOS の app-level hide からの復帰
            #[cfg(target_os = "macos")]
            {
                let _ = app.show();
            }

            let _ = window.unminimize();
            // ★ これが神: トレイアイコンの直下にウィンドウを配置
            let _ = window.move_window(Position::TrayCenter);
            let _ = window.show();
            let _ = window.set_always_on_top(true);
            let _ = window.set_focus();
        }
    }
}

Position::TrayCentertauri-plugin-positioner が提供する機能で、
トレイアイコンの位置を自動検出して直下にウィンドウを配置 してくれます。

Position::TrayCenter を使うには、Cargo.toml で features = ["tray-icon"] が必要です。
これを忘れると TrayCenter のバリアントが存在しないと言われます。


3. トレイアイコンの構築

setup() 内でトレイアイコンを構築します。

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_positioner::init())
        // ... 他のプラグイン ...
        .setup(|app| {
            // トレイアイコンを構築
            let _tray = tauri::tray::TrayIconBuilder::with_id("main_tray")
                .icon(
                    tauri::image::Image::from_bytes(
                        include_bytes!("../icons/tray.png")
                    )
                    .expect("failed to load tray icon"),
                )
                .icon_as_template(true)  // macOS: ダークモード対応
                .tooltip("MyMenuBarApp")
                .on_tray_icon_event(|tray, event| {
                    // ★ positioner にトレイイベントを渡す(位置計算に必要)
                    tauri_plugin_positioner::on_tray_event(
                        tray.app_handle(), &event
                    );

                    match event {
                        tauri::tray::TrayIconEvent::Click {
                            button: tauri::tray::MouseButton::Left,
                            button_state: tauri::tray::MouseButtonState::Up,
                            ..
                        } => {
                            toggle_window(tray.app_handle());
                        }
                        _ => {}
                    }
                })
                .show_menu_on_left_click(false)  // 左クリックでメニューを出さない
                .build(app)?;

            Ok(())
        })
}

ハマりポイント

tauri_plugin_positioner::on_tray_event() を呼ぶ のを忘れると、
Position::TrayCenter が正しい位置を計算できず、ウィンドウが画面の左上に表示されます。

トレイアイコンの仕様

  • icon_as_template(true) を設定すると、macOS がダークモード/ライトモードに合わせて自動で色を調整してくれます
  • アイコン画像は 白色 + 透過背景 の PNG を用意してください
  • サイズは 22x22 px が推奨です

4. フォーカスが外れたら自動隠蔽

これが メニューバーアプリらしい動作 のカギです。

.on_window_event(|window, event| match event {
    // ✕ボタンで閉じない(隠すだけ)
    tauri::WindowEvent::CloseRequested { api, .. } => {
        if window.label() == "main" {
            let _ = window.hide();
            api.prevent_close();
        }
    }

    // ★ フォーカスが外れたら自動で隠す
    tauri::WindowEvent::Focused(focused) => {
        if !focused && window.label() == "main" {
            let _ = window.hide();

            // macOS特有: アプリ自体も hide する
            #[cfg(target_os = "macos")]
            {
                let _ = window.app_handle().hide();
            }
        }
    }

    _ => {}
})

window.hide()app.hide() の違い

ここが macOS 特有の罠 です。

メソッド 効果
window.hide() ウィンドウだけ隠す
app_handle().hide() アプリ全体 を隠す(macOS の NSApplication.hide()

window.hide() だけだと、macOS の Mission Control やアプリスイッチャーにアプリが残り続けます。
app.hide() を併用することで、完全に見えなくなる ユーティリティアプリの動作を実現できます。

逆に toggle_window で表示する際は app.show() を呼んで復帰させる必要があります。


5. macOS Vibrancy(すりガラスUI)

.setup(|app| {
    // Vibrancy の適用
    #[cfg(target_os = "macos")]
    if let Some(window) = app.get_webview_window("main") {
        use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
        let _ = apply_vibrancy(
            &window,
            NSVisualEffectMaterial::HudWindow,
            None,
            Some(18.0),  // 角丸の半径
        );
    }

    // ... トレイアイコンの構築 ...
    Ok(())
})

Material の選択肢

Material 見た目 おすすめ用途
HudWindow 暗めのすりガラス ダークUI のHUDアプリ
Popover 標準のポップオーバー風 通知パネル風
Menu メニュー風 ドロップダウン風
Sidebar サイドバー風 設定画面
UnderWindowBackground 薄い背景 控えめなUI

HudWindow がメニューバーアプリには最も合います。

CSS 側の対応

Vibrancy を見せるには、フロントエンドの背景を透過させる必要があります。

body {
  background: transparent;
}

.app-container {
  background: rgba(0, 0, 0, 0.3); /* 半透明にして Vibrancy を透かせる */
  backdrop-filter: blur(0px);      /* CSS の blur は不要(OS が処理する) */
}

6. グローバルショートカット

ウィンドウが隠れていても、ショートカットキーで呼び出せるようにします。

.plugin(
    tauri_plugin_global_shortcut::Builder::new()
        .with_shortcuts(["CmdOrCtrl+Shift+H"])
        .unwrap_or(tauri_plugin_global_shortcut::Builder::new())
        .with_handler(|app, _shortcut, event| {
            if event.state == tauri_plugin_global_shortcut::ShortcutState::Pressed {
                toggle_window(app);
            }
        })
        .build(),
)

unwrap_or() でフォールバックしているのは、ショートカットが他のアプリと衝突した場合に
アプリ全体がクラッシュしないようにするためです。


7. ログイン時の自動起動

.plugin(tauri_plugin_autostart::init(
    tauri_plugin_autostart::MacosLauncher::LaunchAgent,
    Some(vec![]),
))

macOS では LaunchAgent を使うのが推奨です。
ユーザーの OS 設定 > ログイン項目にも自動で表示されます。


8. 完成コード(全体像)

pub fn run() {
    tauri::Builder::default()
        // プラグイン
        .plugin(tauri_plugin_positioner::init())
        .plugin(tauri_plugin_autostart::init(
            tauri_plugin_autostart::MacosLauncher::LaunchAgent,
            Some(vec![]),
        ))
        .plugin(
            tauri_plugin_global_shortcut::Builder::new()
                .with_shortcuts(["CmdOrCtrl+Shift+H"])
                .unwrap_or(tauri_plugin_global_shortcut::Builder::new())
                .with_handler(|app, _shortcut, event| {
                    if event.state == tauri_plugin_global_shortcut::ShortcutState::Pressed {
                        toggle_window(app);
                    }
                })
                .build(),
        )
        // コマンド
        .invoke_handler(tauri::generate_handler![quit_app])
        // セットアップ
        .setup(|app| {
            // Vibrancy
            #[cfg(target_os = "macos")]
            if let Some(window) = app.get_webview_window("main") {
                use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
                let _ = apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, Some(18.0));
            }

            // Tray Icon
            let _tray = tauri::tray::TrayIconBuilder::with_id("main_tray")
                .icon(tauri::image::Image::from_bytes(include_bytes!("../icons/tray.png"))?)
                .icon_as_template(true)
                .on_tray_icon_event(|tray, event| {
                    tauri_plugin_positioner::on_tray_event(tray.app_handle(), &event);
                    if let tauri::tray::TrayIconEvent::Click {
                        button: tauri::tray::MouseButton::Left,
                        button_state: tauri::tray::MouseButtonState::Up, ..
                    } = event {
                        toggle_window(tray.app_handle());
                    }
                })
                .show_menu_on_left_click(false)
                .build(app)?;

            Ok(())
        })
        // ウィンドウイベント
        .on_window_event(|window, event| match event {
            tauri::WindowEvent::CloseRequested { api, .. } => {
                if window.label() == "main" {
                    let _ = window.hide();
                    api.prevent_close();
                }
            }
            tauri::WindowEvent::Focused(focused) => {
                if !focused && window.label() == "main" {
                    let _ = window.hide();
                    #[cfg(target_os = "macos")]
                    { let _ = window.app_handle().hide(); }
                }
            }
            _ => {}
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

ハマりポイントまとめ

# 対処
1 Vibrancy が効かない macOSPrivateApi: true を忘れてないか確認
2 TrayCenter が左上に表示される on_tray_event() を呼んでいるか確認
3 app.hide() 後に復帰しない toggle_windowapp.show() を呼ぶ
4 ショートカット登録失敗でクラッシュ unwrap_or() でフォールバック
5 Dock にアイコンが出る skipTaskbar: true を設定
6 CSS 背景が不透明 body { background: transparent; }

まとめ

Tauri v2 でメニューバーアプリを作る最小構成は:

  1. tauri-plugin-positionerTrayCenter 配置
  2. window-vibrancy ですりガラス UI
  3. on_window_event でフォーカスアウト時の自動隠蔽
  4. TrayIconBuilder でトレイアイコン構築

Electron と比べて バイナリサイズが圧倒的に小さく(10MB 以下)、
メモリ使用量も少ないため、常駐アプリに Tauri は最適です。

この記事のコードは HiyokoBar の実装をベースにしています


この記事は HiyokoBar の開発経験をベースに書いています。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?