ウィンドウにフォーカスを当てる・外す

setFocus()(権限 core:window:allow-set-focus)で手前に出し、onFocusChanged() で変化を追う。外す API はないため、代わりの手段と focus: false で奪わない作り方も示す。

ウィンドウ 対象: Tauri 2.x 更新日: 読了目安: 約8分 win-012
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 手前に出してフォーカスする
  4. フォーカスの変化を監視する
  5. フォーカスを外す・奪わない
  6. 2. バックエンドから実装する (Rust)
  7. 動作確認
  8. よくあるエラーと対処法
  9. OS ごとの違いと注意点
  10. 関連レシピ

通知をクリックしたときや二重起動されたときに既存のウィンドウを手前に出す、アプリに戻ってきたらデータを読み直す、作業中の入力を奪わないお知らせウィンドウを出す。フォーカスを扱うのはこうした場面です。手前に出すのは JS の setFocus() か Rust の set_focus()、状態を知るのは isFocused() と onFocusChanged() です(プラグイン不要)。最初に押さえておきたいのは、フォーカスを外す専用の API はないことです。「別の手段で外す」と「最初から奪わない」を使い分けます。

前提条件

読み取りとイベントは core:default で足りますが、フォーカスを動かす操作は権限の追加が必要です。

使う API権限core:default に
isFocused()core:window:allow-is-focused含まれる
onFocusChanged()core:event:allow-listen含まれる
setFocus()core:window:allow-set-focus含まれない
setFocusable()core:window:allow-set-focusable含まれない

1 章の例では、手前に出す前に show() / unminimize() も呼び、お知らせウィンドウを JS から作るので、それらの権限も足します。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "core:window:allow-set-focus",
    "core:window:allow-show",
    "core:window:allow-unminimize",
    "core:webview:allow-create-webview-window"
  ]
}

設定ファイル(またはウィンドウを作るときのオプション)と実行時 API は次のように使い分けます。focus は作成時の 1 回だけ効き、focusable はその後もずっと効きます。

やりたいこと使うもの
作成時にフォーカスを奪わないfocus: false(Rust は focused(false))
以後フォーカスを受け取らないfocusable: false / setFocusable(false)
手前に出して入力を受けるsetFocus() / set_focus()
フォーカスの変化を知るonFocusChanged() / WindowEvent::Focused

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

手前に出してフォーカスする

setFocus() は「手前に出してフォーカスする」操作ですが、非表示や最小化のウィンドウはこれだけでは戻らないことがあります。表示(ウィンドウを表示・非表示にする)、最小化の解除(ウィンドウを最大化・最小化する)、setFocus() の順にまとめておくと、どの状態からでも同じように呼べます。

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

export async function bringToFront(label: string) {
  const win = await Window.getByLabel(label);
  if (!win) throw new Error(`window '${label}' not found`);
  await win.show();                                    // 非表示なら表示する
  if (await win.isMinimized()) await win.unminimize(); // 最小化なら元に戻す
  await win.setFocus();                                // 手前に出してキー入力を受ける
}

フォーカスの変化を監視する

onFocusChanged() は、フォーカスを得たとき true、失ったとき false を受け取ります。よくある使い方は「しばらく離れていたらデータを読み直す」「非アクティブの間は自作タイトルバーを淡色にする」です。setFocus() の Promise はフォーカスが移り終わる前に解決することがあるので、直後の isFocused() で確かめるより、このイベントで状態を受け取る方が確実です。

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

const win = getCurrentWindow();
let blurredAt = Date.now();

export const stopWatching = await win.onFocusChanged(({ payload: focused }) => {
  console.log('focused:', focused);
  document.body.classList.toggle('inactive', !focused); // 淡色表示用の CSS クラス
  if (!focused) {
    blurredAt = Date.now();
  } else if (Date.now() - blurredAt > 5 * 60 * 1000) {
    console.log('5 分以上離れていたのでデータを読み直す');
  }
});

フォーカスを外す・奪わない

ウィンドウの blur() にあたる API はないので、目的に合わせて次の手段を選びます。

やりたいこと方法
アプリ内の別ウィンドウへ移す移したい側の setFocus()
このウィンドウを退かすminimize() か hide()(次にフォーカスを受けるウィンドウは OS が決める)
クリックされてもフォーカスを受けないsetFocusable(false) / focusable: false
ページ内の入力欄から外すだけDOM の element.blur()(ウィンドウのフォーカスとは別物)

作業中のユーザーの入力を奪いたくないお知らせ用のウィンドウは、作るときに focus: false と focusable: false を指定します。

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

export function showToast() {
  const toast = new WebviewWindow('toast', {
    url: '/toast.html',
    width: 320,
    height: 80,
    decorations: false,
    alwaysOnTop: true,
    skipTaskbar: true,
    focus: false,     // 作成時にフォーカスを移さない
    focusable: false, // クリックされてもフォーカスを受けない
  });
  toast.once('tauri://error', (e) => console.error('create failed', e));
}

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

Rust にも同じ「手前に出す」関数を置いておくと、トレイやグローバルショートカット、二重起動を検知したとき(Single Instance プラグイン のコールバック)など、どこからでも呼べます。フォーカスの変化は WindowEvent::Focused で受けます。例では、ランチャーのような popup ウィンドウを、フォーカスを失ったら隠すようにしています。自作コマンドは capability に追加しなくても呼べます。

use tauri::{AppHandle, Manager, WindowEvent};

/// 非表示・最小化・裏に隠れている、のどの状態からでも手前に出してフォーカスする
fn bring_to_front(app: &AppHandle, label: &str) -> tauri::Result<()> {
    let Some(window) = app.get_webview_window(label) else {
        return Ok(()); // まだ作られていない(または閉じられた)
    };
    window.show()?;
    if window.is_minimized()? {
        window.unminimize()?;
    }
    window.set_focus()
}

#[tauri::command]
fn focus_window(app: AppHandle, label: String) -> Result<(), String> {
    bring_to_front(&app, &label).map_err(|e| e.to_string())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .on_window_event(|window, event| {
            // popup はフォーカスを失ったら隠す(ランチャーやトレイのポップアップ向け)
            if window.label() == "popup" {
                if let WindowEvent::Focused(false) = event {
                    let _ = window.hide();
                }
            }
        })
        .invoke_handler(tauri::generate_handler![focus_window])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';

await invoke('focus_window', { label: 'popup' });

動作確認

npm run tauri dev で起動し、別のアプリをクリックしてから戻ると、コンソールに次のように出ます。離れている間は body に inactive クラスが付きます。focus_window で popup を出してから別の場所をクリックすると、popup が消えます。

focused: false
focused: true

setTimeout(showToast, 3000) で呼んでから main の入力欄に文字を打ち続けると、お知らせが出たあとも入力は main に入り続けます。

よくあるエラーと対処法

  • 「window.set_focus not allowed. Permissions associated with this command: core:window:allow-set-focus」: 権限の追加漏れです。リリースビルドでは「Command plugin:window|set_focus not allowed by ACL」だけになります。
  • setFocus() を呼んでもウィンドウが出てこない: 非表示か最小化の状態です。show()、最小化の解除、setFocus() の順に呼びます。
  • setFocus() 直後の isFocused() が false: 移り終わる前に確かめています。状態は onFocusChanged() で受け取ります。
  • popup が意図せず消える: フォーカスを失ったら隠す処理は、popup からダイアログや DevTools を開いてフォーカスが移ったときにも動きます。ダイアログを出す間はフラグを立てて隠さないようにします。
  • トーストを作ると「webview.create_webview_window not allowed.」で始まるエラーになる: core:webview:allow-create-webview-window の追加漏れです。

OS ごとの違いと注意点

  • macOS: フォーカス中のウィンドウに setFocusable(false) を呼んでも、フォーカスは外れません。その状態で setFocus() を呼ぶと、ウィンドウは重なり順の最背面に回ります。外したいなら先に別のウィンドウへ移してから setFocusable(false) にします。
  • フォーカスを奪うのは最後の手段: 処理が終わったことを知らせるだけなら、入力を横取りしない タスクバーの点滅 か通知を使います。setFocus() は、通知のクリック、トレイ、二重起動など、ユーザーの操作がきっかけのときに使うのが無難です。
  • 最前面との違い: 常に最前面 は重なり順の指定で、フォーカスとは別です。固定しただけではフォーカスは移らず、逆に setFocus() で手前に出しても、他のウィンドウをクリックされれば裏に回ります。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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