Tauri でマルチウィンドウアプリを実現する方法

デスクトップアプリケーション開発において、複数のウィンドウを扱うことは多くの開発者が直面する課題です。Tauri は、Web 技術を使ってネイティブアプリを構築できるフレームワークですが、マルチウィンドウ機能の実装には独特のアプローチが必要になります。
この記事では、Tauri でマルチウィンドウアプリを構築するための実践的な手順を、実際のエラーと解決策とともに詳しく解説していきます。初心者の方でも理解しやすいよう、段階的に進めていきましょう。
Tauri マルチウィンドウの基本概念
Tauri では、ウィンドウは「メインウィンドウ」と「子ウィンドウ」という概念で管理されます。メインウィンドウはアプリケーションの起動時に作成され、子ウィンドウは必要に応じて動的に作成できます。
ウィンドウの種類と特徴
種類 | 特徴 | 用途 |
---|---|---|
メインウィンドウ | アプリ起動時に自動作成 | アプリのメイン画面 |
子ウィンドウ | 動的に作成可能 | モーダル、設定画面、詳細表示 |
Tauri のウィンドウ管理は、Rust のバックエンドとフロントエンド(HTML/CSS/JavaScript)の連携によって実現されます。この仕組みを理解することで、より柔軟なアプリケーション設計が可能になります。
開発環境の準備
まず、Tauri の開発環境を整備しましょう。必要なツールとセットアップ手順を確認します。
必要なツールのインストール
bash# Rustのインストール(未インストールの場合)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Node.jsの確認(v16以上推奨)
node --version
# Yarnのインストール
npm install -g yarn
# Tauri CLIのインストール
yarn add -g @tauri-apps/cli
プロジェクトの作成
bash# 新しいTauriプロジェクトを作成
yarn create tauri-app my-multi-window-app
# プロジェクトディレクトリに移動
cd my-multi-window-app
# 依存関係のインストール
yarn install
よくあるエラーと解決策
エラー 1: tauri: command not found
bash# エラーの原因:Tauri CLIがインストールされていない
# 解決策:グローバルインストールを実行
yarn add -g @tauri-apps/cli
エラー 2: error: linker 'cc' not found
bash# エラーの原因:Cコンパイラが不足
# 解決策:Xcode Command Line Toolsをインストール(macOS)
xcode-select --install
メインウィンドウの設定
メインウィンドウは、アプリケーションの顔となる重要な要素です。適切な設定により、ユーザーエクスペリエンスを大幅に向上させることができます。
tauri.conf.json の基本設定
json{
"tauri": {
"windows": [
{
"label": "main",
"title": "マルチウィンドウアプリ",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false,
"visible": true
}
]
}
}
ウィンドウの詳細設定
json{
"tauri": {
"windows": [
{
"label": "main",
"title": "メインウィンドウ",
"width": 1024,
"height": 768,
"minWidth": 400,
"minHeight": 300,
"maxWidth": 1920,
"maxHeight": 1080,
"center": true,
"decorations": true,
"transparent": false,
"alwaysOnTop": false,
"skipTaskbar": false
}
]
}
}
ウィンドウの初期化処理
typescript// src/main.ts
import { app, BrowserWindow } from '@tauri-apps/api/window';
// メインウィンドウの作成
const mainWindow = new BrowserWindow('main');
// ウィンドウが準備完了したときの処理
mainWindow.once('tauri://created', () => {
console.log('メインウィンドウが作成されました');
});
// ウィンドウが閉じられたときの処理
mainWindow.once('tauri://close-requested', () => {
console.log('メインウィンドウが閉じられようとしています');
});
子ウィンドウの作成方法
子ウィンドウの作成は、Tauri マルチウィンドウ機能の核心部分です。適切な実装により、ユーザーの作業効率を大幅に向上させることができます。
基本的な子ウィンドウ作成
typescript// src/windows/child-window.ts
import { WebviewWindow } from '@tauri-apps/api/window';
// 子ウィンドウを作成する関数
async function createChildWindow() {
try {
const childWindow = new WebviewWindow('child', {
url: 'child.html',
title: '子ウィンドウ',
width: 600,
height: 400,
resizable: true,
center: true,
});
return childWindow;
} catch (error) {
console.error(
'子ウィンドウの作成に失敗しました:',
error
);
throw error;
}
}
ウィンドウの種類別作成方法
typescript// モーダルウィンドウの作成
async function createModalWindow() {
const modalWindow = new WebviewWindow('modal', {
url: 'modal.html',
title: 'モーダル',
width: 400,
height: 300,
resizable: false,
center: true,
alwaysOnTop: true,
decorations: false,
});
return modalWindow;
}
// 設定ウィンドウの作成
async function createSettingsWindow() {
const settingsWindow = new WebviewWindow('settings', {
url: 'settings.html',
title: '設定',
width: 800,
height: 600,
resizable: true,
center: true,
minWidth: 600,
minHeight: 400,
});
return settingsWindow;
}
よくあるエラーと解決策
エラー 3: Window label must be unique
typescript// エラーの原因:同じラベルのウィンドウが既に存在
// 解決策:ウィンドウの存在確認を行う
async function createUniqueWindow(label: string) {
const existingWindow = WebviewWindow.getByLabel(label);
if (existingWindow) {
// 既存のウィンドウをフォーカス
await existingWindow.setFocus();
return existingWindow;
}
// 新しいウィンドウを作成
return new WebviewWindow(label, {
url: `${label}.html`,
title: label,
width: 600,
height: 400,
});
}
エラー 4: Failed to load resource: net::ERR_FILE_NOT_FOUND
typescript// エラーの原因:HTMLファイルが見つからない
// 解決策:正しいパスを指定する
const childWindow = new WebviewWindow('child', {
url: '/src/pages/child.html', // 正しいパスを指定
title: '子ウィンドウ',
});
ウィンドウ間通信の実装
ウィンドウ間でデータをやり取りすることは、マルチウィンドウアプリの重要な機能です。Tauri では、イベントシステムとコマンドシステムを活用して実現します。
イベントベースの通信
typescript// src/communication/events.ts
import { emit, listen } from '@tauri-apps/api/event';
// イベントを送信する関数
export async function sendMessageToAllWindows(
message: string
) {
try {
await emit('window-message', {
message,
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error('メッセージの送信に失敗しました:', error);
}
}
// イベントを受信する関数
export async function listenToMessages() {
try {
await listen('window-message', (event) => {
console.log('受信したメッセージ:', event.payload);
// メッセージの処理
handleMessage(event.payload);
});
} catch (error) {
console.error('メッセージの受信に失敗しました:', error);
}
}
コマンドベースの通信
typescript// src/communication/commands.ts
import { invoke } from '@tauri-apps/api/tauri';
// メインウィンドウから子ウィンドウにデータを送信
export async function sendDataToChild(data: any) {
try {
const result = await invoke('send_data_to_child', {
windowLabel: 'child',
data: data,
});
return result;
} catch (error) {
console.error('データの送信に失敗しました:', error);
throw error;
}
}
// 子ウィンドウからメインウィンドウにデータを送信
export async function sendDataToMain(data: any) {
try {
const result = await invoke('send_data_to_main', {
data: data,
});
return result;
} catch (error) {
console.error('データの送信に失敗しました:', error);
throw error;
}
}
Rust 側でのコマンド実装
rust// src-tauri/src/main.rs
use tauri::{Manager, Window};
#[tauri::command]
async fn send_data_to_child(
window: Window,
window_label: String,
data: serde_json::Value,
) -> Result<(), String> {
// 指定されたウィンドウにイベントを送信
window
.emit_to(&window_label, "data-received", data)
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
async fn send_data_to_main(
window: Window,
data: serde_json::Value,
) -> Result<(), String> {
// メインウィンドウにイベントを送信
window
.emit("data-from-child", data)
.map_err(|e| e.to_string())?;
Ok(())
}
よくあるエラーと解決策
エラー 5: Event listener not found
typescript// エラーの原因:イベントリスナーが正しく設定されていない
// 解決策:アプリケーション起動時にリスナーを設定
document.addEventListener('DOMContentLoaded', async () => {
try {
await listenToMessages();
console.log('イベントリスナーが設定されました');
} catch (error) {
console.error(
'イベントリスナーの設定に失敗しました:',
error
);
}
});
ウィンドウ管理のベストプラクティス
効率的なウィンドウ管理は、ユーザーエクスペリエンスとアプリケーションのパフォーマンスに直結します。実践的なベストプラクティスを紹介します。
ウィンドウの状態管理
typescript// src/windows/window-manager.ts
import {
WebviewWindow,
getAll,
} from '@tauri-apps/api/window';
class WindowManager {
private windows: Map<string, WebviewWindow> = new Map();
// ウィンドウを登録
async registerWindow(
label: string,
window: WebviewWindow
) {
this.windows.set(label, window);
// ウィンドウが閉じられたときの処理
window.once('tauri://close-requested', () => {
this.windows.delete(label);
});
}
// ウィンドウの存在確認
hasWindow(label: string): boolean {
return this.windows.has(label);
}
// ウィンドウを取得
getWindow(label: string): WebviewWindow | undefined {
return this.windows.get(label);
}
// 全ウィンドウを閉じる
async closeAllWindows() {
for (const [label, window] of this.windows) {
try {
await window.close();
} catch (error) {
console.error(
`${label}ウィンドウの閉じる処理に失敗しました:`,
error
);
}
}
this.windows.clear();
}
}
export const windowManager = new WindowManager();
メモリ管理の最適化
typescript// src/windows/memory-management.ts
import { WebviewWindow } from '@tauri-apps/api/window';
// ウィンドウのメモリ使用量を監視
export async function monitorWindowMemory(
window: WebviewWindow
) {
// 定期的にメモリ使用量をチェック
setInterval(async () => {
try {
// ウィンドウが非表示の場合は最小化
const isVisible = await window.isVisible();
if (!isVisible) {
await window.minimize();
}
} catch (error) {
console.error('メモリ監視エラー:', error);
}
}, 30000); // 30秒ごとにチェック
}
// 非アクティブウィンドウの最適化
export async function optimizeInactiveWindows() {
const allWindows = await WebviewWindow.getAll();
for (const window of allWindows) {
try {
const isFocused = await window.isFocused();
if (!isFocused) {
// 非アクティブウィンドウの処理を軽量化
await window.evaluate(() => {
// アニメーションや重い処理を一時停止
document.body.style.animationPlayState = 'paused';
});
}
} catch (error) {
console.error('ウィンドウ最適化エラー:', error);
}
}
}
エラーハンドリングの実装
typescript// src/windows/error-handling.ts
import { WebviewWindow } from '@tauri-apps/api/window';
// ウィンドウ作成時のエラーハンドリング
export async function createWindowWithErrorHandling(
label: string,
options: any
): Promise<WebviewWindow> {
try {
const window = new WebviewWindow(label, options);
// エラーイベントの監視
window.once('tauri://error', (error) => {
console.error(`ウィンドウエラー (${label}):`, error);
// エラー通知の表示
showErrorMessage(
`ウィンドウの作成に失敗しました: ${error}`
);
});
return window;
} catch (error) {
console.error('ウィンドウ作成エラー:', error);
throw new Error(
`ウィンドウの作成に失敗しました: ${error}`
);
}
}
// エラーメッセージの表示
function showErrorMessage(message: string) {
// ユーザーフレンドリーなエラー表示
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
document.body.appendChild(errorDiv);
// 3秒後に自動削除
setTimeout(() => {
errorDiv.remove();
}, 3000);
}
実装例:シンプルなマルチウィンドウアプリ
実際のアプリケーション例を通じて、学んだ知識を実践的に活用してみましょう。シンプルなメモアプリを例に、マルチウィンドウ機能を実装します。
プロジェクト構造
cssmy-multi-window-app/
├── src/
│ ├── main.ts
│ ├── windows/
│ │ ├── window-manager.ts
│ │ └── child-window.ts
│ ├── communication/
│ │ └── events.ts
│ └── pages/
│ ├── main.html
│ ├── child.html
│ └── modal.html
├── src-tauri/
│ └── src/
│ └── main.rs
└── tauri.conf.json
メインウィンドウの実装
html<!-- src/pages/main.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>マルチウィンドウメモアプリ</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana,
sans-serif;
margin: 20px;
background: linear-gradient(
135deg,
#667eea 0%,
#764ba2 100%
);
color: white;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.button {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
}
.button:hover {
background: rgba(255, 255, 255, 0.3);
}
.memo-list {
margin-top: 20px;
}
.memo-item {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
margin: 10px 0;
border-radius: 8px;
border-left: 4px solid #4caf50;
}
</style>
</head>
<body>
<div class="container">
<h1>📝 マルチウィンドウメモアプリ</h1>
<div class="controls">
<button class="button" onclick="createMemoWindow()">
📄 新しいメモ
</button>
<button
class="button"
onclick="createSettingsWindow()"
>
⚙️ 設定
</button>
<button class="button" onclick="showHelp()">
❓ ヘルプ
</button>
</div>
<div id="memo-list" class="memo-list">
<h3>保存されたメモ</h3>
<div id="memos"></div>
</div>
</div>
<script type="module" src="../main.ts"></script>
</body>
</html>
メインロジックの実装
typescript// src/main.ts
import { WebviewWindow } from '@tauri-apps/api/window';
import { emit, listen } from '@tauri-apps/api/event';
import { windowManager } from './windows/window-manager';
// グローバル関数の定義
declare global {
interface Window {
createMemoWindow: () => void;
createSettingsWindow: () => void;
showHelp: () => void;
}
}
// メモウィンドウの作成
window.createMemoWindow = async () => {
try {
if (windowManager.hasWindow('memo')) {
const existingWindow =
windowManager.getWindow('memo');
await existingWindow?.setFocus();
return;
}
const memoWindow = new WebviewWindow('memo', {
url: '/src/pages/child.html',
title: '新しいメモ',
width: 600,
height: 500,
resizable: true,
center: true,
});
await windowManager.registerWindow('memo', memoWindow);
// メモ保存イベントの監視
await listen('memo-saved', (event) => {
addMemoToList(event.payload);
});
} catch (error) {
console.error(
'メモウィンドウの作成に失敗しました:',
error
);
showError('メモウィンドウの作成に失敗しました');
}
};
// 設定ウィンドウの作成
window.createSettingsWindow = async () => {
try {
if (windowManager.hasWindow('settings')) {
const existingWindow =
windowManager.getWindow('settings');
await existingWindow?.setFocus();
return;
}
const settingsWindow = new WebviewWindow('settings', {
url: '/src/pages/modal.html',
title: '設定',
width: 400,
height: 300,
resizable: false,
center: true,
alwaysOnTop: true,
});
await windowManager.registerWindow(
'settings',
settingsWindow
);
} catch (error) {
console.error(
'設定ウィンドウの作成に失敗しました:',
error
);
showError('設定ウィンドウの作成に失敗しました');
}
};
// ヘルプの表示
window.showHelp = () => {
showInfo(
'マルチウィンドウメモアプリの使い方',
'新しいメモボタンでメモウィンドウを開き、設定ボタンで設定を変更できます。'
);
};
// メモリストへの追加
function addMemoToList(memo: any) {
const memosContainer = document.getElementById('memos');
if (!memosContainer) return;
const memoElement = document.createElement('div');
memoElement.className = 'memo-item';
memoElement.innerHTML = `
<h4>${memo.title}</h4>
<p>${memo.content.substring(0, 100)}...</p>
<small>作成日時: ${new Date(
memo.timestamp
).toLocaleString()}</small>
`;
memosContainer.appendChild(memoElement);
}
// エラー表示
function showError(message: string) {
alert(`エラー: ${message}`);
}
// 情報表示
function showInfo(title: string, message: string) {
alert(`${title}\n\n${message}`);
}
// アプリケーション初期化
document.addEventListener('DOMContentLoaded', async () => {
console.log('メインウィンドウが初期化されました');
// ウィンドウ間通信の設定
await listen('window-message', (event) => {
console.log(
'他のウィンドウからのメッセージ:',
event.payload
);
});
});
子ウィンドウの実装
html<!-- src/pages/child.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>メモエディタ</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana,
sans-serif;
margin: 20px;
background: #f5f5f5;
}
.editor {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
input[type='text'] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
margin-bottom: 15px;
}
textarea {
width: 100%;
height: 300px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
resize: vertical;
}
.button {
background: #4caf50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
.button:hover {
background: #45a049;
}
.button.secondary {
background: #f44336;
}
.button.secondary:hover {
background: #da190b;
}
</style>
</head>
<body>
<div class="editor">
<h2>📝 メモエディタ</h2>
<input
type="text"
id="memo-title"
placeholder="メモのタイトルを入力してください"
/>
<textarea
id="memo-content"
placeholder="メモの内容を入力してください"
></textarea>
<div class="controls">
<button class="button" onclick="saveMemo()">
💾 保存
</button>
<button
class="button secondary"
onclick="closeWindow()"
>
❌ 閉じる
</button>
</div>
</div>
<script type="module" src="../child-window.ts"></script>
</body>
</html>
子ウィンドウのロジック
typescript// src/child-window.ts
import { emit } from '@tauri-apps/api/event';
// メモの保存
window.saveMemo = async () => {
const titleInput = document.getElementById(
'memo-title'
) as HTMLInputElement;
const contentInput = document.getElementById(
'memo-content'
) as HTMLTextAreaElement;
const title = titleInput.value.trim();
const content = contentInput.value.trim();
if (!title || !content) {
alert('タイトルと内容を入力してください');
return;
}
try {
const memo = {
title,
content,
timestamp: new Date().toISOString(),
};
// メインウィンドウにメモを送信
await emit('memo-saved', memo);
alert('メモが保存されました!');
// フォームをクリア
titleInput.value = '';
contentInput.value = '';
} catch (error) {
console.error('メモの保存に失敗しました:', error);
alert('メモの保存に失敗しました');
}
};
// ウィンドウを閉じる
window.closeWindow = async () => {
try {
const { close } = await import(
'@tauri-apps/api/window'
);
await close();
} catch (error) {
console.error(
'ウィンドウの閉じる処理に失敗しました:',
error
);
}
};
// グローバル関数の定義
declare global {
interface Window {
saveMemo: () => void;
closeWindow: () => void;
}
}
// 初期化処理
document.addEventListener('DOMContentLoaded', () => {
console.log('メモエディタが初期化されました');
// 自動保存機能(5分ごと)
setInterval(() => {
const titleInput = document.getElementById(
'memo-title'
) as HTMLInputElement;
const contentInput = document.getElementById(
'memo-content'
) as HTMLTextAreaElement;
if (titleInput.value || contentInput.value) {
console.log('自動保存中...');
// ここでローカルストレージに保存することも可能
}
}, 300000); // 5分 = 300,000ミリ秒
});
まとめ
Tauri でマルチウィンドウアプリを実現する方法について、実践的な手順を詳しく解説してきました。
学んだポイント
-
ウィンドウの基本概念: メインウィンドウと子ウィンドウの違いを理解し、適切に使い分けることが重要です。
-
開発環境の準備: 必要なツールのインストールと、よくあるエラーの解決方法を身につけました。
-
ウィンドウ作成の実装: 基本的なウィンドウ作成から、高度なカスタマイズまで実装できるようになりました。
-
ウィンドウ間通信: イベントベースとコマンドベースの通信方法を理解し、実装できるようになりました。
-
ベストプラクティス: ウィンドウ管理、メモリ最適化、エラーハンドリングの実践的な方法を学びました。
今後の発展
この記事で学んだ知識を基に、さらに高度なマルチウィンドウアプリケーションを開発することができます。例えば:
- ドラッグ&ドロップによるウィンドウ間データ転送
- ウィンドウのレイアウト保存と復元
- 複数モニター対応
- ウィンドウのアニメーション効果
Tauri のマルチウィンドウ機能を活用することで、ユーザーにとって使いやすく、効率的なデスクトップアプリケーションを構築できます。この記事が、あなたのアプリケーション開発の一助となれば幸いです。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来