ウィンドウの作成とライフサイクル
スプラッシュスクリーン、開発サーバーポーリング、macOS 固有の調整、ウィンドウ破棄時のクリーンアップのパターン。
なぜ Rust でウィンドウを作成するのか
Tauri v2 は tauri.conf.json でウィンドウを定義できるが、Rust でプログラム的に作成することで以下を制御できる:
- スプラッシュスクリーンのタイミング
- 開発サーバーの準備完了ポーリング
- 条件付きウィンドウ作成
- 複数ウィンドウインスタンス
- プラットフォーム固有の設定
スプラッシュスクリーンパターン
アプリの初期化中に軽量なスプラッシュウィンドウを表示し、その後メインウィンドウに置き換える:
.setup(|app| {
// 1. Create splash window (non-fatal if it fails)
if let Err(e) = native::window::create_splash_window(app.handle()) {
eprintln!("Failed to create splash window: {e}");
}
// 2. Do initialization work
let project_root = resolve_project_root();
let app_state = Arc::new(AppState::new(project_root));
app.manage(app_state);
// Start file watchers, etc.
// 3. Close splash, create main window
native::window::close_splash_window(app.handle());
native::window::create_main_window(app.handle())?;
Ok(())
})
スプラッシュウィンドウの作成
スプラッシュウィンドウはフレームなし、透明、常に最前面で表示される:
pub fn create_splash_window(
handle: &tauri::AppHandle,
) -> tauri::Result<()> {
let url = WebviewUrl::App("../frontend/splash.html".into());
let _window = WebviewWindowBuilder::new(handle, "splash", url)
.title("myapp")
.inner_size(400.0, 200.0)
.decorations(false)
.transparent(true)
.always_on_top(true)
.resizable(false)
.build()?;
Ok(())
}
スプラッシュの閉じ方
pub fn close_splash_window(handle: &tauri::AppHandle) {
if let Some(window) = handle.get_webview_window("splash") {
let _ = window.close();
}
}
📝 Note
プログラムでウィンドウを作成する場合、tauri.conf.json の "windows" 配列は空にしておく:"windows": []。両方の場所でウィンドウを定義すると、重複が発生する。
開発サーバーポーリング付きメインウィンドウ
開発時には、ウィンドウ作成時に Vite 開発サーバーがまだ準備できていない場合がある。パターンは:ローディングページを即座に表示し、サーバーが応答したら開発サーバーにナビゲートする。
const DEV_SERVER_URL: &str = "http://localhost:37461";
pub fn create_main_window(
handle: &tauri::AppHandle,
) -> tauri::Result<()> {
// Unique label for multiple windows
let label = if handle.get_webview_window("main").is_some() {
format!("main-{}", std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis())
} else {
"main".to_string()
};
let url = if cfg!(debug_assertions) {
// Inline loading page as data: URL
WebviewUrl::External(
loading_page_data_url().parse().unwrap(),
)
} else {
WebviewUrl::default() // Bundled frontend
};
let window = WebviewWindowBuilder::new(handle, &label, url)
.title("myapp")
.inner_size(1400.0, 800.0)
.visible(false) // Start hidden
.on_navigation(move |url: &tauri::Url| {
let url_str = url.as_str();
// Allow local and tauri URLs
if url_str.starts_with("http://localhost")
|| url_str.starts_with("tauri://")
|| url_str.starts_with("data:")
{
return true;
}
// Open external URLs in default browser
if url_str.starts_with("http://")
|| url_str.starts_with("https://")
{
let owned = url_str.to_string();
tauri::async_runtime::spawn_blocking(move || {
let _ = open::that(owned);
});
return false;
}
false // Block unknown schemes
})
.build()?;
// In dev: poll Vite, navigate when ready
if cfg!(debug_assertions) {
let win = window.clone();
std::thread::spawn(move || {
if wait_for_dev_server(DEV_SERVER_URL, 120) {
let url: tauri::Url =
DEV_SERVER_URL.parse().unwrap();
let _ = win.navigate(url);
}
});
}
// Show after a short delay
let win = window.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(200));
let _ = win.show();
});
Ok(())
}
開発サーバーの準備完了チェック
HTTP リクエストの代わりに TCP 接続試行を使用してより高速にポーリングする:
fn wait_for_dev_server(url: &str, timeout_secs: u64) -> bool {
let addr = url
.strip_prefix("http://")
.or_else(|| url.strip_prefix("https://"))
.and_then(|h| h.split('/').next())
.expect("invalid URL");
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(timeout_secs);
while start.elapsed() < timeout {
if std::net::TcpStream::connect(addr).is_ok() {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
false
}
💡 Tip
TcpStream::connect は準備完了チェックにおいて HTTP リクエストより高速である。完全な HTTP レスポンスを待たずに、ポートが開いた時点で即座に返る。
macOS:長押しアクセント文字メニューの抑制
macOS はキーを長押しするとアクセント文字ポップアップを表示する。これはキーリピートを使用するアプリ(テキストエディタ、ターミナルエミュレーターなど)に干渉する。setup() 内で抑制する:
#[cfg(target_os = "macos")]
{
use objc2_foundation::{NSUserDefaults, NSString};
let defaults = NSUserDefaults::standardUserDefaults();
unsafe {
let key = NSString::from_str("ApplePressAndHoldEnabled");
defaults.setBool_forKey(false, &key);
}
}
📝 Note
これは現在のプロセスにのみ影響する。システム全体の設定は変更しない。他のアプリケーションは通常通りアクセントメニューを表示し続ける。
外部リンクの処理
on_navigation コールバックは、WebView が読み込みを許可する URL を制御する。外部リンクをデフォルトブラウザで開くために使用する:
.on_navigation(move |url: &tauri::Url| {
let url_str = url.as_str();
// Allow local dev server and tauri asset URLs
if url_str.starts_with("http://localhost")
|| url_str.starts_with("https://localhost")
|| url_str.starts_with("tauri://")
|| url_str.starts_with("asset://")
|| url_str.starts_with("data:")
{
return true;
}
// External HTTP(S) links -> default browser
if url_str.starts_with("http://")
|| url_str.starts_with("https://")
{
let owned = url_str.to_string();
tauri::async_runtime::spawn_blocking(move || {
let _ = open::that(owned);
});
return false;
}
// Block unknown schemes (javascript:, file:, etc.)
false
})
⚠️ Warning
javascript: および file: スキームは常にブロックする。これらを許可すると、WebView にセキュリティ脆弱性が生じる。
ウィンドウ破棄イベントの処理
ウィンドウが破棄された際にリソースをクリーンアップするために on_window_event を使用する:
.on_window_event(|window, event| {
if let tauri::WindowEvent::Destroyed = event {
if window.label() == "main" {
if let Some(state) = window.try_state::<Arc<AppState>>() {
// Kill all PTY sessions
commands::terminal::kill_all_ptys(&state);
}
}
}
})
サイドカープロセスのクリーンアップ
サイドカープロセスを生成するアプリでは、run コールバックを使用してウィンドウ破棄時にクリーンアップする:
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(move |app_handle, event| match &event {
tauri::RunEvent::WindowEvent {
event: tauri::WindowEvent::Destroyed,
..
} => {
if let Ok(mut guard) = sidecar_arc.lock() {
if let Some(mut sidecar) = guard.take() {
kill_sidecar(&mut sidecar);
}
}
app_handle.exit(0);
}
_ => {}
});
⚠️ Warning
アプリが子プロセス(PTY セッション、サイドカーサーバー)を生成する場合、ウィンドウ破棄時に明示的に kill する必要がある。そうしなければ、アプリ終了後もオーファンプロセスとして実行し続ける。
ウィンドウライフサイクルの概要
重要なポイント
- プログラムでウィンドウを作成する場合、
tauri.conf.jsonの"windows": []を空にする - ウィンドウは非表示で開始する — コンテンツが準備できてから表示し、白いフラッシュを回避する
- ローディングページには
data:URL を使用する — バンドルされた HTML ファイルは不要 - TCP 接続で開発サーバーをポーリングする — HTTP リクエストより高速
- 子プロセスはウィンドウ破棄時に必ずクリーンアップする
on_navigationでリンクの動作を制御する — 外部リンクはデフォルトブラウザで開く- プラットフォーム固有のコード は
#[cfg(target_os = "macos")]ガードの背後に配置する