134 lines
4.1 KiB
Go
134 lines
4.1 KiB
Go
//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
|
||
}
|