Yang

Tauri 多窗口管理的几种方式

0 views
2 mins

窗口创建

在 Tauri 项目中,实现多窗口管理是一个比较常见的需求,比如我们需要在主窗口中打开一个新的窗口,或者在新窗口中打开一个新的窗口等等。本文将介绍如何在 Tauri 项目中实现多窗口管理, 分别使用 Rust 和 JavaScript 两种方式实现

1.Tauri 配置文件实现

如果是使用 Tauri 官方脚手架创建的项目, 可以直接在 tauri.cong.json 文件中配置 windows 来创建一个新的窗口

tauri.conf.json
 
1
"windows": [
2
{
3
"title": "APP",
4
"width": 1180,
5
"height": 900
6
},
7
{
8
"title": "设置",
9
"width": 600,
10
"height": 800,
11
"label": "setting",
12
"url": "/settings",
13
"center": true,
14
"resizable": false,
15
"visible": true
16
}
17
]

url 配置的地址则是前端项目的 Router 地址, 这里以 Sveltekit 为例, 在 src/routes 目录下创建一个 settings.svelte 文件, 并在 src/app.svelte 中配置路由

CleanShot 2024-07-29 at 16.57.56@2x.png

WindowConfig 配置项可以看这里

  • label : 窗口的唯一标识符, 可以通过该标识符获取窗口实例
  • url :支持两种类型 WindowURL
    • 外部URL
    • 应用程序 URL 的路径部分。例如,要加载 tauri://localhost/settings ,只需设置 url /settings 即可
  • visible 配置的值如果为 true,则应用启动时会自动打开多窗口,反之设置为false则默认隐藏,关于 windows 下更多的配置属性看这里

CleanShot 2024-07-29 at 17.08.08@2x.png

2.运行时通过Rust创建

如果需要在运行时动态创建窗口, 可以通过 Rust 代码来实现, 配置参数与 tauri.conf.json windows 配置项一致 在 Rust main.rs 中添加如下代码

 
1
fn main() {
2
tauri::Builder::default()
3
.setup(|app| {
4
WindowBuilder::new(
5
app,
6
"settings",
7
WindowUrl::External("http://localhost:1420/settings".parse().unwrap()),
8
)
9
.title("设置")
10
.visible(false)
11
.inner_size(600.0, 500.0)
12
.position(550.0, 100.0)
13
.build()?;
14
Ok(())
15
})
16
.run(tauri::generate_context!())
17
.expect("error while running tauri application");
18
}

3. Tauri command

在 Tauri 中, 通过 tauri::command 定义一个创建窗口的命令,并注册到 Tauri 中, 在前端项目中通过 tauri.invoke 来调用该命令

 
1
#[tauri::command]
2
fn greet(name: &str) -> String {
3
format!("Hello, {}! You've been greeted from Rust!", name)
4
}
5
6
#[tauri::command]
7
fn open_settings(app: AppHandle) {
8
WindowBuilder::new(
9
&app,
10
"settings",
11
WindowUrl::External("http://localhost:1420/settings".parse().unwrap()),
12
)
13
.title("设置")
14
.inner_size(1400.0, 800.0)
15
.build()
16
.expect("Failed to create window");
17
}
18
19
fn main() {
20
tauri::Builder::default()
21
.invoke_handler(tauri::generate_handler![open_settings])
22
.run(tauri::generate_context!())
23
.expect("error while running tauri application");
24
}

前端通过 invoke 调用该命令

 
1
import { invoke } from '@tauri-apps/api/tauri';
2
3
async function openSettings() {
4
await invoke('open_settings', { name });
5
}

4.使用JSAPI创建

需要首先配置 tauri.conf.json 文件, 添加 allowlist 配置项, 开启创建窗口的权限

 
1
{
2
"tauri": {
3
"allowlist": {
4
"window": {
5
"create": true
6
}
7
}
8
}
9
}

否则创建窗口时会报错: window > create' not in the allowlist (https://tauri.app/docs/api/config#tauri.allowlist)

然后使用 WebviewWindow 类来创建窗口

 
1
import { WebviewWindow } from '@tauri-apps/api/window';
2
3
function onCreateWindow() {
4
const webview = new WebviewWindow('settings', {
5
url: '/settings',
6
title: '设置',
7
width: 800,
8
height: 600,
9
x: 100,
10
y: 100,
11
});
12
13
webview.once('tauri://created', () => {
14
console.log('窗口创建成功');
15
});
16
17
webview.once('tauri://error', (error) => {
18
console.log('窗口创建失败', error);
19
});
20
}

通过 WebviewWindow API 关闭窗口

 
1
import { WebviewWindow } from '@tauri-apps/api/window';
2
3
function onCloseWindow() {
4
// 获取窗口的引用
5
const window = WebviewWindow.getByLabel('settings');
6
window?.close();
7
}

设置窗口默认展示行为

再通过 tauri.conf.json 配置或者 Rust 代码创建窗口时, 可以通过 visible 属性来设置窗口的默认展示行为, 默认情况下,当是同窗口顶部操作栏手动关闭一个窗口时,窗口会被销毁。这就导致了我们通过前端代码打开的设置窗口,如果手动关闭后,窗口被销毁,再次打开时就无法打开了。 为了能够在关闭设置窗口后可以继续打开,有两种方式可以实现,Tauri 提供了一种机制,可以在窗口关闭事件触发时将其隐藏,而不是销毁窗口。 使用 WebviewWindow API 创建的窗口在关闭后还可以重新创建

 
1
fn main() {
2
tauri::Builder::default()
3
.setup(|app| {
4
let app_handle = app.handle();
5
let window = app_handle.get_window("setting").unwrap();
6
window.on_window_event(move |event| match event {
7
WindowEvent::CloseRequested { api, .. } => {
8
// 取消默认的关闭行为
9
api.prevent_close();
10
// 隐藏窗口而不是关闭
11
let window = app_handle.get_window("setting").unwrap();
12
window.hide().unwrap();
13
}
14
_ => {}
15
});
16
Ok(())
17
})
18
.run(tauri::generate_context!())
19
.expect("error while running tauri application");
20
}

再次通过前端代码打开设置窗口,然后点击设置窗口的关闭按钮,设置窗口消失,再次打开正常。

如果你在 windows 配置了 decorations 值为 false (隐藏顶部栏和边框),并且使用了自定义的顶部栏,则可以通过自定义关闭按钮来设置关闭行为为隐藏

 
1
import { appWindow } from '@tauri-apps/api/window';
2
3
function onCloseWindow() {
4
appWindow.hide().catch((e) => console.error('Failed to hide window:', e));
5
}

appWindow.hide() 是隐藏当前窗口(即调用该方法所在的窗口)。如果你有多个窗口,并且想要在特定窗口上执行操作(例如隐藏某个特定窗口)。

打开窗口

对于使用 tauri.cong.json 或者 Rust 代码在初始化时创建并设置默认隐藏的窗口, 可以通过 invoke event 事件的方式来打开打开窗口

1.使用 invoke 打开窗口

首先定义 invoke

 
1
#[command]
2
pub fn open_setting(app_handle: tauri::AppHandle) {
3
if let Some(window) = app_handle.get_window("setting") {
4
window.show().unwrap();
5
window.set_focus().unwrap();
6
}
7
}

前端调用 invoke 打开窗口

 
1
import { invoke } from '@tauri-apps/api';
2
3
async function onOpenSetting() {
4
try {
5
await invoke('open_setting');
6
} catch (error) {
7
console.error('error:', error);
8
}
9
}

2.Event 事件触发

rust 中定义事件监听

 
1
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
2
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3
4
mod api;
5
mod app;
6
7
use app::invoke;
8
use app::window;
9
use tauri::Manager;
10
use tauri_plugin_store::Builder as windowStorePlugin;
11
12
fn main() {
13
let tauri_app = tauri::Builder::default().setup(|app| {
14
// 设置事件监听
15
let app_handle = app.handle();
16
let app_handle_clone = app_handle.clone();
17
tauri::async_runtime::spawn(async move {
18
// 这里通过监听前端的 emit 事件来触发窗口的打开
19
app_handle_clone.listen_global("open_setting", move |_event| {
20
let window = app_handle.get_window("setting").unwrap();
21
window.show().unwrap();
22
window.set_focus().unwrap();
23
});
24
});
25
26
Ok(())
27
});
28
29
tauri_app
30
.run(tauri::generate_context!())
31
.expect("error while running tauri application");
32
}

前端调用 emit 触发事件

 
1
import { emit } from '@tauri-apps/api/event';
2
3
async function showSettingsWindow() {
4
await emit('open_setting');
5
}

  • invoke 方法用于前端直接调用 Rust 后端的命令,并等待结果。它适用于需要获得立即响应或需要进行数据交互的场景。 - emit 方法是一次性的单向 IPC 消息,用于前端向后端发送事件,不要求立即响应。它适用于需要异步处理或广播消息的场景。更多请查看 - Inter-Process Communication | Tauri Apps

获取窗口实例

1.使用 JS API 获取窗口实例

 
1
import { WebviewWindow } from '@tauri-apps/api/window';
2
3
function onCloseWindow() {
4
// 获取窗口的引用
5
const window = WebviewWindow.getByLabel('settings');
6
window?.close();
7
}

2.使用Rust 获取

 
1
#[tauri::command]
2
fn get_window(app_handle: tauri::AppHandle, label: String) -> Result<(), String> {
3
if let Some(window) = app_handle.get_window(&label) {
4
println!("window: {:?}", window);
5
window.close();
6
}
7
Ok(())
8
}

当要获取的窗口存在时, 前端使用 invoke 调用 get_window 会获取到窗口实例, 并调用 close 方法关闭窗口

窗口间通信

在 Tauri 中窗口之间通信有几种场景和方式,一种是主进程与窗口之间的相互通信,另一种是窗口与窗口之间的相互通信.

不推荐直接进行窗口与窗口之间的通信,而是通过主进程设置事件监听的方式进行窗口之间的事件转发,通过主进程全局管理所有窗口和事件,更符合 Tauri 的设计理念和安全模型,确保各窗口之间的通信是有序和受控的。

CleanShot 2024-08-13 at 09.43.54@2x.png

上图中描述了Event 的处理方式,Tauri 应用中有三个独立的窗口 主窗口 Main 、设置窗口 Settings 以及 About 窗口,需求是要在这三个窗口之间进行 Event 通信,例如从 Main 发送事件到 Settings 或者从 Settings 发送事件到 About

Rust 主进程设置 Event 事件转发:

main.rs
 
1
/**
2
* 发送到Main窗口
3
*/
4
pub fn event_listener_to_main(app_handle: AppHandle) {
5
let app = app_handle.clone();
6
app.listen_global("EVENT_SEND_TO_MAIN", move |event| {
7
if let Some(payload) = event.payload() {
8
match serde_json::from_str::<Value>(payload) {
9
Ok(parsed_payload) => {
10
if let Some(window) = app_handle.get_window("main") {
11
window.emit("EVENT_TO_MAIN", parsed_payload).unwrap();
12
} else {
13
println!("Main window not found");
14
}
15
}
16
Err(e) => println!("Failed to parse payload: {:?}", e),
17
}
18
} else {
19
println!("No payload found in event");
20
}
21
});
22
}

event_listener_to_main 方法定义了向 main 窗口发送事件的方式,监听了全局 Event EVENT_SEND_TO_MAIN , 当收到消息时,会获取 mian 窗口, main 窗口存在时,向其广播 EVENT_TO_MAIN 事件,这样避免了向其他两个窗口广播事件。 同理, Settings 窗口与 About 窗口也需要设置事件监听转发

 
1
pub fn event_listener_to_settings(app_handle: AppHandle) {
2
let app = app_handle.clone();
3
app.listen_global("EVENT_SEND_TO_SETTINGS", move |event| {
4
if let Some(payload) = event.payload() {
5
match serde_json::from_str::<Value>(payload) {
6
Ok(parsed_payload) => {
7
if let Some(window) = app_handle.get_window("settings") {
8
window.emit("EVENT_TO_SETTINGS", parsed_payload).unwrap();
9
} else {
10
println!("Main window not found");
11
}
12
}
13
Err(e) => println!("Failed to parse payload: {:?}", e),
14
}
15
} else {
16
println!("No payload found in event");
17
}
18
});
19
}

Main Settings 等窗口需要监听当前窗口的事件, 例如 Main 窗口需要监听 EVENT_SEND_TO_MAIN Settings 需要监听 EVENT_SEND_TO_SETTINGS 。 前端进行事件调用时,可以在 payload 参数里额外自定义一个 Event 类型,用于对事件进行细分处理,Event 参数如下

 
1
type EventPayload<T extends any> = {
2
type: string;
3
payload: T;
4
}

调用 event 方法

 
1
event.emit('EVENT_SEND_TO_MAIN', {
2
type: 'GET_USER_INFO',
3
payload: { id }
4
})

参考资料