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) }