Tauri 中的多窗口管理
窗口创建
在 Tauri 项目中,实现多窗口管理是一个比较常见的需求,比如我们需要在主窗口中打开一个新的窗口,或者在新窗口中打开一个新的窗口等等。本文将介绍如何在 Tauri 项目中实现多窗口管理, 分别使用 Rust 和 JavaScript 两种方式实现
1.Tauri 配置文件实现
如果是使用 Tauri 官方脚手架创建的项目,可以直接在 tauri.cong.json
文件中配置 windows
来创建一个新的窗口
"windows": [
{
"title": "APP",
"width": 1180,
"height": 900
},
{
"title": "设置",
"width": 600,
"height": 800,
"label": "setting",
"url": "/settings",
"center": true,
"resizable": false,
"visible": true
}
]
url
配置的地址则是前端项目的 Router 地址,这里以 Sveltekit 为例,在 src/routes
目录下创建一个 settings.svelte
文件,并在 src/app.svelte
中配置路由
WindowConfig
配置项可以看这里
label
: 窗口的唯一标识符,可以通过该标识符获取窗口实例url
:支持两种类型 WindowURL- 外部 URL
- 应用程序 URL 的路径部分。例如,要加载
tauri://localhost/settings
,只需设置url
为/settings
即可
visible
配置的值如果为 true,则应用启动时会自动打开多窗口,反之设置为 false 则默认隐藏,关于 windows 下更多的配置属性看这里
2.运行时通过 Rust 创建
如果需要在运行时动态创建窗口,可以通过 Rust 代码来实现,配置参数与 tauri.conf.json
中 windows
配置项一致
在 Rust
main.rs 中添加如下代码
fn main() {
tauri::Builder::default()
.setup(|app| {
WindowBuilder::new(
app,
"settings",
WindowUrl::External("http://localhost:1420/settings".parse().unwrap()),
)
.title("设置")
.visible(false)
.inner_size(600.0, 500.0)
.position(550.0, 100.0)
.build()?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
3. Tauri command
在 Tauri 中,通过 tauri::command
定义一个创建窗口的命令,并注册到 Tauri 中,在前端项目中通过 tauri.invoke
来调用该命令
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[tauri::command]
fn open_settings(app: AppHandle) {
WindowBuilder::new(
&app,
"settings",
WindowUrl::External("http://localhost:1420/settings".parse().unwrap()),
)
.title("设置")
.inner_size(1400.0, 800.0)
.build()
.expect("Failed to create window");
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![open_settings])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
前端通过 invoke
调用该命令
import { invoke } from '@tauri-apps/api/tauri';
async function openSettings() {
await invoke('open_settings', { name });
}
4.使用 JSAPI 创建
需要首先配置 tauri.conf.json
文件,添加 allowlist
配置项,开启创建窗口的权限
{
"tauri": {
"allowlist": {
"window": {
"create": true
}
}
}
}
否则创建窗口时会报错:
window > create' not in the allowlist (https://tauri.app/docs/api/config#tauri.allowlist)
然后使用 WebviewWindow
类来创建窗口
import { WebviewWindow } from '@tauri-apps/api/window';
function onCreateWindow() {
const webview = new WebviewWindow('settings', {
url: '/settings',
title: '设置',
width: 800,
height: 600,
x: 100,
y: 100
});
webview.once('tauri://created', () => {
console.log('窗口创建成功');
});
webview.once('tauri://error', (error) => {
console.log('窗口创建失败', error);
});
}
通过 WebviewWindow
API 关闭窗口
import { WebviewWindow } from '@tauri-apps/api/window';
function onCloseWindow() {
// 获取窗口的引用
const window = WebviewWindow.getByLabel('settings');
window?.close();
}
设置窗口默认展示行为
再通过 tauri.conf.json
配置或者 Rust
代码创建窗口时,可以通过 visible
属性来设置窗口的默认展示行为,默认情况下,当是同窗口顶部操作栏手动关闭一个窗口时,窗口会被销毁。这就导致了我们通过前端代码打开的设置窗口,如果手动关闭后,窗口被销毁,再次打开时就无法打开了。
为了能够在关闭设置窗口后可以继续打开,有两种方式可以实现,Tauri 提供了一种机制,可以在窗口关闭事件触发时将其隐藏,而不是销毁窗口。
使用 WebviewWindow
API 创建的窗口在关闭后还可以重新创建
fn main() {
tauri::Builder::default()
.setup(|app| {
let app_handle = app.handle();
let window = app_handle.get_window("setting").unwrap();
window.on_window_event(move |event| match event {
WindowEvent::CloseRequested { api, .. } => {
// 取消默认的关闭行为
api.prevent_close();
// 隐藏窗口而不是关闭
let window = app_handle.get_window("setting").unwrap();
window.hide().unwrap();
}
_ => {}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
再次通过前端代码打开设置窗口,然后点击设置窗口的关闭按钮,设置窗口消失,再次打开正常。
import { appWindow } from '@tauri-apps/api/window';
function onCloseWindow() {
appWindow.hide().catch((e) => console.error('Failed to hide window:', e));
}
appWindow.hide()
是隐藏当前窗口(即调用该方法所在的窗口)。如果你有多个窗口,并且想要在特定窗口上执行操作(例如隐藏某个特定窗口)。
打开窗口
对于使用 tauri.cong.json
或者 Rust
代码在初始化时创建并设置默认隐藏的窗口,可以通过 invoke
和 event
事件的方式来打开打开窗口
1.使用 invoke 打开窗口
首先定义 invoke
#[command]
pub fn open_setting(app_handle: tauri::AppHandle) {
if let Some(window) = app_handle.get_window("setting") {
window.show().unwrap();
window.set_focus().unwrap();
}
}
前端调用 invoke
打开窗口
import { invoke } from '@tauri-apps/api';
async function onOpenSetting() {
try {
await invoke('open_setting');
} catch (error) {
console.error('error:', error);
}
}
2.Event 事件触发
rust
中定义事件监听
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod api;
mod app;
use app::invoke;
use app::window;
use tauri::Manager;
use tauri_plugin_store::Builder as windowStorePlugin;
fn main() {
let tauri_app = tauri::Builder::default().setup(|app| {
// 设置事件监听
let app_handle = app.handle();
let app_handle_clone = app_handle.clone();
tauri::async_runtime::spawn(async move {
// 这里通过监听前端的 emit 事件来触发窗口的打开
app_handle_clone.listen_global("open_setting", move |_event| {
let window = app_handle.get_window("setting").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
});
});
Ok(())
});
tauri_app
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
前端调用 emit
触发事件
import { emit } from '@tauri-apps/api/event';
async function showSettingsWindow() {
await emit('open_setting');
}
invoke
方法用于前端直接调用 Rust 后端的命令,并等待结果。它适用于需要获得立即响应或需要进行数据交互的场景。emit
方法是一次性的单向 IPC 消息,用于前端向后端发送事件,不要求立即响应。它适用于需要异步处理或广播消息的场景。更多请查看 - Inter-Process Communication | Tauri Apps
获取窗口实例
1.使用 JS API 获取窗口实例
import { WebviewWindow } from '@tauri-apps/api/window';
function onCloseWindow() {
// 获取窗口的引用
const window = WebviewWindow.getByLabel('settings');
window?.close();
}
2.使用 Rust 获取
#[tauri::command]
fn get_window(app_handle: tauri::AppHandle, label: String) -> Result<(), String> {
if let Some(window) = app_handle.get_window(&label) {
println!("window: {:?}", window);
window.close();
}
Ok(())
}
当要获取的窗口存在时,前端使用 invoke
调用 get_window
会获取到窗口实例,并调用 close 方法关闭窗口
窗口间通信
在 Tauri 中窗口之间通信有几种场景和方式,一种是主进程与窗口之间的相互通信,另一种是窗口与窗口之间的相互通信。
不推荐直接进行窗口与窗口之间的通信,而是通过主进程设置事件监听的方式进行窗口之间的事件转发,通过主进程全局管理所有窗口和事件,更符合 Tauri 的设计理念和安全模型,确保各窗口之间的通信是有序和受控的。
上图中描述了 Event 的处理方式,Tauri 应用中有三个独立的窗口 主窗口 Main
、设置窗口 Settings
以及 About
窗口,需求是要在这三个窗口之间进行 Event 通信,例如从 Main
发送事件到 Settings
或者从 Settings
发送事件到 About
。
在 Rust
主进程设置 Event 事件转发:
// main.rs
/**
* 发送到 Main 窗口
*/
pub fn event_listener_to_main(app_handle: AppHandle) {
let app = app_handle.clone();
app.listen_global("EVENT_SEND_TO_MAIN", move |event| {
if let Some(payload) = event.payload() {
match serde_json::from_str::<Value>(payload) {
Ok(parsed_payload) => {
if let Some(window) = app_handle.get_window("main") {
window.emit("EVENT_TO_MAIN", parsed_payload).unwrap();
} else {
println!("Main window not found");
}
}
Err(e) => println!("Failed to parse payload: {:?}", e),
}
} else {
println!("No payload found in event");
}
});
}
event_listener_to_main
方法定义了向 main
窗口发送事件的方式,监听了全局 Event EVENT_SEND_TO_MAIN
, 当收到消息时,会获取 mian
窗口,main
窗口存在时,向其广播 EVENT_TO_MAIN
事件,这样避免了向其他两个窗口广播事件。
同理,Settings
窗口与 About
窗口也需要设置事件监听转发
pub fn event_listener_to_settings(app_handle: AppHandle) {
let app = app_handle.clone();
app.listen_global("EVENT_SEND_TO_SETTINGS", move |event| {
if let Some(payload) = event.payload() {
match serde_json::from_str::<Value>(payload) {
Ok(parsed_payload) => {
if let Some(window) = app_handle.get_window("settings") {
window.emit("EVENT_TO_SETTINGS", parsed_payload).unwrap();
} else {
println!("Main window not found");
}
}
Err(e) => println!("Failed to parse payload: {:?}", e),
}
} else {
println!("No payload found in event");
}
});
}
Main
和 Settings
等窗口需要监听当前窗口的事件,例如 Main 窗口需要监听 EVENT_SEND_TO_MAIN
,Settings
需要监听 EVENT_SEND_TO_SETTINGS
。
前端进行事件调用时,可以在 payload
参数里额外自定义一个 Event 类型,用于对事件进行细分处理,Event 参数如下
type EventPayload<T extends any> = {
type: string;
payload: T;
};
调用 event
方法
event.emit('EVENT_SEND_TO_MAIN', {
type: 'GET_USER_INFO',
payload: { id }
});
Multiwindow
Tauri 2.0rc版本开始支持版本多窗口管理,可以在一个Window中打开多个 WebviewWindow, PR, 目前 Tauri 发布了2.0版本,但是依然通过指定 feature 来开启多窗口支持,官方仓库Demo, 通过执行 cargo run --example multiwebview --features unstable
来启动多窗口示例
如果你想在自己的项目中使用多窗口,需要在 Cargo.toml
文件中添加 tauri
依赖
tauri = { version = "2", features = ["unstable"] }
创建一个打开多窗口的示例
use tauri::{command, Manager};
use tauri::{LogicalPosition, LogicalSize, WebviewUrl};
const COLLECTOR_URL: &str = "https://hoholi.com/";
const WEBVIEW_NAME: &str = "collector_view";
#[command]
pub fn create_collector_view(app_handle: tauri::AppHandle) {
let _main = app_handle.get_window("main").unwrap();
let main_size = _main.outer_size().unwrap();
_main
.add_child(
tauri::webview::WebviewBuilder::new(
WEBVIEW_NAME,
WebviewUrl::External(COLLECTOR_URL.parse().unwrap()),
)
.auto_resize(),
LogicalPosition::new(80., 100.),
LogicalSize::new(main_size.width as f64 / 2., main_size.height as f64 - 200.),
)
.unwrap();
}
#[command]
pub fn close_collector_view(app_handle: tauri::AppHandle) {
let _main = app_handle.get_window("main").unwrap();
let collector_view = _main.get_webview_window(WEBVIEW_NAME).unwrap();
collector_view.close().unwrap();
}
#[command]
pub fn hide_collector_view(app_handle: tauri::AppHandle) {
let _main = app_handle.get_window("main").unwrap();
let collector_view = _main.get_webview_window(WEBVIEW_NAME).unwrap();
collector_view.hide().unwrap();
}