Files
u-desktop/win32.go
T

301 lines
9.5 KiB
Go

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