通知をクリックしたときや二重起動されたときに既存のウィンドウを手前に出す、アプリに戻ってきたらデータを読み直す、作業中の入力を奪わないお知らせウィンドウを出す。フォーカスを扱うのはこうした場面です。手前に出すのは 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()で手前に出しても、他のウィンドウをクリックされれば裏に回ります。
