桌面应用和 Web 应用有一个根本区别:桌面应用可以拥有多个操作系统窗口。用户期望确认对话框、消息提示、设置面板能作为独立窗口弹出,可以拖离主窗口范围,甚至放到另一块屏幕上。这不是 MUI Dialog 能解决的问题。
本文记录 Foundation 项目(Wails v3 + React 19 + MUI 9)如何实现类型化子窗口系统,以及顺带完成的 MUI X Pro/Premium 组件自实现。
子窗口的需求场景
Foundation 是一个桌面端工具类应用。以下场景需要真正的操作系统窗口而非模态弹窗:
确认操作(删除数据、重置配置)需要用户明确响应,且不能被主窗口遮挡
消息通知需要独立于主窗口生命周期,主窗口最小化后通知仍可见
未来的编辑器、预览器等功能需要独立窗口空间
MUI Dialog 是页面内的模态层,它无法脱离主窗口的渲染区域。Wails v3 提供了原生窗口创建能力,但没有现成的多窗口通信框架。因此需要自己搭建一套。
架构设计
子窗口系统分为四层:
Go 后端:ChildWindowService
internal/app/childwindow.go 是一个 Wails Service,负责窗口的创建、注册和生命周期管理。核心逻辑:
Open(opts)创建无边框窗口,通过 URL query 参数传递窗口类型和初始数据窗口注册到 map 中,关闭时自动清理并发射
child:closed:<id>事件Close(id)和List()提供外部控制能力
窗口创建时的关键配置:Frameless: true(自定义标题栏)、AlwaysOnTop: true(确认框不被遮挡)、DisableResize: true(固定尺寸的对话框)。尺寸按类型预设——confirm 420×220,message 420×200,通用窗口 600×400。
前端入口:独立 React 根
子窗口有独立的 HTML 入口(child.html)和 React 根节点(child.tsx),不与主窗口共享 React 树。这是刻意的设计——跨窗口无法共享 Context、Redux 或任何 React 状态,强行共享只会引入难以调试的问题。
child.tsx 读取 URL query 中的 type 参数,路由到对应的窗口组件。每种窗口类型是一个独立的 MVVM 文件夹,结构与主窗口的 pages 完全一致:View、ViewModel、styles.ts、lang/。
事件总线:窗口间通信
窗口间通信基于 Wails 的全局事件系统,协议如下:
这套协议让主窗口可以 await 子窗口的返回值,类似 window.confirm() 但异步且类型安全。
ChildTitleBar
无边框窗口没有系统标题栏,用户无法拖拽和关闭。ChildTitleBar 组件提供拖拽区域和关闭按钮,是每个子窗口的必备组件。样式与主窗口 TitleBar 保持一致,但更紧凑。
内置窗口类型
首批实现了三种窗口类型:
新增窗口类型只需:在 childwindows/ 下新建文件夹,在 child.tsx 的 switch 中加一个 case,在后端 resolveSize 中加默认尺寸。
次要更新:MUI X 组件自实现
在子窗口之外,本次还完成了 MUI X 付费功能的自实现,避免购买 Pro/Premium license。
DataGrid 增强(13 个 feature hooks):列钉住、列/行拖拽、树形数据、主从表、行钉住、懒加载、列头筛选、剪贴板粘贴、行分组、聚合、Excel 导出、单元格选择。
自定义图表(8 个纯 SVG 组件):Heatmap、Funnel、Radar、Candlestick、Sankey、Gantt、Treemap、ZoomableChart。
统一入口 @/components/x-pro,配套 11 个 SKILL 文档目录确保 AI 代理正确使用自实现组件。
结论
桌面应用的多窗口能力是 Web 应用做不到的差异化体验。关键是把通信协议设计清楚——事件总线加类型化消息,让窗口间的数据流可预测、可调试。子窗口不共享 React 树这个约束看似限制,实际上避免了跨窗口状态同步的复杂度。

参与讨论
(Participate in the discussion)
参与讨论