装壳的收官工序是形态:默认窗口带着系统标题栏,像"浏览器里跑的应用";无边框加自绘标题栏之后,它才开始像"一个产品"。本节实现这套改造,顺带把第二个窗口的创建与定向通信一并装上——多窗口是桌面应用区别于网页的分水岭。
配置里给窗口加 decorations: false,系统标题栏与边框消失,页面顶到窗口边缘:
{ "app": { "windows": [ { "label": "main", "title": "随身笔记", "width": 960, "height": 640, "decorations": false } ] } }
代价立即到账:窗口不能拖了。系统标题栏原本免费提供的拖拽、双击最大化、右键窗口菜单,全部需要自己接回。免费的东西在拿走之前,先想清楚要补哪些。

Tauri 用一个 HTML 属性声明拖拽区:贴着该属性的元素,按住即可拖动窗口。
<div data-tauri-drag-region class="titlebar"> <span class="title">随身笔记</span> </div>
三个实操细节决定体验:其一,属性只作用于被点击的那个元素——标题栏里的按钮、输入框不能贴,否则点击变拖拽;空白垫片元素才贴。其二,双击最大化默认就有:在拖拽区双击,Tauri 自动切换最大化,与系统行为一致。其三,可拖区要留足高度(建议 40 像素上下),太窄的用户抓不准。
最小化、最大化、关闭三个按钮直接调前端窗口 API,同步返回不需要过命令:
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; const win = getCurrentWebviewWindow(); document.querySelector('#btn-min')?.addEventListener('click', () => win.minimize()); document.querySelector('#btn-max')?.addEventListener('click', () => win.toggleMaximize()); document.querySelector('#btn-close')?.addEventListener('click', () => win.close());
close() 与点系统关闭按钮走同一条路——2.4 节的 CloseRequested 拦截照样生效,"有未保存内容先询问"的逻辑无需重写。这一致性是设计红利:自绘按钮免费继承了安全层的行为。
创建第二个窗口有两条路。声明式:windows 数组里再写一项,应用启动即建。命令式:运行中按需创建,适合"编辑窗按需开"的交互:
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; async function openEditor(noteId: number) { const label = `editor-${noteId}`; if (await WebviewWindow.getByLabel(label)) { await getCurrentWebviewWindow().emitTo(label, 'open-note', { id: noteId }); return; // 已开着,直接通知它换内容 } const editor = new WebviewWindow(label, { url: 'index.html#/editor', width: 720, height: 520, title: '编辑笔记', }); editor.once('tauri://created', () => { editor.emitTo(label, 'open-note', { id: noteId }); }); }
三条纪律:标签全局唯一且创建后不可改,用业务语义命名(editor-42 优于 window-2);每个新窗口都会重新加载页面,路由(如 hash 的 editor 段)决定它渲染哪块界面;跨窗口通信一律带标签——4.3 节的 emitTo 在这里兑现价值,广播留给真正全体相关的事件。
无边框窗口的尺寸位置用户会调,下次启动应恢复——别手写存取,交给官方 window-state 插件(第 7 章安装),启动时自动还原各窗口的几何信息。手写这套逻辑的坑(多显示器、退出时机)比想象的多,能交给标准件就别自造。
自绘窗口的观感由细节堆出来,四个配置值得一并调掉。其一,圆角与阴影:无边框窗口默认方角,视觉上"太硬";对支持的窗口设置圆角属性,并保留系统投影,产品感立刻不同。其二,透明背景:窗口级透明用于过渡与圆角边缘的融合,注意别滥用——大面积透明区域是合成开销与掉帧的来源。其三,最大化时的贴边:自绘标题栏应用常见缺陷是最大化后内容贴住屏幕边缘无留白,给窗口内容区加最大宽度约束或边距即可。其四,最小尺寸:minWidth 与 minHeight 一定要配——无边框窗口用户拖拽更随意,没有下限的尺寸会撕碎你的界面布局。这四条都不影响功能,但用户对"这应用专不专业"的判断恰恰来自它们。
两个高频症状提前备好解法。新窗口白屏:九成是 url 路径在打包产物下解析不到——开发期看似正常的相对路径,生产环境走自定义协议后语义变了,统一用与构建产物结构一致的路径写法并在 CI 出包后实测一次。窗口创建报"标签已存在":创建前先用 getByLabel 检查(本节代码已示范),还要注意异常退场后标签残留的场景——应用以"窗口列表"为准清理孤儿标签,而不是假设窗口状态永远干净。多窗口的复杂度不在创建而在状态同步,4.3 的事件纪律在这里是基础设施。
壳装完了:面板能显示、命令能调用、事件能推送、窗口像产品。下一章下到芯里——用 Rust 把这些界面背后的能力造扎实。