Files
u-desk/app_window_fix.go
T
lxy 75a42c0b6a 修复: 粘贴图失效与SFTP串台回退等一批问题
粘贴图根修(Ctrl+V直读系统剪贴板,不再依赖不可靠的paste事件)/SFTP串台双刀(拨号期禁回退本机+盘符路径自动回根)/PDF导出目录设置与文案带落盘路径/性能批(连接保存下载防重/md渲染防抖+预热/hljs按需加载/审计锁外置/快捷方式解析超时/webview-watch可停)/前端62类型错清零(含keyPath蛇形字段炸弹)/目录图标字段错位修复
2026-10-24 13:48:54 +08:00

172 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build windows
package main
import (
"strings"
"sync/atomic"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
"u-desk/internal/common"
)
// 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
// WM_SIZE 去重:脏标记 + 单 worker(每条 WM_SIZE 只置位,不再批量 spawn goroutine)
var (
webviewSizeDirty atomic.Bool
webviewSizeWorkerActive 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 兜底:panic 取证落盘后不外抛,
// 让调用方(如周期循环)能继续跑下一轮
func runGuarded(fn func()) {
defer func() {
if r := recover(); r != nil {
common.AppendPanicLog("webview-fix", r)
}
}()
fn()
}
// fixWebviewFill WM_SIZE 路径:等 resize 序列结束、wails 内部去抖落定后校验一次。
// WM_SIZE 只置脏标记;活跃期间的后续 WM_SIZE 合并进去(worker 循环消费),
// 拖拽缩放的风暴从"每条消息一个 goroutine"降为全程单 worker
func fixWebviewFill(hwnd uintptr) {
webviewTargetHwnd = hwnd
webviewSizeDirty.Store(true)
if !webviewSizeWorkerActive.CompareAndSwap(false, true) {
return
}
common.Go("webview-fix", func() {
defer webviewSizeWorkerActive.Store(false)
for webviewSizeDirty.CompareAndSwap(true, false) {
time.Sleep(150 * time.Millisecond)
if webviewFixRunning.CompareAndSwap(false, true) {
runGuarded(func() { checkAndFixFill(webviewTargetHwnd) })
webviewFixRunning.Store(false)
}
}
})
}
// stopWebviewWatch 周期兜底循环的停止信号;StartWebviewFillWatch 重入时先关旧的(重启语义)
var stopWebviewWatch chan struct{}
// StartWebviewFillWatch 周期兜底:覆盖"无新 WM_SIZE 的迟到重放"场景
// (主线程忙时排队的过期重绘在窗口静止时才执行,此时不会产生任何 WM_SIZE)
func StartWebviewFillWatch() {
if stopWebviewWatch != nil {
select {
case <-stopWebviewWatch: // 旧循环已自行退出
default:
close(stopWebviewWatch)
}
}
stop := make(chan struct{})
stopWebviewWatch = stop
common.Go("webview-watch", func() {
for {
select {
case <-stop:
return
case <-time.After(3 * time.Second):
}
if webviewFixRunning.CompareAndSwap(false, true) {
runGuarded(func() {
checkAndFixFill(findMainWindowHwnd())
})
webviewFixRunning.Store(false)
}
}
})
}
// findMainWindowHwnd 找本进程的可见主窗口
// 回调必须进程级复用(同 enumChildWebviewHosts):runtime 的 Windows callback
// 硬上限 2000 个且按闭包指针去重,每 3 秒新建一个约 100 分钟即耗尽,
// 触发 runtime throw("too many callback functions") 直接静默退出(无法 recover)
var mainWindowHwndResult uintptr
var enumMainWindowHwnd = syscall.NewCallback(func(h uintptr, _ uintptr) uintptr {
var winPid uint32
windows.GetWindowThreadProcessId(windows.HWND(h), &winPid)
if winPid == windows.GetCurrentProcessId() && windows.IsWindowVisible(windows.HWND(h)) {
mainWindowHwndResult = h
return 0
}
return 1
})
func findMainWindowHwnd() uintptr {
mainWindowHwndResult = 0
windows.EnumWindows(enumMainWindowHwnd, nil)
return mainWindowHwndResult
}