//go:build windows package main import ( "strings" "sync/atomic" "syscall" "time" "unsafe" "golang.org/x/sys/windows" ) // Chrome_WidgetWin 系列类名是 WebView2 的宿主窗口层级 const webviewHostClassFragment = "Chrome_WidgetWin" // wmSize Win32 窗口尺寸变化消息 const wmSizeMsg uint32 = 0x0005 var ( // user32 对这三个 API 只导出无后缀名(无 A/W 变体,带 W 后缀找不到 proc 会 panic) procGetClientRectFix = windows.NewLazySystemDLL("user32.dll").NewProc("GetClientRect") procSetWindowPosFix = windows.NewLazySystemDLL("user32.dll").NewProc("SetWindowPos") procGetAncestorFix = windows.NewLazySystemDLL("user32.dll").NewProc("GetAncestor") ) // 修复去重:同一时刻只允许一个校验在跑(回调写共享切片要求互斥) var webviewFixRunning atomic.Bool // 当前校验的目标主窗口(枚举回调过滤直属子窗口用) var webviewTargetHwnd uintptr // webviewHosts 收集结果容器;enumChildWebviewHosts 回调写入 var webviewHosts []windows.HWND // 包级变量:syscall.NewCallback 指向的 Go 闭包必须存活于整个进程生命周期 var enumChildWebviewHosts = syscall.NewCallback(func(h uintptr, _ uintptr) uintptr { // GA_PARENT(1):只收直属子窗口,避免动到更深层的 WebView2 内部窗口 parent, _, _ := procGetAncestorFix.Call(h, 1) if parent != webviewTargetHwnd { return 1 } var buf [256]uint16 if _, err := windows.GetClassName(windows.HWND(h), &buf[0], int32(len(buf))); err != nil { return 1 } if strings.Contains(syscall.UTF16ToString(buf[:]), webviewHostClassFragment) { webviewHosts = append(webviewHosts, windows.HWND(h)) } return 1 }) // checkAndFixFill 校验 WebView2 宿主是否铺满窗口客户区,未铺满则强制铺满。 // wails alpha 的 WM_SIZE 处理存在竞态:主线程忙时消息被 resizePending 标志丢弃, // 迟到执行的重铺又使用闭包捕获的过期 bounds,webview 会停在中间尺寸且不再有 // 新消息来纠正,表现为整个应用内容收窄、四周露出窗口底色。 func checkAndFixFill(hwnd uintptr) { if hwnd == 0 { return } var rc struct{ Left, Top, Right, Bottom int32 } if r, _, _ := procGetClientRectFix.Call(hwnd, uintptr(unsafe.Pointer(&rc))); r == 0 { return } cw, ch := int(rc.Right-rc.Left), int(rc.Bottom-rc.Top) if cw <= 0 || ch <= 0 { return } webviewTargetHwnd = hwnd webviewHosts = webviewHosts[:0] windows.EnumChildWindows(windows.HWND(hwnd), enumChildWebviewHosts, nil) for _, h := range webviewHosts { // SWP_NOZORDER(0x0004):只改尺寸位置,不干预 wails 管理的层级 procSetWindowPosFix.Call(uintptr(h), 0, 0, 0, uintptr(cw), uintptr(ch), 0x0004) } } // runGuarded 统一 panic 兜底:windowsgui 下 goroutine panic 会静默杀进程 func runGuarded(fn func()) { defer func() { recover() }() fn() } // fixWebviewFill WM_SIZE 路径:等 resize 序列结束、wails 内部去抖落定后校验一次 func fixWebviewFill(hwnd uintptr) { if !webviewFixRunning.CompareAndSwap(false, true) { return } go func() { defer webviewFixRunning.Store(false) runGuarded(func() { time.Sleep(150 * time.Millisecond) checkAndFixFill(hwnd) }) }() } // StartWebviewFillWatch 周期兜底:覆盖"无新 WM_SIZE 的迟到重放"场景 // (主线程忙时排队的过期重绘在窗口静止时才执行,此时不会产生任何 WM_SIZE) func StartWebviewFillWatch() { go runGuarded(func() { for { time.Sleep(3 * time.Second) if webviewFixRunning.CompareAndSwap(false, true) { runGuarded(func() { checkAndFixFill(findMainWindowHwnd()) }) webviewFixRunning.Store(false) } } }) } // findMainWindowHwnd 找本进程的可见主窗口 func findMainWindowHwnd() uintptr { var found uintptr pid := windows.GetCurrentProcessId() cb := syscall.NewCallback(func(h uintptr, _ uintptr) uintptr { var winPid uint32 windows.GetWindowThreadProcessId(windows.HWND(h), &winPid) if winPid == pid && windows.IsWindowVisible(windows.HWND(h)) { found = h return 0 } return 1 }) runGuarded(func() { windows.EnumWindows(cb, nil) }) return found }