Tauri 中的多窗口管理

Published on 22 February 2024
12 min read
Rust
Tauri
Tauri 中的多窗口管理

窗口创建

2024-11-16更新:Tauri 多窗口支持,在一个窗口中打开多个 WebviewWindow

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

1.Tauri 配置文件实现

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

json
"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 下更多的配置属性看这里

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

2.运行时通过 Rust 创建

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

rust
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 来调用该命令

rust
#[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 调用该命令

javascript
import { invoke } from '@tauri-apps/api/tauri';

async function openSettings() {
	await invoke('open_settings', { name });
}

4.使用 JSAPI 创建

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

json
{
	"tauri": {
		"allowlist": {
			"window": {
				"create": true
			}
		}
	}
}

否则创建窗口时会报错:

window > create' not in the allowlist (https://tauri.app/docs/api/config#tauri.allowlist)

然后使用 WebviewWindow 类来创建窗口

typescript
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 关闭窗口

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

function onCloseWindow() {
	// 获取窗口的引用
	const window = WebviewWindow.getByLabel('settings');
	window?.close();
}

设置窗口默认展示行为

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

rust
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");
}

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

如果你在 windows 配置了 `decorations` 值为 `false`(隐藏顶部栏和边框),并且使用了自定义的顶部栏,则可以通过自定义关闭按钮来设置关闭行为为隐藏
typescript
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 代码在初始化时创建并设置默认隐藏的窗口,可以通过 invokeevent 事件的方式来打开打开窗口

1.使用 invoke 打开窗口

首先定义 invoke

rust
#[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 打开窗口

rust
import { invoke } from '@tauri-apps/api';

async function onOpenSetting() {
	try {
		await invoke('open_setting');
	} catch (error) {
		console.error('error:', error);
	}
}

2.Event 事件触发

rust 中定义事件监听

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 触发事件

javascript
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 获取窗口实例

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

function onCloseWindow() {
	// 获取窗口的引用
	const window = WebviewWindow.getByLabel('settings');
	window?.close();
}

2.使用 Rust 获取

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 的设计理念和安全模型,确保各窗口之间的通信是有序和受控的。

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

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

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

rust
// 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 窗口也需要设置事件监听转发

rust
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");
        }
    });
}

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

typescript
type EventPayload<T extends any> = {
	type: string;
	payload: T;
};

调用 event 方法

typescript
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 依赖

toml
tauri = { version = "2", features = ["unstable"] }

创建一个打开多窗口的示例

rust
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();
}

参考资料