Files
u-desktop/win32.go

301 lines
9.5 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.
package main
import (
"log"
"unsafe"
"github.com/jchv/go-webview2"
"golang.org/x/sys/windows"
)
var (
user32 = windows.NewLazySystemDLL("user32.dll")
procFindWindowW = user32.NewProc("FindWindowW")
procFindWindowExW = user32.NewProc("FindWindowExW")
procSendMessageTimeoutW = user32.NewProc("SendMessageTimeoutW")
procSetParent = user32.NewProc("SetParent")
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
procGetWindowRect = user32.NewProc("GetWindowRect")
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
procMoveWindow = user32.NewProc("MoveWindow")
procShowWindow = user32.NewProc("ShowWindow")
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW")
procGetWindowLongPtrW = user32.NewProc("GetWindowLongPtrW")
procGetDpiForWindow = user32.NewProc("GetDpiForWindow")
procGetClientRect = user32.NewProc("GetClientRect")
procMessageBoxW = user32.NewProc("MessageBoxW")
procGetMessageW = user32.NewProc("GetMessageW")
procPostMessageW = user32.NewProc("PostMessageW")
procTranslateMessage = user32.NewProc("TranslateMessage")
procDispatchMessageW = user32.NewProc("DispatchMessageW")
procGetParent = user32.NewProc("GetParent")
procCallWindowProcW = user32.NewProc("CallWindowProcW")
procSetForegroundWindow = user32.NewProc("SetForegroundWindow")
procScreenToClient = user32.NewProc("ScreenToClient")
procEnumChildWindows = user32.NewProc("EnumChildWindows")
procGetWindow = user32.NewProc("GetWindow")
procSetWindowsHookExW = user32.NewProc("SetWindowsHookExW")
procCallNextHookEx = user32.NewProc("CallNextHookEx")
procWindowFromPoint = user32.NewProc("WindowFromPoint")
)
var wv webview2.WebView
var wvHwnd uintptr
var jsQueue = make(chan string, 64)
var paused int32
const (
wmEvalJS = 0x0401
wmSetHtml = 0x0402
)
const (
gwlStyle = uintptr(0xFFFFFFF0) // GWL_STYLE = -16
gwlExStyle = uintptr(0xFFFFFFFEC) // GWL_EXSTYLE = -20
wsPopup = uintptr(0x80000000)
wsVisible = uintptr(0x10000000)
wsChild = uintptr(0x02000000)
wsSizebox = uintptr(0x00040000)
wsMaxbox = uintptr(0x00010000)
)
const gwlpWndProc = ^uintptr(3) // GWLP_WNDPROC = -4
const (
wsExTransparent = uintptr(0x00000020)
wsExLayered = uintptr(0x00080000)
wsExToolwindow = uintptr(0x00000080)
)
const (
gwChild = 5 // GW_CHILD
gwHwndNext = 2 // GW_HWNDNEXT
)
var htmlQueue = make(chan string, 1)
// overlay 窗口子类化:动态 hit test
var (
overlayOldWndProc uintptr
overlayWndProcCb uintptr
overlayHitRect struct{ Left, Top, Right, Bottom int32 }
overlayHasHitRect bool
childOldWndProcs = make(map[uintptr]uintptr) // 子窗口 hwnd -> 旧 wndproc
)
// overlayWndProc 拦截 WM_NCHITTEST交互区域返回 HTCLIENT其余 HTTRANSPARENT穿透
// 注意:由于 overlay 在桌面图标层之下,此 wndproc 主要作为备用
func overlayWndProc(hwnd, msg, wp, lp uintptr) uintptr {
if msg == 0x0084 && overlayHasHitRect { // WM_NCHITTEST
x := int32(int16(lp & 0xFFFF))
y := int32(int16(lp >> 16))
pt := struct{ X, Y int32 }{x, y}
procScreenToClient.Call(wvHwnd, uintptr(unsafe.Pointer(&pt)))
if pt.X >= overlayHitRect.Left && pt.X <= overlayHitRect.Right &&
pt.Y >= overlayHitRect.Top && pt.Y <= overlayHitRect.Bottom {
return uintptr(1) // HTCLIENT
}
return 0xFFFFFFFF // HTTRANSPARENT
}
// 查找该窗口的旧 wndproc
oldProc := overlayOldWndProc
if v, ok := childOldWndProcs[hwnd]; ok {
oldProc = v
}
r, _, _ := procCallWindowProcW.Call(oldProc, hwnd, msg, wp, lp)
return r
}
// installMouseHook 安装 WH_MOUSE_LL 全局钩子拦截刷新按钮点击
// overlay 在 WorkerW 中位于桌面图标层之下WM_NCHITTEST 无法到达
// 因此用低级鼠标钩子直接在 Go 层处理点击
var llMouseHook uintptr
var llMouseHookCb uintptr // 防止 GC 回收回调函数
var progmanHwnd uintptr // Progman 句柄,用于判断点击是否在桌面
var btnHoverActive bool // 按钮悬停状态,避免重复推送 JS
// isMouseInBtnRect 检查鼠标是否在按钮 hit rect 内(屏幕坐标转 wvHwnd 客户端坐标)
func isMouseInBtnRect(screenX, screenY int32) bool {
if !overlayHasHitRect {
return false
}
pt := struct{ X, Y int32 }{screenX, screenY}
procScreenToClient.Call(wvHwnd, uintptr(unsafe.Pointer(&pt)))
return pt.X >= overlayHitRect.Left && pt.X <= overlayHitRect.Right &&
pt.Y >= overlayHitRect.Top && pt.Y <= overlayHitRect.Bottom
}
// isDesktopAtPoint 检查屏幕坐标处是否为桌面背景(无其他窗口遮挡)
func isDesktopAtPoint(screenX, screenY int32) bool {
ptPacked := uintptr(screenX) | (uintptr(uint32(screenY)) << 32)
hwndAt, _, _ := procWindowFromPoint.Call(ptPacked)
for h := hwndAt; h != 0; h, _, _ = procGetParent.Call(h) {
if h == progmanHwnd {
return true
}
}
return false
}
func installMouseHook() {
llMouseHookCb = windows.NewCallback(func(nCode, wp, lp uintptr) uintptr {
if int32(nCode) >= 0 {
type msll struct {
X, Y int32
MouseData uint32
Flags uint32
Time uint32
DwExtraInfo uintptr
}
s := (*msll)(unsafe.Pointer(lp))
switch wp {
case 0x0200: // WM_MOUSEMOVE — 悬停检测
inBtn := isMouseInBtnRect(s.X, s.Y)
if inBtn && !btnHoverActive {
btnHoverActive = true
evalJS("document.querySelector('.knowledge-refresh-btn')?.classList.add('hover-active')")
} else if !inBtn && btnHoverActive {
btnHoverActive = false
evalJS("document.querySelector('.knowledge-refresh-btn')?.classList.remove('hover-active')")
}
case 0x0201: // WM_LBUTTONDOWN — 点击
if !isDesktopAtPoint(s.X, s.Y) {
break
}
if isMouseInBtnRect(s.X, s.Y) {
pushKnowledgeLoading("")
go fetchAndPushKnowledge()
return 1
}
}
}
r, _, _ := procCallNextHookEx.Call(llMouseHook, nCode, wp, lp)
return r
})
var llMouseHookVal uintptr
llMouseHookVal, _, _ = procSetWindowsHookExW.Call(
14, // WH_MOUSE_LL
llMouseHookCb, 0, 0,
)
llMouseHook = llMouseHookVal
log.Printf("mouse hook installed, progman=0x%x, hook=0x%x", progmanHwnd, llMouseHookVal)
}
// subclassOverlayWindows 子类化 WebView2 父窗口及其所有子窗口
func subclassOverlayWindows() {
overlayWndProcCb = windows.NewCallback(overlayWndProc)
oldProc, _, _ := procSetWindowLongPtrW.Call(wvHwnd, gwlpWndProc, overlayWndProcCb)
overlayOldWndProc = oldProc
subclassChildTree(wvHwnd)
}
// subclassChildTree 递归子类化 parent 下的所有子窗口
func subclassChildTree(parent uintptr) {
child, _, _ := procGetWindow.Call(parent, uintptr(gwChild))
for child != 0 {
old, _, _ := procSetWindowLongPtrW.Call(child, gwlpWndProc, overlayWndProcCb)
childOldWndProcs[child] = old
subclassChildTree(child)
child, _, _ = procGetWindow.Call(child, uintptr(gwHwndNext))
}
}
var (
classProgman = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("Progman")))
classShellDefView = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("SHELLDLL_DefView")))
classWorkerW = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("WorkerW")))
)
func evalJS(js string) {
select {
case jsQueue <- js:
log.Println("evalJS queued:", js[:min(len(js), 60)])
default:
log.Println("jsQueue full, dropping eval")
}
if wvHwnd != 0 {
procPostMessageW.Call(wvHwnd, wmEvalJS, 0, 0)
}
}
func findWorkerW() uintptr {
progman, _, _ := procFindWindowW.Call(
classProgman, 0)
if progman == 0 {
log.Println("findWorkerW: Progman not found")
return 0
}
progmanHwnd = progman // 保存 Progman 句柄供鼠标钩子判断
// 发送 0x052C 触发 Progman 创建 WorkerW
var result uintptr
procSendMessageTimeoutW.Call(progman, 0x052C, 0, 0, 0x0000, 1000, uintptr(unsafe.Pointer(&result)))
// 方法1: Progman 下找 SHELLDLL_DefView再找其后的 WorkerW
shellDefView, _, _ := procFindWindowExW.Call(progman, 0, classShellDefView, 0)
if shellDefView != 0 {
ww, _, _ := procFindWindowExW.Call(progman, shellDefView, classWorkerW, 0)
if ww != 0 {
log.Printf("findWorkerW: 方法1成功, WorkerW=0x%x", ww)
return ww
}
}
// 方法2: 遍历顶层 WorkerW找到含 SHELLDLL_DefView 的那个,取它后面的 WorkerW
var prev uintptr
for {
ww, _, _ := procFindWindowExW.Call(0, prev, classWorkerW, 0)
if ww == 0 {
break
}
child, _, _ := procFindWindowExW.Call(ww, 0, classShellDefView, 0)
if child != 0 {
// 这个 WorkerW 包含 SHELLDLL_DefView找它后面的 WorkerW
next, _, _ := procFindWindowExW.Call(0, ww, classWorkerW, 0)
if next != 0 {
log.Printf("findWorkerW: 方法2成功, WorkerW=0x%x (after 0x%x)", next, ww)
return next
}
}
prev = ww
}
// 方法3: 遍历顶层 WorkerW找任意不含 SHELLDLL_DefView 的可见 WorkerW
prev = 0
for {
ww, _, _ := procFindWindowExW.Call(0, prev, classWorkerW, 0)
if ww == 0 {
break
}
child, _, _ := procFindWindowExW.Call(ww, 0, classShellDefView, 0)
if child == 0 {
log.Printf("findWorkerW: 方法3成功, WorkerW=0x%x", ww)
return ww
}
prev = ww
}
// 方法4: 兜底Progman 下的第一个 WorkerW
ww, _, _ := procFindWindowExW.Call(progman, 0, classWorkerW, 0)
if ww != 0 {
log.Printf("findWorkerW: 方法4(兜底), WorkerW=0x%x", ww)
}
return ww
}
func getDPI(hwnd uintptr) int {
dpi, _, _ := procGetDpiForWindow.Call(hwnd)
if dpi == 0 {
return 96
}
return int(dpi)
}
func getScreenSize() (int32, int32) {
w, _, _ := procGetSystemMetrics.Call(0)
h, _, _ := procGetSystemMetrics.Call(1)
return int32(w), int32(h)
}