Tauriの基礎 - ウインドウ管理

提供: MochiuWiki : SUSE, EC, PCB

概要

Tauriのウインドウ管理機能は、デスクトップアプリケーションにおけるウインドウの作成、操作、イベント処理を包括的にサポートする。
単一ウインドウのアプリケーションから複数ウインドウを連携させるマルチウインドウアプリケーションまで、柔軟なウインドウアーキテクチャを構築できる。

Tauri v2では、WebviewWindow APIが中心となり、RustバックエンドとJavaScript フロントエンドの両方からウインドウ操作が可能である。

設定ファイル (tauri.conf.json) による宣言的な初期設定と、プログラムによる動的なウインドウ作成の2つのアプローチを提供する。

ウインドウのサイズ変更、最小化、最大化、閉じる等の基本操作に加え、ウインドウ間のデータ共有、イベント通知、状態の永続化 (window-stateプラグイン) 等もサポートする。

クロスプラットフォーム対応を考慮した設計により、Windows、MacOS、Linuxで一貫した動作を実現する。


ウインドウ管理の基本概念

WebviewWindow アーキテクチャ

Tauri v2におけるウインドウ管理の中核は WebviewWindow である。

ウインドウ関連の主要コンポーネント
コンポーネント 説明
WebviewWindow WebViewコンテンツを表示するウインドウ
HTML/CSS/JavaScriptでUIを構築
WebviewWindowBuilder プログラムによるウインドウ作成用ビルダー
プロパティをチェーンで設定
AppHandle アプリケーション全体へのアクセス
ウインドウの取得・作成に使用


ウインドウの識別子

各ウインドウは一意のラベル (label) で識別される。

 // ウインドウラベルの例
 "main"      // メインウインドウ
 "settings"  // 設定ウインドウ
 "about"     // バージョン情報
 "dialog"    // ダイアログウインドウ


ウインドウのライフサイクル

ウインドウライフサイクル
フェーズ 説明 イベント
作成 WebviewWindowBuilderで作成 Created
表示 visible=true で表示 Shown
フォーカス ユーザが操作開始 Focused
操作中 ユーザがUIを操作 -
非フォーカス 別ウインドウにフォーカス移動 Blurred
最小化 タスクバーに格納 Minimized
復元 最小化から復元 Restored
閉じる ユーザまたはプログラムで閉じる CloseRequested




tauri.conf.jsonでのウインドウ初期設定

基本的な設定

tauri.conf.json ファイルの app.windows 配列でウインドウを定義する。

 {
   "productName": "my-tauri-app",
   "version": "1.0.0",
   "app": {
     "windows": [
       {
         "label": "main",
         "title": "My Application",
         "width": 1024,
         "height": 768,
         "resizable": true,
         "fullscreen": false,
         "focus": true
       }
     ]
   }
 }


設定プロパティ一覧

ウインドウ設定プロパティ
プロパティ 説明 デフォルト値
label string ウインドウの一意識別子 "main"
title string ウインドウタイトル アプリ名
url string 初期URL "/"
width number 幅 (ピクセル) 800
height number 高さ (ピクセル) 600
minWidth number 最小幅 -
minHeight number 最小高さ -
maxWidth number 最大幅 -
maxHeight number 最大高さ -
resizable boolean リサイズ可能か true
maximizable boolean 最大化可能か true
minimizable boolean 最小化可能か true
closable boolean 閉じる可能か true
fullscreen boolean フルスクリーン false
visible boolean 起動時に表示 true
focus boolean 起動時にフォーカス true
transparent boolean 透明背景 false
decorations boolean タイトルバー表示 true
alwaysOnTop boolean 常に最前面 false
center boolean 画面中央に配置 false
x number X座標 -
y number Y座標 -



マルチウインドウ設定

複数のウインドウを定義する例を示す。

 {
   "app": {
     "windows": [
       {
         "label": "main",
         "title": "Main Window",
         "width": 1200,
         "height": 800,
         "center": true
       },
       {
         "label": "settings",
         "title": "Settings",
         "url": "/settings",
         "width": 600,
         "height": 500,
         "visible": false,
         "resizable": false
       },
       {
         "label": "about",
         "title": "About",
         "url": "/about",
         "width": 400,
         "height": 300,
         "resizable": false,
         "maximizable": false,
         "minimizable": false
       }
     ]
   }
 }


スプラッシュスクリーン設定

アプリケーション起動時にスプラッシュスクリーンを表示して、初期化完了後にメインウインドウを表示する設定。

 {
   "app": {
     "windows": [
       {
         "label": "main",
         "title": "Main Window",
         "visible": false
       },
       {
         "label": "splashscreen",
         "title": "Loading...",
         "url": "/splashscreen",
         "width": 400,
         "height": 300,
         "resizable": false,
         "decorations": false,
         "center": true
       }
     ]
   }
 }



WebviewWindow API

現在のウインドウを取得

フロントエンド (JavaScript / TypeScript) から現在のウインドウを取得する。

 // src/utils/window.ts
 import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
 
 // 現在のウインドウを取得
 const currentWindow = getCurrentWebviewWindow();
 
 // ウインドウ情報を取得
 console.log('Label:', await currentWindow.label());
 console.log('Title:', await currentWindow.title());
 console.log('Size:', await currentWindow.innerSize());


ウインドウ操作メソッド

 // src/utils/windowOperations.ts
 import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
 
 export async function minimizeWindow() {
   const window = getCurrentWebviewWindow();
   await window.minimize();
 }
 
 export async function maximizeWindow() {
   const window = getCurrentWebviewWindow();
   await window.maximize();
 }
 
 export async function unmaximizeWindow() {
   const window = getCurrentWebviewWindow();
   await window.unmaximize();
 }
 
 export async function toggleMaximize() {
   const window = getCurrentWebviewWindow();
   const isMaximized = await window.isMaximized();
   if (isMaximized) {
     await window.unmaximize();
   } else {
     await window.maximize();
   }
 }
 
 export async function closeWindow() {
   const window = getCurrentWebviewWindow();
   await window.close();
 }
 
 export async function setWindowTitle(title: string) {
   const window = getCurrentWebviewWindow();
   await window.setTitle(title);
 }
 
 export async function resizeWindow(width: number, height: number) {
   const window = getCurrentWebviewWindow();
   await window.setSize({ width, height });
 }
 
 export async function centerWindow() {
   const window = getCurrentWebviewWindow();
   await window.center();
 }


全てのウインドウを取得

 import { getAllWebviewWindows } from '@tauri-apps/api/webviewWindow';
 
 async function listAllWindows() {
   const windows = await getAllWebviewWindows();
   for (const win of windows) {
     console.log(`Window: ${await win.label()}`);
   }
 }


特定のウインドウを取得

 import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
 
 async function getSpecificWindow(label: string) {
   try {
     const window = await WebviewWindow.getByLabel(label);
     if (window) {
       console.log(`Found window: ${label}`);
       return window;
     }
   }
   catch (error) {
     console.error(`Window not found: ${label}`);
   }
 
   return null;
 }



動的ウインドウ作成

JavaScript/TypeScript での作成

フロントエンドから動的にウインドウを作成する。

 // src/utils/createWindow.ts
 import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
 
 export async function createSettingsWindow() {
   // 既存のウインドウを確認
   const existing = await WebviewWindow.getByLabel('settings');
   if (existing) {
     // 既に存在する場合はフォーカス
     await existing.setFocus();
     return existing;
   }
 
   // 新しいウインドウを作成
   const webview = new WebviewWindow('settings', {
     url: '/settings',
     title: 'Settings',
     width: 600,
     height: 500,
     center: true,
     resizable: true,
   });
 
   // ウインドウイベントをリッスン
   webview.once('tauri://created', () => {
     console.log('Settings window created');
   });
 
   webview.once('tauri://error', (error) => {
     console.error('Failed to create window:', error);
   });
 
   return webview;
 }
 
 // ドキュメントウインドウを作成
 export async function createDocumentWindow(documentId: string) {
   const label = `document-${documentId}`;
   
   const existing = await WebviewWindow.getByLabel(label);
   if (existing) {
     await existing.setFocus();
     return existing;
   }
 
   const webview = new WebviewWindow(label, {
     url: `/document/${documentId}`,
     title: `Document - ${documentId}`,
     width: 800,
     height: 600,
   });
 
   return webview;
 }


Rustでの作成

Rustバックエンドから動的にウインドウを作成する。

 // src-tauri/src/lib.rs
 use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
 
 #[tauri::command]
 async fn open_settings_window(app: AppHandle) -> Result<(), String> {
    // 既存のウインドウを確認
    if let Some(window) = app.get_webview_window("settings") {
       window.set_focus().map_err(|e| e.to_string())?;
       return Ok(());
    }
 
    // 新しいウインドウを作成
    let _window = WebviewWindowBuilder::new(
       &app,
       "settings",  // ラベル
       WebviewUrl::App("settings.html".into())  // URL
    )
    .title("Settings")
    .inner_size(600.0, 500.0)
    .center()
    .resizable(true)
    .build()
    .map_err(|e| e.to_string())?;
 
    Ok(())
 }
 
 #[tauri::command]
 async fn open_document_window(
     app: AppHandle,
     document_id: String,
 ) -> Result<(), String> {
     let label = format!("document-{}", document_id);
 
    // 既存のウインドウを確認
    if let Some(window) = app.get_webview_window(&label) {
       window.set_focus().map_err(|e| e.to_string())?;
       return Ok(());
    }
 
    let url = format!("/document/{}", document_id);
 
    WebviewWindowBuilder::new(&app, &label, WebviewUrl::App(url.into()))
       .title(format!("Document - {}", document_id))
       .inner_size(800.0, 600.0)
       .build()
       .map_err(|e| e.to_string())?;
 
    Ok(())
 }
 
 #[tauri::command]
 async fn close_window(app: AppHandle, label: String) -> Result<(), String> {
    if let Some(window) = app.get_webview_window(&label) {
       window.close().map_err(|e| e.to_string())?;
    }
    Ok(())
 }


setupフックでのウインドウ作成

アプリケーション起動時にsetupフックでウインドウを作成する。

 use tauri::{Builder, Manager, WebviewUrl, WebviewWindowBuilder};
 
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
    Builder::default()
       .setup(|app| {
          // メインウインドウの設定
          let main_window = app.get_webview_window("main").unwrap();
 
          // 追加のウインドウを作成
          let _dev_tools = WebviewWindowBuilder::new(
             app,
             "dev-tools",
             WebviewUrl::App("dev-tools.html".into())
          )
          .title("Developer Tools")
          .inner_size(400.0, 600.0)
          .visible(false)  // 最初は非表示
          .build()?;
 
          Ok(())
       })
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }



マルチウインドウ

ウインドウ間のデータ共有

状態管理とイベントを使用してウインドウ間でデータを共有する。

 // src-tauri/src/lib.rs
 use std::sync::Mutex;
 use tauri::{AppHandle, Manager, State};
 
 struct SharedData {
    documents: Vec<Document>,
 }
 
 #[tauri::command]
 async fn get_documents(state: State<'_, Mutex<SharedData>>) -> Vec<Document> {
    let data = state.lock().unwrap();
    data.documents.clone()
 }
 
 #[tauri::command]
 async fn update_document(
    app: AppHandle,
    state: State<'_, Mutex<SharedData>>,
    document: Document,
 ) -> Result<(), String> {
    // 状態を更新
    {
       let mut data = state.lock().unwrap();
       // ドキュメントを更新
    }
 
    // 全てのウインドウに通知
    app.emit("document-updated", &document)
       .map_err(|e| e.to_string())?;
 
    Ok(())
 }


ウインドウ間イベント通信

 // src/hooks/useWindowEvents.ts
 import { listen, UnlistenFn } from '@tauri-apps/api/event';
 import { useEffect, useState } from 'react';
 
 interface DocumentUpdatedEvent {
   id: string;
   title: string;
   content: string;
 }
 
 export function useDocumentUpdates() {
   const [documents, setDocuments] = useState<DocumentUpdatedEvent[]>([]);
 
   useEffect(() => {
     let unlisten: UnlistenFn;
 
     const setupListener = async () => {
       unlisten = await listen<DocumentUpdatedEvent>('document-updated', (event) => {
         console.log('Document updated:', event.payload);
         setDocuments(prev => {
           const index = prev.findIndex(d => d.id === event.payload.id);
           if (index >= 0) {
             const updated = [...prev];
             updated[index] = event.payload;
             return updated;
           }
           return [...prev, event.payload];
         });
       });
     };
 
     setupListener();
 
     return () => {
       if (unlisten) {
         unlisten();
       }
     };
   }, []);
 
   return documents;
 }


マルチウインドウのベストプラクティス

  • ウインドウの重複を避ける
    同じラベルのウインドウが既に存在する場合は、新規作成せずフォーカスを移す。
  • リソース管理
    不要になったウインドウは適切にクローズする。
  • 状態の同期
    ウインドウ間で状態が一貫していることを保証する。



ウインドウイベント

Rustでのイベントハンドリング

 use tauri::{Builder, Manager, WindowEvent};
 
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
    Builder::default()
       .on_window_event(|window, event| {
          match event {
             WindowEvent::Resized(size) => {
                println!(
                   "Window {} resized to {}x{}",
                   window.label(),
                   size.width,
                   size.height
                );
             }
             WindowEvent::Moved(position) => {
                println!(
                   "Window {} moved to ({}, {})",
                   window.label(),
                   position.x,
                   position.y
                );
             }
             WindowEvent::CloseRequested { api, .. } => {
                // 閉じる要求をキャンセルして確認ダイアログを表示
                api.prevent_close();
 
                // 確認処理後に閉じる
                // window.close().unwrap();
             }
             WindowEvent::Destroyed => {
                println!("Window {} destroyed", window.label());
             }
             WindowEvent::Focused(focused) => {
                if *focused {
                   println!("Window {} focused", window.label());
                }
                else {
                   println!("Window {} unfocused", window.label());
                }
             }
             WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
                println!("Scale factor changed to {}", scale_factor);
             }
             _ => {}
          }
       })
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }


TypeScriptでのイベントリスナー

 // src/hooks/useWindowEvent.ts
 import { useEffect } from 'react';
 import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
 import { UnlistenFn } from '@tauri-apps/api/event';
 
 export function useWindowEvent(
   eventType: string,
   callback: (payload: unknown) => void
 ) {
   useEffect(() => {
     let unlisten: UnlistenFn | undefined;
 
     const setup = async () => {
       const window = getCurrentWebviewWindow();
 
       // @ts-ignore - ウインドウイベントの型定義
       unlisten = await window.onWindowEvent(eventType, callback);
     };
 
     setup();
 
     return () => {
       if (unlisten) {
         unlisten();
       }
     };
   }, [eventType, callback]);
 }
 
 // 使用例
 function MyComponent() {
   useWindowEvent('resized', (payload) => {
     console.log('Window resized:', payload);
   });
 
   useWindowEvent('focused', (payload) => {
     console.log('Focus changed:', payload);
   });
 
   return <div>Window Event Demo</div>;
 }


閉じる確認ダイアログ

 use tauri::{Builder, Manager, WindowEvent};
 
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
    Builder::default()
       .on_window_event(|window, event| {
          if let WindowEvent::CloseRequested { api, .. } = event {
             // メインウインドウの閉じる要求を処理
             if window.label() == "main" {
                // 閉じるを一時的にキャンセル
                api.prevent_close();
 
                // フロントエンドに確認を依頼
                let _ = window.emit("confirm-close", ());
             }
          }
       })
       .invoke_handler(tauri::generate_handler![confirm_close])
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }
 
 #[tauri::command]
 async fn confirm_close(app: tauri::AppHandle, confirmed: bool) {
    if confirmed {
       // アプリケーションを終了
       app.exit(0);
    }
 }


 // src/components/App.tsx
 import { listen } from '@tauri-apps/api/event';
 import { invoke } from '@tauri-apps/api/core';
 import { useEffect, useState } from 'react';
 
 export function App() {
   const [showConfirmDialog, setShowConfirmDialog] = useState(false);
 
   useEffect(() => {
     let unlisten: (() => void) | undefined;
 
     const setup = async () => {
       unlisten = await listen('confirm-close', () => {
         setShowConfirmDialog(true);
       });
     };
 
     setup();
 
     return () => {
       if (unlisten) unlisten();
     };
   }, []);
 
   const handleClose = async (confirmed: boolean) => {
     setShowConfirmDialog(false);
     await invoke('confirm_close', { confirmed });
   };
 
   return (
     <div>
       {/* アプリケーションコンテンツ */}
 
       {showConfirmDialog && (
         <div className="dialog">
           <p>アプリケーションを終了しますか?</p>
           <button onClick={() => handleClose(true)}>はい</button>
           <button onClick={() => handleClose(false)}>いいえ</button>
         </div>
       )}
     </div>
   );
 }



window-stateプラグインによる状態保持

プラグインのインストール

Cargo.tomlファイルに依存関係を追加する。

 # src-tauri/Cargo.toml
 [dependencies]
 tauri = { version = "2", features = ["macos-private-api"] }
 tauri-plugin-window-state = "2"


プラグインの初期化

 // src-tauri/src/lib.rs
 use tauri::Builder;
 use tauri_plugin_window_state::WindowState;
 
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
    Builder::default()
       .plugin(WindowState::default())
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }


状態の保存と復元

window-stateプラグインは、以下に示すウインドウの状態を自動的に保存・復元する。

  • 位置 (x, y)
  • サイズ (width, height)
  • 最大化状態
  • フルスクリーン状態


 use tauri::Builder;
 use tauri_plugin_window_state::{WindowState, StateFlags};
 
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
    Builder::default()
       .plugin(
          WindowState::default()
          // 保存する状態を指定
          .with_state_flags(
             StateFlags::SIZE
             | StateFlags::POSITION
             | StateFlags::MAXIMIZED
             | StateFlags::FULLSCREEN
          )
       )
       .run(tauri::generate_context!())
       .expect("error while running tauri application");
 }


手動での保存と復元

 use tauri::{AppHandle, Manager};
 use tauri_plugin_window_state::{WindowState, StateFlags};
 
 #[tauri::command]
 async fn save_window_state(app: AppHandle) -> Result<(), String> {
    let state = app.state::<WindowState>();
    state.save(StateFlags::all())
       .map_err(|e| e.to_string())?;
    Ok(())
 }
 
 #[tauri::command]
 async fn restore_window_state(app: AppHandle) -> Result<(), String> {
    let state = app.state::<WindowState>();
    state.restore(StateFlags::all(), Some("main"))
       .map_err(|e| e.to_string())?;
    Ok(())
 }



React + TypeScriptでのサンプルコード

ウインドウ管理カスタムフック

 // src/hooks/useWindowManager.ts
 import { useState, useEffect, useCallback } from 'react';
 import { 
   getCurrentWebviewWindow, 
   WebviewWindow,
   WindowOptions 
 } from '@tauri-apps/api/webviewWindow';
 import { invoke } from '@tauri-apps/api/core';
 
 // ウインドウ情報を表す型
 interface WindowInfo {
   label: string;         // ウインドウの一意識別子
   title: string;         // ウインドウタイトル
   isMaximized: boolean;  // 最大化状態かどうか
   isMinimized: boolean;  // 最小化状態かどうか
   isVisible: boolean;    // 表示状態かどうか
 }
 
 export function useWindowManager() {
   const [currentWindow, setCurrentWindow] = useState<WebviewWindow | null>(null);  // 現在のウインドウインスタンス
   const [windowInfo, setWindowInfo] = useState<WindowInfo | null>(null);           // ウインドウ情報
 
   // 初期化 : 現在のウインドウを取得して情報をセット
   useEffect(() => {
     const init = async () => {
       // 現在のウインドウを取得
       const window = getCurrentWebviewWindow();
       setCurrentWindow(window);
 
       // ウインドウの各種情報を非同期で取得
       const info: WindowInfo = {
         label: window.label,
         title: await window.title(),
         isMaximized: await window.isMaximized(),
         isMinimized: await window.isMinimized(),
         isVisible: await window.isVisible(),
       };
       setWindowInfo(info);
     };
 
     init();
   }, []);
 
   // ウインドウを最小化
   const minimize = useCallback(async () => {
     if (currentWindow) {
       await currentWindow.minimize();
     }
   }, [currentWindow]);
 
   // ウインドウを最大化
   const maximize = useCallback(async () => {
     if (currentWindow) {
       await currentWindow.maximize();
     }
   }, [currentWindow]);
 
   // ウインドウの最大化を解除
   const unmaximize = useCallback(async () => {
     if (currentWindow) {
       await currentWindow.unmaximize();
     }
   }, [currentWindow]);
 
   // ウインドウの最大化をトグル (最大化 / 解除を切り替え)
   const toggleMaximize = useCallback(async () => {
     if (currentWindow) {
       const isMax = await currentWindow.isMaximized();
       if (isMax) {
         await currentWindow.unmaximize();  // 最大化を解除
       } else {
         await currentWindow.maximize();    // 最大化する
       }
     }
   }, [currentWindow]);
 
   // ウインドウを閉じる
   const close = useCallback(async () => {
     if (currentWindow) {
       await currentWindow.close();
     }
   }, [currentWindow]);
 
   // ウインドウタイトルを設定
   const setTitle = useCallback(async (title: string) => {
     if (currentWindow) {
       await currentWindow.setTitle(title);
     }
   }, [currentWindow]);
 
   // 新しいウインドウを作成 (既存の場合はフォーカス)
   const createWindow = useCallback(async (
     label: string,          // ウインドウラベル
     options: WindowOptions  // ウインドウオプション
   ) => {
     // 同じラベルのウインドウが既に存在するか確認
     const existing = await WebviewWindow.getByLabel(label);
     if (existing) {
       await existing.setFocus();  // 既存ウインドウにフォーカス
       return existing;
     }
 
     // 新しいウインドウを作成
     const newWindow = new WebviewWindow(label, options);
     return newWindow;
   }, []);
 
   // 設定ウインドウを開く (Rustコマンド経由)
   const openSettingsWindow = useCallback(async () => {
     await invoke('open_settings_window');
   }, []);
 
   // フックの利用者に公開する値と関数
   return {
     currentWindow,       // 現在のウインドウインスタンス
     windowInfo,          // ウインドウ情報
     minimize,            // 最小化関数
     maximize,            // 最大化関数
     unmaximize,          // 最大化解除関数
     toggleMaximize,      // 最大化トグル関数
     close,               // 閉じる関数
     setTitle,            // タイトル設定関数
     createWindow,        // ウインドウ作成関数
     openSettingsWindow,  // 設定ウインドウを開く関数
   };
 }


カスタムタイトルバーコンポーネント

 // src/components/TitleBar.tsx
 import React from 'react';
 import { useWindowManager } from '../hooks/useWindowManager';
 import './TitleBar.css';
 
 interface TitleBarProps {
   title?: string;
 }
 
 export function TitleBar({ title = 'My Application' }: TitleBarProps) {
   const { minimize, maximize, toggleMaximize, close } = useWindowManager();
   const [isMaximized, setIsMaximized] = React.useState(false);
 
   const handleMaximize = async () => {
     await toggleMaximize();
     setIsMaximized(!isMaximized);
   };
 
   return (
     <div className="titlebar" data-tauri-drag-region>
       <div className="titlebar-title" data-tauri-drag-region>
         {title}
       </div>
       <div className="titlebar-controls">
         <button 
           className="titlebar-button minimize"
           onClick={minimize}
           aria-label="最小化"
         >
           <svg width="12" height="12" viewBox="0 0 12 12">
             <rect y="5" width="12" height="2" fill="currentColor" />
           </svg>
         </button>
         <button 
           className="titlebar-button maximize"
           onClick={handleMaximize}
           aria-label={isMaximized ? '元に戻す' : '最大化'}
         >
           {isMaximized ? (
             <svg width="12" height="12" viewBox="0 0 12 12">
               <rect x="2" y="4" width="8" height="8" fill="none" stroke="currentColor" strokeWidth="1" />
               <rect x="0" y="0" width="8" height="8" fill="var(--bg-color)" stroke="currentColor" strokeWidth="1" />
             </svg>
           ) : (
             <svg width="12" height="12" viewBox="0 0 12 12">
               <rect x="0" y="0" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="1" />
             </svg>
           )}
         </button>
         <button 
           className="titlebar-button close"
           onClick={close}
           aria-label="閉じる"
         >
           <svg width="12" height="12" viewBox="0 0 12 12">
             <line x1="1" y1="1" x2="11" y2="11" stroke="currentColor" strokeWidth="1.5" />
             <line x1="11" y1="1" x2="1" y2="11" stroke="currentColor" strokeWidth="1.5" />
           </svg>
         </button>
       </div>
     </div>
   );
 }


 /* src/components/TitleBar.css */
 .titlebar {
   display: flex;
   justify-content: space-between;
   align-items: center;
   height: 32px;
   background-color: #1e1e1e;
   color: #ffffff;
   user-select: none;
 }
 
 .titlebar-title {
   flex: 1;
   padding-left: 12px;
   font-size: 13px;
 }
 
 .titlebar-controls {
   display: flex;
 }
 
 .titlebar-button {
   width: 46px;
   height: 32px;
   border: none;
   background: transparent;
   color: #ffffff;
   cursor: pointer;
   display: flex;
   align-items: center;
   justify-content: center;
 }
 
 .titlebar-button:hover {
   background-color: #3c3c3c;
 }
 
 .titlebar-button.close:hover {
   background-color: #e81123;
 }



トラブルシューティング

ウインドウが表示されない

  • 原因
    visible: falseが設定されている、または初期化エラーが発生している。
  • 解決方法
     // tauri.conf.json  visible を確認
     {
       "app": {
         "windows": [{
           "label": "main",
           "visible": true
         }]
       }
     }
    


マルチウインドウでイベントが届かない

  • 原因
    イベントのスコープが正しく設定されていない。
  • 解決方法
     // 全てのウインドウにイベントを送信
     app.emit("event-name", &payload)?;
     
     // 特定のウインドウに送信
     app.emit_to("window-label", "event-name", &payload)?;
    


ウインドウサイズが保存されない

  • 原因
    window-stateプラグインが正しく設定されていない。
  • 解決方法
     // プラグインを正しく初期化
     Builder::default()
        .plugin(WindowState::default())
    


クロスプラットフォームでの挙動の違い

  • 原因
    OSごとのウインドウ管理の実装差異。
  • 解決方法
     use tauri::utils::platform;
     
     // プラットフォーム固有の処理
     #[cfg(target_os = "macos")]
     {
        // MacOS固有の処理
     }
     
     #[cfg(target_os = "windows")]
     {
        // Windows固有の処理
     }
     
     #[cfg(target_os = "linux")]
     {
        // Linux固有の処理
     }
    



関連情報