Skip to content

Tauri 窗口管理与高级通信技巧

前言

在前面的文章中,我们已经了解了 Tauri 的基本概念和架构设计。但要开发真正实用的桌面应用,掌握窗口管理和通信机制是必不可少的。

我记得开发第一个多窗口 Tauri 应用时,遇到了各种问题:窗口之间如何通信?如何创建子窗口?如何实现无边框窗口?后来通过不断实践和探索,才逐渐掌握了这些技巧。

今天,我就把这些实战经验分享给大家,让你少走弯路。

窗口管理基础

创建和管理窗口

在配置文件中定义窗口

最简单的方式是在 tauri.conf.json 中定义:

json
{
  "tauri": {
    "windows": [
      {
        "label": "main",                     // 窗口标识符
        "title": "Main Window",              // 窗口标题
        "url": "index.html",                 // 窗口加载的页面
        "width": 800,
        "height": 600,
        "center": true,
        "resizable": true,
        "fullscreen": false
      },
      {
        "label": "settings",
        "title": "Settings",
        "url": "settings.html",
        "width": 400,
        "height": 500,
        "visible": false                     // 初始隐藏
      }
    ]
  }
}

动态创建窗口

在运行时创建新窗口:

rust
// 后端创建窗口
use tauri::{Manager, WindowBuilder, WindowUrl};

#[tauri::command]
fn open_settings_window(app: tauri::AppHandle) -> Result<(), String> {
    let settings_window = WindowBuilder::new(
        &app,
        "settings",                          // 窗口标识符
        WindowUrl::App("settings.html".into())
    )
    .title("Settings")
    .inner_size(400.0, 500.0)
    .center()
    .resizable(false)
    .build()
    .map_err(|e| e.to_string())?;

    Ok(())
}
typescript
// 前端创建窗口
import { WebviewWindow } from '@tauri-apps/api/window';

async function openSettingsWindow() {
    const settingsWindow = new WebviewWindow('settings', {
        url: 'settings.html',
        title: 'Settings',
        width: 400,
        height: 500,
        center: true,
        resizable: false
    });

    // 等待窗口加载完成
    settingsWindow.once('tauri://created', () => {
        console.log('Settings window created');
    });

    // 监听窗口加载错误
    settingsWindow.once('tauri://error', (e) => {
        console.error('Error creating window:', e);
    });
}

窗口操作

获取窗口实例

rust
// Rust 端
use tauri::Manager;

#[tauri::command]
fn manipulate_window(app: tauri::AppHandle) {
    // 通过标签获取窗口
    if let Some(window) = app.get_window("main") {
        // 窗口操作
        window.set_title("New Title").unwrap();
        window.center().unwrap();
    }
}
typescript
// 前端
import { appWindow, WebviewWindow } from '@tauri-apps/api/window';

// 获取当前窗口
const currentWindow = appWindow;

// 获取其他窗口
const mainWindow = WebviewWindow.getByLabel('main');
const settingsWindow = WebviewWindow.getByLabel('settings');

常用窗口操作

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

// 显示/隐藏
await appWindow.show();
await appWindow.hide();

// 最大化/最小化
await appWindow.maximize();
await appWindow.minimize();
await appWindow.unmaximize();

// 全屏
await appWindow.setFullscreen(true);

// 设置大小
await appWindow.setSize({ width: 800, height: 600 });

// 设置位置
await appWindow.setPosition({ x: 100, y: 100 });

// 设置标题
await appWindow.setTitle('New Title');

// 置顶
await appWindow.setAlwaysOnTop(true);

// 设置是否可调整大小
await appWindow.setResizable(false);

// 设置是否显示装饰(边框、标题栏)
await appWindow.setDecorations(false);

// 关闭窗口
await appWindow.close();

窗口状态查询

typescript
// 获取窗口信息
const isMaximized = await appWindow.isMaximized();
const isFullscreen = await appWindow.isFullscreen();
const isVisible = await appWindow.isVisible();
const isFocused = await appWindow.isFocused();

// 获取窗口尺寸和位置
const size = await appWindow.innerSize();
const position = await appWindow.innerPosition();
const outerSize = await appWindow.outerSize();
const outerPosition = await appWindow.outerPosition();

console.log('Window size:', size.width, 'x', size.height);
console.log('Window position:', position.x, ',', position.y);

窗口间通信

方式一:事件系统(推荐)

全局事件(所有窗口都能接收)

typescript
// 窗口 A - 发送事件
import { emit } from '@tauri-apps/api/event';

await emit('user-login', {
    userId: 123,
    username: 'john_doe',
    timestamp: Date.now()
});

// 窗口 B - 接收事件
import { listen } from '@tauri-apps/api/event';

const unlisten = await listen('user-login', (event) => {
    console.log('User logged in:', event.payload);
    // payload: { userId: 123, username: 'john_doe', timestamp: ... }
});

// 不需要时取消监听
unlisten();

窗口特定事件

typescript
// 发送事件到特定窗口
import { WebviewWindow } from '@tauri-apps/api/window';

const settingsWindow = WebviewWindow.getByLabel('settings');
await settingsWindow.emit('update-config', {
    theme: 'dark',
    language: 'zh-CN'
});

// 接收来自特定窗口的事件
import { appWindow } from '@tauri-apps/api/window';

await appWindow.listen('request-data', (event) => {
    console.log('Data requested from:', event.windowLabel);
    console.log('Data:', event.payload);
});

后端作为中转

rust
use tauri::Manager;

#[tauri::command]
async fn broadcast_message(
    app: tauri::AppHandle,
    message: String
) -> Result<(), String> {
    // 向所有窗口广播消息
    app.emit_all("broadcast", message)
        .map_err(|e| e.to_string())?;
    
    Ok(())
}

#[tauri::command]
async fn send_to_window(
    app: tauri::AppHandle,
    window_label: String,
    event: String,
    payload: String
) -> Result<(), String> {
    // 发送到特定窗口
    if let Some(window) = app.get_window(&window_label) {
        window.emit(&event, payload)
            .map_err(|e| e.to_string())?;
    }
    
    Ok(())
}
typescript
// 使用后端中转
import { invoke } from '@tauri-apps/api/tauri';

// 广播消息
await invoke('broadcast_message', {
    message: 'Hello all windows!'
});

// 发送到特定窗口
await invoke('send_to_window', {
    windowLabel: 'settings',
    event: 'update-theme',
    payload: JSON.stringify({ theme: 'dark' })
});

方式二:共享状态

使用 Tauri Store 插件

bash
# 安装
npm install @tauri-apps/plugin-store
rust
// Cargo.toml
[dependencies]
tauri-plugin-store = "0.1"

// main.rs
fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_store::Builder::default().build())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
typescript
// 窗口 A - 设置数据
import { Store } from '@tauri-apps/plugin-store';

const store = new Store('.settings.dat');

await store.set('user', {
    id: 123,
    name: 'John Doe'
});

await store.set('theme', 'dark');

// 保存到磁盘
await store.save();

// 窗口 B - 读取数据
const user = await store.get('user');
const theme = await store.get('theme');

// 监听变化
await store.onChange((key, value) => {
    console.log(`${key} changed to:`, value);
});

使用后端状态管理

rust
use std::sync::Mutex;
use tauri::State;
use serde::{Serialize, Deserialize};

#[derive(Default, Serialize, Deserialize, Clone)]
struct AppConfig {
    theme: String,
    language: String,
    font_size: u32,
}

struct AppState {
    config: Mutex<AppConfig>,
}

#[tauri::command]
fn get_config(state: State<AppState>) -> AppConfig {
    state.config.lock().unwrap().clone()
}

#[tauri::command]
fn update_config(
    state: State<AppState>,
    app: tauri::AppHandle,
    config: AppConfig
) -> Result<(), String> {
    *state.config.lock().unwrap() = config.clone();
    
    // 通知所有窗口配置已更新
    app.emit_all("config-updated", config)
        .map_err(|e| e.to_string())?;
    
    Ok(())
}

fn main() {
    tauri::Builder::default()
        .manage(AppState {
            config: Mutex::new(AppConfig::default()),
        })
        .invoke_handler(tauri::generate_handler![
            get_config,
            update_config
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
typescript
// 任何窗口都可以获取和更新配置
interface AppConfig {
    theme: string;
    language: string;
    fontSize: number;
}

// 获取配置
const config = await invoke<AppConfig>('get_config');

// 更新配置
await invoke('update_config', {
    config: {
        theme: 'dark',
        language: 'zh-CN',
        fontSize: 14
    }
});

// 监听配置变化
await listen<AppConfig>('config-updated', (event) => {
    console.log('Config updated:', event.payload);
    // 更新 UI
});

高级窗口技巧

无边框窗口 + 自定义标题栏

配置窗口

json
{
  "tauri": {
    "windows": [
      {
        "decorations": false,               // 移除系统标题栏
        "transparent": true                  // 可选:窗口透明
      }
    ]
  }
}

实现自定义标题栏

tsx
// CustomTitleBar.tsx
import React from 'react';
import { appWindow } from '@tauri-apps/api/window';
import './CustomTitleBar.css';

export function CustomTitleBar() {
    const minimize = () => appWindow.minimize();
    const maximize = () => appWindow.toggleMaximize();
    const close = () => appWindow.close();

    return (
        <div className="titlebar" data-tauri-drag-region>
            <div className="titlebar-title">My App</div>
            <div className="titlebar-buttons">
                <button className="titlebar-button" onClick={minimize}>

                </button>
                <button className="titlebar-button" onClick={maximize}>

                </button>
                <button className="titlebar-button close" onClick={close}>
                    ×
                </button>
            </div>
        </div>
    );
}
css
/* CustomTitleBar.css */
.titlebar {
    height: 32px;
    background: #2c2c2c;
    color: white;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 10px;
    user-select: none;
}

.titlebar-title {
    font-size: 14px;
    flex: 1;
}

.titlebar-buttons {
    display: flex;
    gap: 4px;
}

.titlebar-button {
    width: 30px;
    height: 30px;
    border: none;
    background: transparent;
    color: white;
    cursor: pointer;
    font-size: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.titlebar-button:hover {
    background: rgba(255, 255, 255, 0.1);
}

.titlebar-button.close:hover {
    background: #e81123;
}

关键属性 data-tauri-drag-region

  • 添加到元素上使其可以拖动窗口
  • 在无边框窗口中必需

系统托盘

配置托盘

json
{
  "tauri": {
    "systemTray": {
      "iconPath": "icons/icon.png",
      "iconAsTemplate": true                // macOS: 使用模板图标
    }
  }
}

实现托盘功能

rust
use tauri::{
    CustomMenuItem, SystemTray, SystemTrayMenu, SystemTrayEvent,
    SystemTrayMenuItem, Manager
};

fn main() {
    // 创建托盘菜单
    let show = CustomMenuItem::new("show".to_string(), "Show");
    let hide = CustomMenuItem::new("hide".to_string(), "Hide");
    let quit = CustomMenuItem::new("quit".to_string(), "Quit");
    
    let tray_menu = SystemTrayMenu::new()
        .add_item(show)
        .add_item(hide)
        .add_native_item(SystemTrayMenuItem::Separator)
        .add_item(quit);
    
    let system_tray = SystemTray::new().with_menu(tray_menu);

    tauri::Builder::default()
        .system_tray(system_tray)
        .on_system_tray_event(|app, event| match event {
            SystemTrayEvent::LeftClick {
                position: _,
                size: _,
                ..
            } => {
                println!("System tray received a left click");
            }
            SystemTrayEvent::RightClick {
                position: _,
                size: _,
                ..
            } => {
                println!("System tray received a right click");
            }
            SystemTrayEvent::DoubleClick {
                position: _,
                size: _,
                ..
            } => {
                println!("System tray received a double click");
            }
            SystemTrayEvent::MenuItemClick { id, .. } => {
                let window = app.get_window("main").unwrap();
                match id.as_str() {
                    "show" => {
                        window.show().unwrap();
                        window.set_focus().unwrap();
                    }
                    "hide" => {
                        window.hide().unwrap();
                    }
                    "quit" => {
                        std::process::exit(0);
                    }
                    _ => {}
                }
            }
            _ => {}
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

动态更新托盘

rust
#[tauri::command]
fn update_tray_menu(app: tauri::AppHandle) -> Result<(), String> {
    let tray_handle = app.tray_handle();
    
    // 更新菜单项
    tray_handle
        .get_item("show")
        .set_title("显示窗口")
        .map_err(|e| e.to_string())?;
    
    // 更新图标
    // tray_handle.set_icon(...);
    
    Ok(())
}

模态对话框窗口

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

async function openModal() {
    const modal = new WebviewWindow('modal', {
        url: 'modal.html',
        title: 'Modal Dialog',
        width: 400,
        height: 300,
        center: true,
        resizable: false,
        alwaysOnTop: true,                   // 置顶
        skipTaskbar: true,                   // 不在任务栏显示
        parent: 'main'                       // 设置父窗口
    });

    // 等待模态窗口关闭
    modal.once('tauri://destroyed', () => {
        console.log('Modal closed');
    });
}

启动画面(Splash Screen)

rust
use tauri::Manager;
use std::time::Duration;

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let splashscreen_window = app.get_window("splashscreen").unwrap();
            let main_window = app.get_window("main").unwrap();
            
            // 隐藏主窗口
            main_window.hide().unwrap();
            
            // 模拟初始化过程
            let app_handle = app.handle();
            tauri::async_runtime::spawn(async move {
                // 执行初始化任务
                std::thread::sleep(Duration::from_secs(3));
                
                // 关闭启动画面,显示主窗口
                splashscreen_window.close().unwrap();
                main_window.show().unwrap();
                main_window.set_focus().unwrap();
            });
            
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
json
{
  "tauri": {
    "windows": [
      {
        "label": "splashscreen",
        "url": "splashscreen.html",
        "width": 400,
        "height": 300,
        "decorations": false,
        "transparent": true,
        "center": true,
        "alwaysOnTop": true
      },
      {
        "label": "main",
        "url": "index.html",
        "visible": false
      }
    ]
  }
}

实战案例:多窗口应用

让我们实现一个完整的多窗口应用,包括:

  • 主窗口
  • 设置窗口
  • 关于窗口
  • 窗口间通信

后端实现

rust
use tauri::{Manager, State, WindowBuilder, WindowUrl};
use std::sync::Mutex;
use serde::{Serialize, Deserialize};

#[derive(Default, Clone, Serialize, Deserialize)]
struct Settings {
    theme: String,
    language: String,
}

struct AppState {
    settings: Mutex<Settings>,
}

#[tauri::command]
fn get_settings(state: State<AppState>) -> Settings {
    state.settings.lock().unwrap().clone()
}

#[tauri::command]
fn save_settings(
    state: State<AppState>,
    app: tauri::AppHandle,
    settings: Settings
) -> Result<(), String> {
    *state.settings.lock().unwrap() = settings.clone();
    
    // 通知所有窗口设置已更新
    app.emit_all("settings-updated", &settings)
        .map_err(|e| e.to_string())?;
    
    Ok(())
}

#[tauri::command]
fn open_settings_window(app: tauri::AppHandle) -> Result<(), String> {
    // 检查窗口是否已存在
    if let Some(window) = app.get_window("settings") {
        window.show().map_err(|e| e.to_string())?;
        window.set_focus().map_err(|e| e.to_string())?;
        return Ok(());
    }
    
    // 创建新窗口
    WindowBuilder::new(
        &app,
        "settings",
        WindowUrl::App("settings.html".into())
    )
    .title("Settings")
    .inner_size(500.0, 400.0)
    .center()
    .resizable(false)
    .build()
    .map_err(|e| e.to_string())?;
    
    Ok(())
}

#[tauri::command]
fn open_about_window(app: tauri::AppHandle) -> Result<(), String> {
    if let Some(window) = app.get_window("about") {
        window.show().map_err(|e| e.to_string())?;
        window.set_focus().map_err(|e| e.to_string())?;
        return Ok(());
    }
    
    WindowBuilder::new(
        &app,
        "about",
        WindowUrl::App("about.html".into())
    )
    .title("About")
    .inner_size(400.0, 300.0)
    .center()
    .resizable(false)
    .build()
    .map_err(|e| e.to_string())?;
    
    Ok(())
}

fn main() {
    tauri::Builder::default()
        .manage(AppState {
            settings: Mutex::new(Settings::default()),
        })
        .invoke_handler(tauri::generate_handler![
            get_settings,
            save_settings,
            open_settings_window,
            open_about_window
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

前端实现

主窗口

tsx
// MainWindow.tsx
import { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/tauri';
import { listen } from '@tauri-apps/api/event';

interface Settings {
    theme: string;
    language: string;
}

function MainWindow() {
    const [settings, setSettings] = useState<Settings | null>(null);

    useEffect(() => {
        // 加载设置
        loadSettings();

        // 监听设置更新
        const unlisten = listen<Settings>('settings-updated', (event) => {
            setSettings(event.payload);
            applyTheme(event.payload.theme);
        });

        return () => {
            unlisten.then(fn => fn());
        };
    }, []);

    const loadSettings = async () => {
        const s = await invoke<Settings>('get_settings');
        setSettings(s);
        applyTheme(s.theme);
    };

    const applyTheme = (theme: string) => {
        document.body.className = theme;
    };

    const openSettings = () => {
        invoke('open_settings_window');
    };

    const openAbout = () => {
        invoke('open_about_window');
    };

    return (
        <div className="main-window">
            <h1>Main Window</h1>
            <p>Current Theme: {settings?.theme}</p>
            <p>Language: {settings?.language}</p>
            
            <button onClick={openSettings}>Settings</button>
            <button onClick={openAbout}>About</button>
        </div>
    );
}

设置窗口

tsx
// SettingsWindow.tsx
import { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/tauri';
import { appWindow } from '@tauri-apps/api/window';

interface Settings {
    theme: string;
    language: string;
}

function SettingsWindow() {
    const [theme, setTheme] = useState('light');
    const [language, setLanguage] = useState('en');

    useEffect(() => {
        loadSettings();
    }, []);

    const loadSettings = async () => {
        const settings = await invoke<Settings>('get_settings');
        setTheme(settings.theme);
        setLanguage(settings.language);
    };

    const handleSave = async () => {
        await invoke('save_settings', {
            settings: { theme, language }
        });
        appWindow.close();
    };

    return (
        <div className="settings-window">
            <h2>Settings</h2>
            
            <div className="setting-item">
                <label>Theme:</label>
                <select value={theme} onChange={(e) => setTheme(e.target.value)}>
                    <option value="light">Light</option>
                    <option value="dark">Dark</option>
                </select>
            </div>
            
            <div className="setting-item">
                <label>Language:</label>
                <select value={language} onChange={(e) => setLanguage(e.target.value)}>
                    <option value="en">English</option>
                    <option value="zh-CN">中文</option>
                </select>
            </div>
            
            <div className="buttons">
                <button onClick={handleSave}>Save</button>
                <button onClick={() => appWindow.close()}>Cancel</button>
            </div>
        </div>
    );
}

性能优化建议

1. 窗口懒加载

rust
// 不要一开始就创建所有窗口,按需创建
#[tauri::command]
fn open_heavy_window(app: tauri::AppHandle) -> Result<(), String> {
    // 只在需要时创建
    if app.get_window("heavy").is_none() {
        WindowBuilder::new(&app, "heavy", WindowUrl::App("heavy.html".into()))
            .build()
            .map_err(|e| e.to_string())?;
    }
    Ok(())
}

2. 事件清理

typescript
// 组件卸载时清理事件监听
useEffect(() => {
    const setupListeners = async () => {
        const unlisten1 = await listen('event1', handler1);
        const unlisten2 = await listen('event2', handler2);
        
        return () => {
            unlisten1();
            unlisten2();
        };
    };
    
    let cleanup: (() => void) | undefined;
    setupListeners().then(fn => cleanup = fn);
    
    return () => cleanup?.();
}, []);

3. 批量更新

rust
// 避免频繁发送事件,使用批量更新
use std::time::Duration;
use tokio::time::sleep;

#[tauri::command]
async fn batch_updates(app: tauri::AppHandle) {
    let mut buffer = Vec::new();
    
    for i in 0..100 {
        buffer.push(i);
        
        // 每10个数据发送一次
        if buffer.len() >= 10 {
            app.emit_all("batch-data", &buffer).unwrap();
            buffer.clear();
            sleep(Duration::from_millis(100)).await;
        }
    }
}

调试技巧

窗口调试

typescript
// 打印所有窗口信息
import { getAllWebviewWindows } from '@tauri-apps/api/window';

async function debugWindows() {
    const windows = await getAllWebviewWindows();
    for (const window of windows) {
        console.log('Window:', window.label);
        console.log('  - Visible:', await window.isVisible());
        console.log('  - Size:', await window.innerSize());
        console.log('  - Position:', await window.innerPosition());
    }
}

事件调试

typescript
// 监听所有事件
import { listen } from '@tauri-apps/api/event';

await listen('*', (event) => {
    console.log('Event:', event.event);
    console.log('Payload:', event.payload);
    console.log('Window:', event.windowLabel);
});

总结

这篇文章我们深入学习了 Tauri 的窗口管理和通信机制:

✅ 核心知识点

  1. 窗口管理

    • 静态配置和动态创建
    • 窗口操作和状态查询
    • 生命周期管理
  2. 窗口间通信

    • 事件系统
    • 共享状态
    • 后端中转
  3. 高级技巧

    • 无边框窗口
    • 自定义标题栏
    • 系统托盘
    • 模态对话框
    • 启动画面
  4. 性能优化

    • 懒加载
    • 事件清理
    • 批量更新

🎯 实战建议

  • 合理规划窗口结构
  • 选择合适的通信方式
  • 注意事件监听清理
  • 做好错误处理

下一篇文章,我们将通过一个完整的 Todo 应用,把所学知识串联起来,实战演练。


相关文章推荐:

有问题欢迎留言讨论,我会及时解答!