窗口管理:创建、多窗口与装饰

桌面应用与网页的一大区别是窗口本身是可编程对象:创建、销毁、最大化、置顶、定制标题栏都需要代码控制。Tauri 2.x 中窗口概念由 WebviewWindow 统一表示,本篇讲多窗口管理、窗口间通信与无边框窗口的实现。

一、静态与动态两种创建方式

静态窗口在 tauri.conf.json 中声明,应用启动即创建:

{
  "app": {
    "windows": [
      { "label": "main", "title": "主窗口", "width": 900, "height": 600 },
      { "label": "settings", "title": "设置", "width": 400, "height": 300, "visible": false }
    ]
  }
}

label 是窗口的唯一标识,后续所有 API 都靠它定位。动态创建用 Rust 代码:

use tauri::WebviewWindowBuilder;

#[tauri::command]
fn open_docs(app: tauri::AppHandle) -> Result<(), String> {
    WebviewWindowBuilder::new(
        &app,
        "docs",
        tauri::WebviewUrl::App("index.html".into()),
    )
    .title("文档")
    .inner_size(800.0, 600.0)
    .build()
    .map_err(|e| e.to_string())?;
    Ok(())
}
方式 适用场景
配置声明 启动即需要的固定窗口
代码创建 按需弹出的子窗口、详情窗口
JS API 创建 纯前端逻辑驱动的窗口

也可以用 JS 端的 WebviewWindow API 创建,适合前端完全主导 UI 的项目,API 形态以官方文档为准。

二、多窗口与窗口间通信

多窗口共享同一个 Rust 核心进程,窗口间通信天然走 Rust 中转或事件系统。常用模式是在一个窗口里 emit,另一个窗口监听。Rust 端也可以按 label 定向发送:

use tauri::{AppHandle, Emitter};

fn focus_main(app: &AppHandle) {
    if let Some(window) = app.get_webview_window("main") {
        let _ = window.emit("switch-tab", "settings");
    }
}

多窗口开发的常见坑是重复创建:相同 label 的窗口已存在时再次 build 会报错,创建前先 get_webview_window 判空。状态共享建议放在 Rust 端统一管理,见「全局状态:tauri::State 与共享数据」。

三、常用窗口属性与控制

窗口运行时操作集中在 WebviewWindow 类型上:

操作 方法
显示与隐藏 show、hide
最大化 maximize、unmaximize
最小化 minimize
置顶 set_always_on_top
全屏 set_fullscreen
标题 set_title
居中 center

前端也有对应 JS API,例如 getCurrentWindow().maximize(),适合做自定义标题栏按钮。

四、无边框与自定义标题栏

把窗口设为无边框后,需要自己实现拖拽与窗口按钮。关键是为拖拽区域添加 data-tauri-drag-region 属性:

<template>
  <div class="titlebar" data-tauri-drag-region>
    <span>我的应用</span>
    <button @click="minimize">-</button>
    <button @click="close">×</button>
  </div>
</template>

<script setup>
import { getCurrentWindow } from '@tauri-apps/api/window'

const minimize = () => getCurrentWindow().minimize()
const close = () => getCurrentWindow().close()
</script>

配合 decorations: false 与 transparent 可以做出各种异形窗口、悬浮小组件。需要注意 Windows 上无边框窗口的阴影、圆角与系统按钮需要自行处理,不同平台表现不一致,建议在目标平台实测。

五、窗口生命周期

窗口创建与销毁可以挂回调,做初始化与资源清理。setup 钩子里可以拿到所有窗口并注入逻辑;on_window_event 处理关闭、聚焦等事件:

use tauri::{WindowEvent};

fn main() {
    tauri::Builder::default()
        .on_window_event(|window, event| {
            if let WindowEvent::CloseRequested { .. } = event {
                println!("窗口关闭:{}", window.label());
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

托盘与窗口联动是常见需求,比如关闭窗口时只隐藏、从托盘再恢复,详见「系统托盘、菜单与全局快捷键」。多窗口架构设计与消息流排查,可以借鉴 Obsidian 指南 里的多视图通信思路,Rust 端类型基础见 Rust 入门,用 AI 辅助生成窗口管理样板代码的方式见 Claude Code 指南。

六、窗口体验的细节打磨

窗口交互的细节直接决定应用的「原生感」。以下这些点值得在发布前逐项检查:

检查项 目标
恢复窗口位置 记住上次的位置与尺寸
单实例保护 重复打开时聚焦已有窗口
深浅色标题栏 跟随系统主题
最小尺寸 防止窗口缩到不可用
高 DPI 缩放 不同分辨率下文字清晰

窗口位置记忆是典型实现:把位置尺寸存到应用数据目录,启动时读取恢复,代码涉及 fs 插件与 State,分别见「官方插件清单:fs、dialog、shell 与 notification」和「全局状态:tauri::State 与共享数据」。

单实例保护在 Windows 与 macOS 体验差异明显:不做处理时双击图标会开出第二个实例,数据状态立刻混乱。做法是在启动时检测已有实例并聚焦其主窗口,实现需要用到单实例插件或系统 API,具体以官方文档为准。

窗口管理的 API 数量多但模式单一:构建器配置初始状态,运行时方法控制动态行为,事件回调响应交互。掌握 builder 模式后其余 API 举一反三。下一章看状态如何在多窗口之间共享。

窗口管理的 API 表面庞大,但日常高频只有十来个:显示、隐藏、聚焦、最大化、居中、关闭。把这十来个练熟,90% 的窗口交互需求当场就能写完。剩下的边角能力等真正需要时查文档即可,不必提前记忆。

顺带一句性能提醒:透明窗口在部分平台有合成开销,老旧设备上会掉帧。是否使用华丽效果,先在目标机器实测再决定,「简单窗口加好的交互逻辑」往往胜过炫技。下一章进入 State 共享,看数据如何在窗口与命令之间流动。

延伸阅读

  • 系统托盘、菜单与全局快捷键——窗口生命周期的另一半
  • 全局状态:tauri::State 与共享数据——多窗口共享状态的方案
  • Rust 入门:WebviewWindow 相关的 Rust 语法