修复: webview尺寸收窄卡住与兜底失效闪退

- 窗口尺寸变化后webview收窄卡住根修
- 尺寸兜底失效致闪退与修复空转加固
- 跨服务器预览残留清理
This commit is contained in:
lxy
2026-10-01 01:02:39 +08:00
parent b9c0786c02
commit e28ae653b0
3 changed files with 166 additions and 2 deletions
+133
View File
@@ -0,0 +1,133 @@
//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
}
+27 -2
View File
@@ -513,8 +513,11 @@ const fileEditorPanelConfig = computed(() => {
currentFileExtension, currentFileExtension,
isBinaryFile: isBinaryFileRef.value, isBinaryFile: isBinaryFileRef.value,
fileMtime: selectedFileItem.value?.modified_time || '', fileMtime: selectedFileItem.value?.modified_time || '',
// 多 tab // 多 tab(按当前服务器过滤,跨服务器的标签不显示在浮动标签栏)
previewTabs: multiPreview.tabs.value, previewTabs: (() => {
const pid = connectionManager.activeProfile?.id
return multiPreview.tabs.value.filter(t => (t.profileId ?? 'local-default') === pid)
})(),
activeTabId: multiPreview.activeTabId.value, activeTabId: multiPreview.activeTabId.value,
// 预览插件(优先于内置 v-if 链;.drawio/.dio 命中,其余为 null) // 预览插件(优先于内置 v-if 链;.drawio/.dio 命中,其余为 null)
activePlugin: currentFileName ? resolvePreviewHandler(currentFileName) : null, activePlugin: currentFileName ? resolvePreviewHandler(currentFileName) : null,
@@ -569,6 +572,7 @@ const handleFileChanged = async (event: any) => {
// 同路径也强制重载:navigate 同路径早退时列表会永远不加载 // 同路径也强制重载:navigate 同路径早退时列表会永远不加载
const switchConnection = async (profileId: string) => { const switchConnection = async (profileId: string) => {
await connectionManager.connect(profileId) await connectionManager.connect(profileId)
await syncPreviewToActiveProfile()
await loadCommonPaths() await loadCommonPaths()
const targetPath = connectionManager.isRemote() ? '/' : 'C:/' const targetPath = connectionManager.isRemote() ? '/' : 'C:/'
if (filePath.value === targetPath) { if (filePath.value === targetPath) {
@@ -596,6 +600,7 @@ const handleGoToPath = async (path: string) => {
const targetProfileId = getProfileIdForPath(path) const targetProfileId = getProfileIdForPath(path)
if (targetProfileId && targetProfileId !== connectionManager.activeProfile?.id) { if (targetProfileId && targetProfileId !== connectionManager.activeProfile?.id) {
await connectionManager.connect(targetProfileId) await connectionManager.connect(targetProfileId)
await syncPreviewToActiveProfile()
} }
await navigate(path) await navigate(path)
} }
@@ -1459,6 +1464,26 @@ const doCloseTab = (tabId: string) => {
} }
} }
/** 切换服务器后预览区联动:只显示当前服务器的标签与内容,避免旧服务器文档残留卡住 */
const syncPreviewToActiveProfile = async () => {
const currentProfileId = connectionManager.activeProfile?.id
const isMine = (t: PreviewTab) => (t.profileId ?? 'local-default') === currentProfileId
const active = multiPreview.activeTab.value
if (active && isMine(active)) return
// 面板上残留的是旧服务器内容,先缓存回它自己的 tab(loaded 标记让切回时秒恢复)
cacheCurrentTabState()
const mine = multiPreview.tabs.value.find(isMine)
if (mine) {
await activateTab(mine)
} else {
// 当前服务器没有已打开的预览:回到空白预览态
selectedFileItem.value = null
clearContent()
}
}
/** 缓存当前 tab 的编辑状态 */ /** 缓存当前 tab 的编辑状态 */
const cacheCurrentTabState = () => { const cacheCurrentTabState = () => {
multiPreview.cacheCurrentTab({ multiPreview.cacheCurrentTab({
+6
View File
@@ -64,6 +64,9 @@ func main() {
app.HandleHotkey() app.HandleHotkey()
return 0, true return 0, true
} }
case wmSizeMsg:
// webview 尺寸失同步兜底(app_window_fix.go)
go fixWebviewFill(hwnd)
} }
return 0, false return 0, false
}, },
@@ -105,6 +108,9 @@ func main() {
app.SetMainWindow(window) app.SetMainWindow(window)
// webview 与窗口尺寸失同步的周期兜底(app_window_fix.go)
StartWebviewFillWatch()
// 外部文件拖入窗口(OS 资源管理器拖放):把本地路径转发给前端处理 // 外部文件拖入窗口(OS 资源管理器拖放):把本地路径转发给前端处理
// (本机面板=复制进当前目录,SFTP 面板=上传) // (本机面板=复制进当前目录,SFTP 面板=上传)
window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) { window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {