Positioner プラグインで位置を調整する

Positioner プラグインの moveWindow()(権限 positioner:default)でウィンドウを画面の四隅やトレイアイコンの真上へ動かす。tray-icon 機能の設定と、基準が作業領域ではない点も示す。

プラグイン拡張 対象: Tauri 2.x 更新日: 読了目安: 約10分 plugin-010
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 画面の角や辺に動かす
  4. トレイアイコンの真上にパネルを出す
  5. 2. バックエンドから実装する (Rust)
  6. 動作確認
  7. よくあるエラーと対処法
  8. OS ごとの違いと注意点
  9. 関連レシピ

「小さなウィジェットを画面の右上に寄せる」「トレイアイコンの真上にパネルを開く」といった配置を、座標を計算せずに名前で指定できるのが Positioner プラグインです。JS は moveWindow()、Rust は WindowExt の move_window() を使います。画面中央に置くだけなら ウィンドウを画面中央に配置する、任意の座標なら ウィンドウの位置を指定する の方法が向いています。

前提条件

npm run tauri add positioner

変わる場所は プラグインをインストールして有効化する のとおりで、デスクトップ専用なので、クレートは Cargo.toml のデスクトップ向けの節に、権限は capabilities/desktop.json に入ります。positioner:default は moveWindow()・moveWindowConstrained()・handleIconState() を許可するもので、core:default には含まれません。

Tray で始まる位置は、プラグインの tray-icon 機能を有効にしたときだけ使えます。tauri add はこれを書かないので手で直し、トレイに必要な tauri 側の機能も並べます(menu-009)。

[dependencies]
tauri = { version = "2", features = ["tray-icon"] }

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-positioner = { version = "2", features = ["tray-icon"] }

JS のトレイの例まで動かす capability です。moveWindow() が動かすのは呼び出したウィンドウなので、呼ぶページのウィンドウを windows に入れます。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "desktop-capability",
  "platforms": ["macOS", "windows", "linux"],
  "windows": ["main", "popup"],
  "permissions": [
    "core:default",
    "core:app:allow-default-window-icon",
    "core:window:allow-show",
    "core:window:allow-hide",
    "core:window:allow-set-focus",
    "positioner:default"
  ]
}

指定できる位置は次のとおりです。

位置置き方
TopLeft TopRight BottomLeft BottomRightモニターの四隅
TopCenter BottomCenter LeftCenter RightCenter各辺の中央
Centerモニターの中央
TrayLeft TrayCenter TrayRightアイコンのすぐ上(入りきらなければ置き直す)
TrayBottomLeft TrayBottomCenter TrayBottomRightウィンドウの上端をアイコンの上端にそろえる

Tray 系の横位置は、Center がアイコンと中央をそろえ、Left / Right はアイコンの左端 / 右端から右へ広がります。

基準はモニター全体で、タスクバーなどを除いた作業領域ではありません。画面の下にタスクバーがあると、BottomRight のウィンドウは下端が隠れます。中央寄せには作業領域を基準にする core の center() を、作業領域の角に置くなら workArea から計算して setPosition()(win-005)を使います。

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

画面の角や辺に動かす

引数は行き先だけで、動くのは呼び出したウィンドウです。ほかのウィンドウは、そのページで呼ぶか Rust で動かします。

import { moveWindow, Position } from '@tauri-apps/plugin-positioner';

// <button data-pos="TopRight"> のようなボタンで、このウィンドウを四隅へ寄せる
document.querySelectorAll<HTMLButtonElement>('button[data-pos]').forEach((button) => {
  button.addEventListener('click', async () => {
    const name = button.dataset.pos as keyof typeof Position;
    await moveWindow(Position[name]); // positioner:allow-move-window が必要
  });
});

基準は今いるモニターなので、別のモニターの角へは、先に setPosition() でそのモニターへ移してから呼びます。

トレイアイコンの真上にパネルを出す

先に handleIconState() で、アイコンの位置と大きさをプラグインへ渡します。クリックもマウスの出入りも位置を含むので、種類を問わず渡します。一度も渡していないと「Tray position not set」で失敗します。トレイは画面の端にあるので、はみ出す分をモニターの内側へ戻す moveWindowConstrained() を使います。次のコードは、枠なしで "visible": false の popup ウィンドウのページで動かします。

import { TrayIcon, type TrayIconEvent } from '@tauri-apps/api/tray';
import { defaultWindowIcon } from '@tauri-apps/api/app';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { handleIconState, moveWindowConstrained, Position } from '@tauri-apps/plugin-positioner';

const popup = getCurrentWindow();

async function onTrayEvent(event: TrayIconEvent) {
  await handleIconState(event); // どの種類のイベントでも、アイコンの位置と大きさを渡す
  if (event.type === 'Click' && event.button === 'Left' && event.buttonState === 'Up') {
    await moveWindowConstrained(Position.TrayCenter); // アイコンの真上。はみ出す分は内側へ
    await popup.show();
    await popup.setFocus();
  }
}

// ほかの場所をクリックしてフォーカスが外れたら隠す
await popup.onFocusChanged(({ payload: focused }) => {
  if (!focused) void popup.hide();
});

if (await TrayIcon.getById('main')) await TrayIcon.removeById('main'); // 再読み込みで増やさない
await TrayIcon.new({
  id: 'main',
  icon: (await defaultWindowIcon()) ?? undefined,
  showMenuOnLeftClick: false,
  action: (event) => void onTrayEvent(event),
});

クリックの判定は トレイアイコンがクリックされた時の処理を書く で詳しく扱っています。この JS 版と次の Rust 版は、どちらか一方だけを使います。

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

WindowExt を use すると WebviewWindow で move_window() と move_window_constrained() が使え、ラベルで取り出したどのウィンドウでも動かせます。トレイのイベントはすべて on_tray_event() に渡します(設定ファイルで作ったアイコンなら Builder::on_tray_icon_event() の中で)。tray-icon 機能を有効にしたら、Rust だけで使う場合もプラグインの登録が必要です。起動時の位置を setup で決めてから show() すれば、JS の権限は要らず、別の位置が一瞬見えることもありません。

{
  "app": {
    "windows": [
      { "label": "main", "title": "My App", "width": 800, "height": 600, "visible": false },
      {
        "label": "popup",
        "url": "popup.html",
        "width": 320,
        "height": 420,
        "visible": false,
        "decorations": false,
        "resizable": false,
        "alwaysOnTop": true,
        "skipTaskbar": true
      }
    ]
  }
}
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::{AppHandle, Manager, WindowEvent};
use tauri_plugin_positioner::{Position, WindowExt};

/// パネルをトレイアイコンの真上に置いて表示する
fn show_panel(app: &AppHandle) -> tauri::Result<()> {
    let Some(panel) = app.get_webview_window("popup") else {
        return Ok(());
    };
    panel.move_window_constrained(Position::TrayCenter)?; // はみ出す分はモニターの内側へ
    panel.show()?;
    panel.set_focus()
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_positioner::init()) // tray-icon 機能を有効にしたら必ず登録する
        .setup(|app| {
            // "visible": false の main を右上に置いてから表示する
            if let Some(main) = app.get_webview_window("main") {
                main.move_window(Position::TopRight)?;
                main.show()?;
            }
            TrayIconBuilder::with_id("main")
                .icon(tauri::include_image!("icons/32x32.png"))
                .show_menu_on_left_click(false)
                .on_tray_icon_event(|tray, event| {
                    // どの種類のイベントでも、アイコンの位置をプラグインへ渡す
                    tauri_plugin_positioner::on_tray_event(tray.app_handle(), &event);
                    if let TrayIconEvent::Click {
                        button: MouseButton::Left,
                        button_state: MouseButtonState::Up,
                        ..
                    } = event
                    {
                        let _ = show_panel(tray.app_handle());
                    }
                })
                .build(app)?;
            Ok(())
        })
        .on_window_event(|window, event| {
            // パネルはフォーカスを失ったら隠す
            if window.label() == "popup" {
                if let WindowEvent::Focused(false) = event {
                    let _ = window.hide();
                }
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

動作確認

npm run tauri dev で起動し、data-pos="BottomRight" のボタンを押すと右下に移ります(下にタスクバーがあれば下端が隠れます)。Rust 版では main が右上に出て、トレイのアイコンを左クリックすると真上にパネルが開き、ほかの場所をクリックすると消えます。起動直後、アイコンにマウスを乗せる前に次を実行すると、コンソールに「Tray position not set」と出ます。

import { moveWindow, Position } from '@tauri-apps/plugin-positioner';

await moveWindow(Position.TrayCenter).catch((e) => console.error(e));

よくあるエラーと対処法

  • 「positioner.move_window not allowed. Permissions associated with this command: positioner:allow-move-window, positioner:default」: 呼び出したウィンドウに権限がありません。desktop.json の windows にそのラベルがあるかも確かめます。リリースビルドでは「Command plugin:positioner|move_window not allowed by ACL」です。
  • 「Tray position not set」: トレイのイベントをまだ受けていないか、handleIconState() / on_tray_event() を呼んでいません。
  • 「invalid args position for command move_window」で始まるエラーや「Command move_window_constrained not found」: プラグインの tray-icon 機能が無効です。JS の Position には Tray 系が常にあるので、型チェックでは気付けません。
  • move_window() で「state() called before manage() for」で始まる panic: tray-icon 機能を有効にしたのに、プラグインを登録していません。

OS ごとの違いと注意点

  • iOS / Android: 使えません。モバイルもビルドするなら、登録を #[cfg(desktop)] で囲みます。
  • Linux: トレイのイベントが届かないので、Tray 系は「Tray position not set」のままです。画面基準の位置は使えます。
  • Tray 系の位置: 上に入りきらないときの置き直し方は OS で違い、トレイが画面の上下どちらにあるかでも収まり方が変わります。配布する OS ごとに確かめ、合わなければ TrayBottomCenter などに替えます。
  • 倍率の違うモニター: 移った先で物理サイズが変わり、角から少しずれることがあります。もう一度呼べば直ります。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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