ウィンドウを常に最前面に固定する

alwaysOnTop と setAlwaysOnTop()(権限 core:window:allow-set-always-on-top)でウィンドウを最前面に固定し、ピン留めボタンでの切り替えと、裏に隠れるダイアログの対処を示す。

ウィンドウ 対象: Tauri 2.x 更新日: 読了目安: 約10分 win-007
目次
  1. 前提条件
  2. 設定ファイルと実行時 API の使い分け
  3. 1. フロントエンドから実装する (TypeScript)
  4. ピン留めボタンで切り替える
  5. サブウィンドウとダイアログを固定ウィンドウの上に出す
  6. alwaysOnBottom との違いと切り替え順
  7. 2. バックエンドから実装する (Rust)
  8. 動作確認
  9. よくあるエラーと対処法
  10. OS ごとの違いと注意点
  11. 関連レシピ

付箋やタイマー、配信用のオーバーレイのように、他のアプリを操作している間も隠れてほしくないウィンドウは「常に最前面」に固定します。起動時から固定するなら tauri.conf.json の alwaysOnTop、ピン留めボタンで切り替えるなら JS の setAlwaysOnTop() か Rust の set_always_on_top() を使います(プラグイン不要)。固定したウィンドウの裏に設定画面やダイアログが隠れる問題と、OS ごとの落とし穴もまとめます。

前提条件

最前面に関する権限のうち core:default に入っているのは状態を読む core:window:allow-is-always-on-top だけで、切り替え用の core:window:allow-set-always-on-top は追加が必要です。setAlwaysOnBottom()・setFocus()・サブウィンドウの作成も、使う分だけ権限を足します。サブウィンドウから呼ぶなら、そのラベルも windows に入れます。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "main と設定ウィンドウで最前面を切り替える",
  "windows": ["main", "settings"],
  "permissions": [
    "core:default",
    "core:window:allow-set-always-on-top",
    "core:window:allow-set-always-on-bottom",
    "core:window:allow-set-focus",
    "core:webview:allow-create-webview-window"
  ]
}

起動時から固定するなら設定ファイルに書きます。これだけなら権限は不要です。

{
  "app": {
    "windows": [
      { "label": "main", "title": "付箋", "width": 360, "height": 420, "alwaysOnTop": true }
    ]
  }
}

2 章のダイアログの例だけ dialog プラグイン(npm run tauri add dialog)を使います。

設定ファイルと実行時 API の使い分け

方法向いている場面JS から使うときの権限
alwaysOnTop: true(設定ファイル / WebviewWindow のオプション)いつも最前面で使うウィンドウ設定ファイルは不要。WebviewWindow は作成の権限だけ
setAlwaysOnTop() / Rust の set_always_on_top()ピン留めボタン、状態の復元core:window:allow-set-always-on-top
isAlwaysOnTop() / Rust の is_always_on_top()現在の状態を UI に反映するcore:default に含まれる

window-state プラグイン(2.4 系)が保存する項目に最前面は含まれないので、次回起動時に戻したいなら自前で保存します。

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

ピン留めボタンで切り替える

<button id="pin-button" type="button" aria-pressed="false">最前面に固定</button>

起動時に isAlwaysOnTop() で実際の状態を読み、前回の状態が localStorage にあれば適用します。ボタンの表示は setAlwaysOnTop() が成功してから変えます。先に変えると、権限不足で失敗したときに見た目と実際がずれます。

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

async function setupPinButton(button: HTMLButtonElement) {
  const win = getCurrentWindow();
  const key = `pinned:${win.label}`;
  const render = (on: boolean) => {
    button.setAttribute('aria-pressed', String(on));
    button.textContent = on ? '固定を解除' : '最前面に固定';
  };

  let pinned = await win.isAlwaysOnTop(); // 設定ファイルの値を反映した実際の状態
  const saved = localStorage.getItem(key);
  if (saved !== null && (saved === 'true') !== pinned) {
    await win.setAlwaysOnTop(saved === 'true'); // 前回終了時の状態を復元
    pinned = saved === 'true';
  }
  render(pinned);
  console.log('[pin] initial:', pinned);

  button.addEventListener('click', async () => {
    button.disabled = true;
    try {
      await win.setAlwaysOnTop(!pinned); // core:window:allow-set-always-on-top が必要
      pinned = !pinned;                  // 成功してから状態と表示を変える
      localStorage.setItem(key, String(pinned));
      render(pinned);
      console.log('[pin] ->', pinned);
    } catch (e) {
      console.error('[pin] failed:', e); // 権限不足はここに届く
    } finally {
      button.disabled = false;
    }
  });
}

window.addEventListener('DOMContentLoaded', () => {
  const button = document.querySelector<HTMLButtonElement>('#pin-button');
  if (button) setupPinButton(button).catch(console.error);
});

サブウィンドウとダイアログを固定ウィンドウの上に出す

最前面のウィンドウは親子関係のない普通のウィンドウより常に上にあるので、固定中の main から設定画面を普通に開くと main の裏に隠れます。parent を指定すると子ウィンドウとして常に親の上に並ぶので、隠れなくなります。Windows では親の最小化・終了にも連動します。親と無関係に使うなら、サブウィンドウ自身も alwaysOnTop: true にします。親の操作までブロックするなら モーダル表示 を使います。

import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
import { getCurrentWindow } from '@tauri-apps/api/window';

export async function openSettings() {
  const opened = await WebviewWindow.getByLabel('settings');
  if (opened) return opened.setFocus(); // 開いていれば前面に出すだけ
  const w = new WebviewWindow('settings', {
    url: '/settings.html', title: '設定', width: 420, height: 320,
    parent: getCurrentWindow(), // 最前面の親より上に並ぶ
  });
  w.once('tauri://error', (e) => console.error('create failed', e));
}

dialog プラグイン の JS API(message()・ask()・save() など)は、呼び出し元のウィンドウを自動で親にします(open() だけは Windows と macOS のみ)。危ないのは Rust から出すダイアログで、2 章のように .parent() を付けます。

alwaysOnBottom との違いと切り替え順

alwaysOnBottom は逆に「常に他のウィンドウの下」に置く指定で、デスクトップに貼り付けるウィジェット向けです(Windows は最背面の保証なし、Wayland は未対応)。生成時のビルダーは片方を指定するともう片方を false に戻しますが、実行時のメソッドにはそうした調整がありません。macOS と Windows では、片方を false にするともう片方の指定まで解除されるため、最背面の状態から setAlwaysOnTop(true) → setAlwaysOnBottom(false) の順に呼ぶと最前面が外れます。先に両方を解除し、最後に目的の側だけをオンにします。

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

export async function setLayer(layer: 'top' | 'normal' | 'bottom') {
  const win = getCurrentWindow();
  await win.setAlwaysOnTop(false);
  await win.setAlwaysOnBottom(false); // core:window:allow-set-always-on-bottom が必要
  if (layer === 'top') await win.setAlwaysOnTop(true);
  if (layer === 'bottom') await win.setAlwaysOnBottom(true);
}

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

トレイや グローバルショートカット から切り替えるときは Rust で set_always_on_top() を呼びます。最前面の変化を知らせる WindowEvent はないので、切り替えたらイベントを送ってフロントのボタン表示を合わせます。

use tauri::Emitter;
use tauri_plugin_dialog::DialogExt;

#[derive(Clone, serde::Serialize)]
struct PinChanged {
    label: String,
    pinned: bool,
}

/// 最前面を反転して新しい状態を返す(トレイやショートカットのハンドラからも呼べる)
fn toggle_pin(window: &tauri::WebviewWindow) -> tauri::Result<bool> {
    let next = !window.is_always_on_top()?;
    window.set_always_on_top(next)?;
    window.emit("pin-changed", PinChanged { label: window.label().into(), pinned: next })?;
    Ok(next)
}

#[tauri::command]
fn toggle_always_on_top(window: tauri::WebviewWindow) -> Result<bool, String> {
    toggle_pin(&window).map_err(|e| e.to_string())
}

/// Rust から出すダイアログは親を明示する(付けないと最前面の window の裏に隠れうる)
fn show_notice(window: &tauri::WebviewWindow, text: &str) {
    window.dialog().message(text).title("お知らせ").parent(window).show(|_| {});
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![toggle_always_on_top])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';

// Rust 側で切り替えられたときも、1 章の pinned と表示を更新する
export function watchPin(onChange: (pinned: boolean) => void) {
  const me = getCurrentWindow().label;
  return listen<{ label: string; pinned: boolean }>('pin-changed', (e) => {
    if (e.payload.label === me) onChange(e.payload.pinned);
  });
}

export const togglePinByRust = () => invoke<boolean>('toggle_always_on_top');

動作確認

npm run tauri dev でボタンを押すと次のログが出て、別のアプリをクリックしてもウィンドウが手前に残ります。再起動すると initial が前回の状態になり、固定中に開いた設定ウィンドウは main の上に出ます。Rust 版は invoke の戻り値が新しい状態で、pin-changed イベントも届きます。

[pin] initial: false
[pin] -> true
[pin] -> false

よくあるエラーと対処法

  • 「window.set_always_on_top not allowed. Permissions associated with this command: core:window:allow-set-always-on-top」: 権限不足です。追加して tauri dev を再起動します。リリースビルドでは「Command plugin:window|set_always_on_top not allowed by ACL」と短く出ます。
  • サブウィンドウからだけ「window.set_always_on_top not allowed on window "settings", webview "settings", URL: local」: capability の windows にそのラベルがありません。
  • macOS でフルスクリーン解除後に固定が外れる: フルスクリーンの切り替え時に、重なり順の指定が通常に戻るためです。抜けたあとで setAlwaysOnTop(true) をかけ直します(このとき isAlwaysOnTop() も false を返します)。
  • Linux で何も起きず、エラーも出ない: Wayland は未対応、X11 もウィンドウマネージャー次第で、どちらも Promise は成功で返ります。Linux の isAlwaysOnTop() は実際に適用された状態を返すので、少し間をおいて読めば確認できます。
  • 設定ファイルの alwaysOnBottom: true が効かないことがある: 2.11 系では、設定から作るときに既定値 false の alwaysOnTop が後から適用され、最背面の指定が解除されることがあります。起動後に setAlwaysOnBottom(true) を呼ぶと確実です。

OS ごとの違いと注意点

  • Windows: 固定してもフォーカスは移りません。setFullscreen() で全画面に出入りしても固定は保たれます。
  • macOS: フルスクリーン の出入りで固定が外れます。すべての操作スペース(Space)に出すなら visibleOnAllWorkspaces: true を併用します(Windows は非対応)。
  • Linux: ウィンドウマネージャーへの「要望」として伝えるだけなので、X11 では無視されることがあり、Wayland は未対応です。
  • iOS / Android: 未対応です(コマンド自体がデスクトップ専用)。
  • 他の最前面ウィンドウ・フルスクリーンとの関係: 最前面は「最前面のグループに入る」指定で、他アプリの最前面ウィンドウより上になる保証はなく、順番を指定する API もありません。手前に出したいときは、前面に出してフォーカスする setFocus() を使います。他アプリのフルスクリーン画面の上に出るかどうかも OS と相手の方式しだいで、Tauri から強制できません。
  • オーバーレイ用途では、生成時にフォーカスを奪わない focus: false や クリックスルー と組み合わせます。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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