多窗口
多窗口应用的常用模式和最佳实践
多窗口应用
Wails v3 提供原生多窗口支持,可用于创建设置窗口、文档窗口、工具面板和检查器窗口。通过简单且一致的 API 跟踪窗口、实现窗口间通信并管理窗口的生命周期。
主窗口与设置窗口
go
package main
import "github.com/wailsapp/wails/v3/pkg/application"
type App struct {
app *application.App
mainWindow *application.WebviewWindow
settingsWindow *application.WebviewWindow
}
func main() {
app := &App{}
app.app = application.New(application.Options{
Name: "Multi-Window App",
})
// Create main window
app.mainWindow = app.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Main Application",
Width: 1200,
Height: 800,
})
// Create settings window (hidden initially)
app.settingsWindow = app.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
Height: 400,
Hidden: true,
})
app.app.Run()
}
// Show settings from main window
func (a *App) ShowSettings() {
if a.settingsWindow != nil {
a.settingsWindow.Show()
a.settingsWindow.Focus()
}
}要点:
- 主窗口始终可见
- 设置窗口已创建但处于隐藏状态
- 按需显示设置窗口
- 复用同一窗口(不要创建多个窗口)
窗口跟踪
获取所有窗口
go
windows := app.Window.GetAll()
fmt.Printf("Total windows: %d\n", len(windows))
for _, window := range windows {
fmt.Printf("- %s (ID: %d)\n", window.Name(), window.ID())
}查找特定窗口
go
// By name
if settings, ok := app.Window.GetByName("settings"); ok {
settings.Show()
}
// By ID
if window, ok := app.Window.GetByID(123); ok {
window.Focus()
}
// Current (focused) window
current := app.Window.Current()窗口注册表模式
跟踪应用中的窗口:
go
type WindowManager struct {
windows map[string]*application.WebviewWindow
mu sync.RWMutex
}
func (wm *WindowManager) Register(name string, window *application.WebviewWindow) {
wm.mu.Lock()
defer wm.mu.Unlock()
wm.windows[name] = window
}
func (wm *WindowManager) Get(name string) *application.WebviewWindow {
wm.mu.RLock()
defer wm.mu.RUnlock()
return wm.windows[name]
}
func (wm *WindowManager) Remove(name string) {
wm.mu.Lock()
defer wm.mu.Unlock()
delete(wm.windows, name)
}窗口通信
使用事件
窗口通过事件系统通信:
go
// In main window - emit event
app.Event.Emit("settings-changed", map[string]interface{}{
"theme": "dark",
"fontSize": 14,
})
// In settings window - listen for event
app.Event.On("settings-changed", func(event *application.CustomEvent) {
data := event.Data.(map[string]interface{})
theme := data["theme"].(string)
fontSize := data["fontSize"].(int)
// Update UI
updateSettings(theme, fontSize)
})共享状态模式
使用共享状态管理器:
go
type AppState struct {
theme string
fontSize int
mu sync.RWMutex
}
var state = &AppState{
theme: "light",
fontSize: 12,
}
func (s *AppState) SetTheme(theme string) {
s.mu.Lock()
s.theme = theme
s.mu.Unlock()
// Notify all windows
app.Event.Emit("theme-changed", theme)
}
func (s *AppState) GetTheme() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.theme
}窗口间消息
在特定窗口之间发送消息:
go
// Get target window
if targetWindow, ok := app.Window.GetByName("preview"); ok {
// Emit event to specific window
targetWindow.EmitEvent("update-preview", previewData)
}常用模式
模式1:单例窗口
确保一个窗口仅存在一个实例:
go
var settingsWindow *application.WebviewWindow
func ShowSettings(app *application.App) {
// Create if doesn't exist
if settingsWindow == nil {
settingsWindow = app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
Height: 400,
})
// Cleanup on close
settingsWindow.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
settingsWindow = nil
})
}
// Show and focus
settingsWindow.Show()
settingsWindow.Focus()
}模式2:文档窗口
同一窗口类型的多个实例:
go
type DocumentWindow struct {
window *application.WebviewWindow
filePath string
modified bool
}
var documents = make(map[string]*DocumentWindow)
func OpenDocument(app *application.App, filePath string) {
// Check if already open
if doc, exists := documents[filePath]; exists {
doc.window.Show()
doc.window.Focus()
return
}
// Create new document window
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: filepath.Base(filePath),
Width: 800,
Height: 600,
})
doc := &DocumentWindow{
window: window,
filePath: filePath,
modified: false,
}
documents[filePath] = doc
// Cleanup on close
window.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
delete(documents, filePath)
})
// Load document
loadDocument(window, filePath)
}模式3:工具面板
始终置顶的浮动窗口:
go
func CreateToolPalette(app *application.App) *application.WebviewWindow {
palette := app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "tools",
Title: "Tools",
Width: 200,
Height: 400,
AlwaysOnTop: true,
DisableResize: true,
})
return palette
}模式4:模态对话框(仅限 macOS)
阻止用户与父窗口交互的子窗口:
go
func ShowModaldialog(parent *application.WebviewWindow, title string) {
dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: title,
Width: 400,
Height: 200,
AlwaysOnTop: true,
DisableResize: true,
})
parent.AttachModal(dialog)
}模式5:检查器/预览窗口
同步更新的关联窗口:
go
type EditorApp struct {
editor *application.WebviewWindow
preview *application.WebviewWindow
}
func (e *EditorApp) UpdatePreview(content string) {
if e.preview != nil && e.preview.IsVisible() {
e.preview.EmitEvent("content-changed", content)
}
}
func (e *EditorApp) TogglePreview() {
if e.preview == nil {
e.preview = app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "preview",
Title: "Preview",
Width: 600,
Height: 800,
})
e.preview.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
e.preview = nil
})
}
if e.preview.IsVisible() {
e.preview.Hide()
} else {
e.preview.Show()
}
}父子关系
创建子窗口
go
childWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Child Window",
})
parentWindow.AttachModal(childWindow)行为:
- 子窗口保持在父窗口上方
- 子窗口随父窗口一起移动
- 子窗口阻止用户与父窗口交互
平台支持:
| macOS | Windows | Linux |
|---|---|---|
| ✅ | ❌ | ❌ |
模态行为
实现类似模态窗口的行为:
go
func ShowModaldialog(parent *application.WebviewWindow, title string) {
dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: title,
Width: 400,
Height: 200,
})
parent.AttachModal(dialog)
}窗口生命周期管理
创建回调
在创建窗口时接收通知:
go
app.Window.OnCreate(func(window application.Window) {
fmt.Printf("Window created: %s\n", window.Name())
// Configure all new windows
window.SetMinSize(400, 300)
})销毁回调
在销毁窗口时执行清理:
go
window.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
fmt.Printf("Window %s is closing\n", window.Name())
// Cleanup resources
cleanup(window.ID())
// Remove from tracking
removeFromRegistry(window.Name())
})应用退出行为
控制应用何时退出:
go
app := application.New(application.Options{
Mac: application.MacOptions{
// Don't quit when last window closes
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
})用例:
- 系统托盘应用
- 后台服务
- 菜单栏应用(macOS)
内存管理
防止内存泄漏
始终清理窗口引用:
go
var windows = make(map[string]*application.WebviewWindow)
func CreateWindow(name string) {
window := app.Window.New()
windows[name] = window
// IMPORTANT: Clean up on close
window.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
delete(windows, name)
})
}关闭窗口
go
// Close — dispatches WindowClosing; a RegisterHook can call e.Cancel().
window.Close()v3 中没有window.Destroy()。若要阻止窗口关闭,请使用RegisterHook(events.Common.WindowClosing, ...)并调用event.Cancel()。
资源清理
go
type ManagedWindow struct {
window *application.WebviewWindow
resources []io.Closer
}
func (mw *ManagedWindow) Destroy() {
// Close all resources
for _, resource := range mw.resources {
resource.Close()
}
// Close the window — WindowClosing listeners run before the window goes away.
mw.window.Close()
}高级模式
窗口池
复用窗口,而不是创建新窗口:
go
type WindowPool struct {
available []*application.WebviewWindow
inUse map[uint]*application.WebviewWindow
mu sync.Mutex
}
func (wp *WindowPool) Acquire() *application.WebviewWindow {
wp.mu.Lock()
defer wp.mu.Unlock()
// Reuse available window
if len(wp.available) > 0 {
window := wp.available[0]
wp.available = wp.available[1:]
wp.inUse[window.ID()] = window
return window
}
// Create new window
window := app.Window.New()
wp.inUse[window.ID()] = window
return window
}
func (wp *WindowPool) Release(window *application.WebviewWindow) {
wp.mu.Lock()
defer wp.mu.Unlock()
delete(wp.inUse, window.ID())
window.Hide()
wp.available = append(wp.available, window)
}窗口组
统一管理相关窗口:
go
type WindowGroup struct {
name string
windows []*application.WebviewWindow
}
func (wg *WindowGroup) Add(window *application.WebviewWindow) {
wg.windows = append(wg.windows, window)
}
func (wg *WindowGroup) ShowAll() {
for _, window := range wg.windows {
window.Show()
}
}
func (wg *WindowGroup) HideAll() {
for _, window := range wg.windows {
window.Hide()
}
}
func (wg *WindowGroup) CloseAll() {
for _, window := range wg.windows {
window.Close()
}
}工作区管理
保存和恢复窗口布局:
go
type WindowLayout struct {
Windows []WindowState `json:"windows"`
}
type WindowState struct {
Name string `json:"name"`
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
}
func SaveLayout() *WindowLayout {
layout := &WindowLayout{}
for _, window := range app.Window.GetAll() {
x, y := window.Position()
width, height := window.Size()
layout.Windows = append(layout.Windows, WindowState{
Name: window.Name(),
X: x,
Y: y,
Width: width,
Height: height,
})
}
return layout
}
func RestoreLayout(layout *WindowLayout) {
for _, state := range layout.Windows {
if window, ok := app.Window.GetByName(state.Name); ok {
window.SetPosition(state.X, state.Y)
window.SetSize(state.Width, state.Height)
}
}
}完整示例
下面是一个可用于生产环境的多窗口应用程序:
go
package main
import (
"encoding/json"
"os"
"sync"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
type MultiWindowApp struct {
app *application.App
windows map[string]*application.WebviewWindow
mu sync.RWMutex
}
func main() {
mwa := &MultiWindowApp{
windows: make(map[string]*application.WebviewWindow),
}
mwa.app = application.New(application.Options{
Name: "Multi-Window Application",
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
})
// Create main window
mwa.CreateMainWindow()
// Load saved layout
mwa.LoadLayout()
mwa.app.Run()
}
func (mwa *MultiWindowApp) CreateMainWindow() {
window := mwa.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "main",
Title: "Main Application",
Width: 1200,
Height: 800,
})
mwa.RegisterWindow("main", window)
}
func (mwa *MultiWindowApp) ShowSettings() {
if window := mwa.GetWindow("settings"); window != nil {
window.Show()
window.Focus()
return
}
window := mwa.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: "Settings",
Width: 600,
Height: 400,
})
mwa.RegisterWindow("settings", window)
}
func (mwa *MultiWindowApp) OpenDocument(path string) {
name := "doc-" + path
if window := mwa.GetWindow(name); window != nil {
window.Show()
window.Focus()
return
}
window := mwa.app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: name,
Title: path,
Width: 800,
Height: 600,
})
mwa.RegisterWindow(name, window)
}
func (mwa *MultiWindowApp) RegisterWindow(name string, window *application.WebviewWindow) {
mwa.mu.Lock()
mwa.windows[name] = window
mwa.mu.Unlock()
window.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
mwa.UnregisterWindow(name)
})
}
func (mwa *MultiWindowApp) UnregisterWindow(name string) {
mwa.mu.Lock()
delete(mwa.windows, name)
mwa.mu.Unlock()
}
func (mwa *MultiWindowApp) GetWindow(name string) *application.WebviewWindow {
mwa.mu.RLock()
defer mwa.mu.RUnlock()
return mwa.windows[name]
}
func (mwa *MultiWindowApp) SaveLayout() {
layout := make(map[string]WindowState)
mwa.mu.RLock()
for name, window := range mwa.windows {
x, y := window.Position()
width, height := window.Size()
layout[name] = WindowState{
X: x,
Y: y,
Width: width,
Height: height,
}
}
mwa.mu.RUnlock()
data, _ := json.Marshal(layout)
os.WriteFile("layout.json", data, 0644)
}
func (mwa *MultiWindowApp) LoadLayout() {
data, err := os.ReadFile("layout.json")
if err != nil {
return
}
var layout map[string]WindowState
if err := json.Unmarshal(data, &layout); err != nil {
return
}
for name, state := range layout {
if window := mwa.GetWindow(name); window != nil {
window.SetPosition(state.X, state.Y)
window.SetSize(state.Width, state.Height)
}
}
}
type WindowState struct {
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
}最佳实践
✅ 应该做
- 跟踪窗口——保留引用以便访问
- 销毁时清理资源——防止内存泄漏
- 使用事件进行通信——实现解耦架构
- 复用窗口——不要创建重复窗口
- 保存和恢复布局——改善用户体验
- 处理窗口关闭操作——存在未保存的数据时,关闭前先请求确认
❌ 不应该做
- 不要创建无限数量的窗口——会导致内存和性能问题
- 不要忘记清理资源——否则会导致内存泄漏
- 不要随意使用全局变量——会导致线程安全问题
- 不要阻塞窗口创建——必要时以异步方式创建
- 不要忽略平台差异——在所有平台上进行测试