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-storerust
// 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 的窗口管理和通信机制:
✅ 核心知识点
窗口管理
- 静态配置和动态创建
- 窗口操作和状态查询
- 生命周期管理
窗口间通信
- 事件系统
- 共享状态
- 后端中转
高级技巧
- 无边框窗口
- 自定义标题栏
- 系统托盘
- 模态对话框
- 启动画面
性能优化
- 懒加载
- 事件清理
- 批量更新
🎯 实战建议
- 合理规划窗口结构
- 选择合适的通信方式
- 注意事件监听清理
- 做好错误处理
下一篇文章,我们将通过一个完整的 Todo 应用,把所学知识串联起来,实战演练。
相关文章推荐:
有问题欢迎留言讨论,我会及时解答!