优化: 知识卡片交互+deepseek模型+鼠标钩子
This commit is contained in:
175
docs/04-功能迭代/overlay鼠标交互实践.md
Normal file
175
docs/04-功能迭代/overlay鼠标交互实践.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Overlay 鼠标交互实践
|
||||
|
||||
> 2026-06-05 | 知识卡片刷新按钮点击 + 悬停效果
|
||||
|
||||
---
|
||||
|
||||
## 背景
|
||||
|
||||
桌面壁纸 overlay 通过 WebView2 嵌入 WorkerW 实现,位于桌面图标层(SysListView32)之下。这意味着:
|
||||
|
||||
- **WM_NCHITTEST 永远到不了 overlay**——`WindowFromPoint` 返回的是 SysListView32
|
||||
- **CSS hover/click 无效**——鼠标事件被上层桌面图标窗口拦截
|
||||
- 需要 WH_MOUSE_LL 全局低级钩子(Hook Type 14)从 Go 层直接拦截
|
||||
|
||||
---
|
||||
|
||||
## 踩坑记录
|
||||
|
||||
### 1. GWLP_WNDPROC 常量值错误
|
||||
|
||||
```go
|
||||
// ❌ 错误:0xFFFFFFF4 = -12 = GWLP_ID,不是 GWLP_WNDPROC
|
||||
const gwlpWndProc = uintptr(0xFFFFFFF4)
|
||||
|
||||
// ✅ 正确:-4 = GWLP_WNDPROC
|
||||
const gwlpWndProc = ^uintptr(3) // 按位取反(3) = -4
|
||||
```
|
||||
|
||||
**教训**:Windows 常量用负数表示时,不要手动换算十六进制,用 Go 的 `^uintptr(N)` 表达。
|
||||
|
||||
### 2. GC 回收 NewCallback 导致全局鼠标异常
|
||||
|
||||
```go
|
||||
// ❌ 错误:cb 是局部变量,函数返回后可被 GC 回收
|
||||
func installMouseHook() {
|
||||
cb := windows.NewCallback(func(...) uintptr { ... })
|
||||
procSetWindowsHookExW.Call(14, cb, 0, 0)
|
||||
}
|
||||
|
||||
// ✅ 正确:存储到包级变量
|
||||
var llMouseHookCb uintptr
|
||||
func installMouseHook() {
|
||||
llMouseHookCb = windows.NewCallback(func(...) uintptr { ... })
|
||||
procSetWindowsHookExW.Call(14, llMouseHookCb, 0, 0)
|
||||
}
|
||||
```
|
||||
|
||||
**现象**:GC 回收回调后,钩子指向无效内存,导致**所有鼠标点击被拦截**——桌面、其他软件全部点不了。
|
||||
|
||||
**教训**:`windows.NewCallback` 返回的引用必须保存在 Go 可达的变量中,Go GC 不知道 Windows 持有回调引用。
|
||||
|
||||
### 3. WindowFromPoint 传参方式
|
||||
|
||||
```go
|
||||
// ❌ 错误:传指针
|
||||
hwndAt, _, _ := procWindowFromPoint.Call(uintptr(unsafe.Pointer(&screenPt)))
|
||||
|
||||
// ✅ 正确:POINT 按值传递,打包到单个 uintptr
|
||||
ptPacked := uintptr(s.X) | (uintptr(uint32(s.Y)) << 32)
|
||||
hwndAt, _, _ := procWindowFromPoint.Call(ptPacked)
|
||||
```
|
||||
|
||||
**教训**:Win32 API 中 `WindowFromPoint(POINT)` 的 POINT 是按值传递的,不是指针。在 64 位下两个 int32 打包为一个 uintptr。
|
||||
|
||||
### 4. SetCursor 在 WH_MOUSE_LL 中无效
|
||||
|
||||
```go
|
||||
// ❌ 无效:被后续窗口的 WM_SETCURSOR 覆盖
|
||||
procSetCursor.Call(handCursor) // 闪一下就被桌面窗口重设为箭头
|
||||
```
|
||||
|
||||
**原因**:钩子返回后 WM_MOUSEMOVE 继续传递给桌面窗口(SysListView32),其在处理 WM_SETCURSOR 时将自己的光标设回去。
|
||||
|
||||
**结论**:在 overlay 这种底层窗口架构下,无法可靠地改变鼠标光标样式。可行的替代方案:
|
||||
- 按钮视觉高亮(旋转 + 放大 + 背景色)✅
|
||||
- 全局 `SetSystemCursor`(影响整个系统,副作用大)❌ 不推荐
|
||||
|
||||
### 5. go-webview2 Bind 参数类型
|
||||
|
||||
```go
|
||||
// ❌ 错误:JSON number 解码为 float64,int32 静默失败
|
||||
wv.Bind("setInteractiveRect", func(left, top, right, bottom int32) string { ... })
|
||||
|
||||
// ✅ 正确:用 float64
|
||||
wv.Bind("setInteractiveRect", func(left, top, right, bottom float64) string { ... })
|
||||
```
|
||||
|
||||
### 6. DPR 缩放坐标转换
|
||||
|
||||
JS `getBoundingClientRect()` 返回 CSS 像素,`ScreenToClient` 返回物理像素。必须乘 DPR:
|
||||
|
||||
```javascript
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
setInteractiveRect(
|
||||
Math.round(r.left * dpr), Math.round(r.top * dpr),
|
||||
Math.round(r.right * dpr), Math.round(r.bottom * dpr)
|
||||
)
|
||||
```
|
||||
|
||||
### 7. HTTP 429 重试时 body 被消耗
|
||||
|
||||
```go
|
||||
// ❌ 错误:第一次请求消耗了 body,重试发送空 body
|
||||
resp, err := httpClient.Do(req)
|
||||
if resp.StatusCode == 429 {
|
||||
time.Sleep(3 * time.Second)
|
||||
resp, err = httpClient.Do(req) // body 已经 EOF
|
||||
}
|
||||
|
||||
// ✅ 正确:创建新 Request
|
||||
req2, _ := http.NewRequest("POST", cpaURL, bytes.NewReader(jsonData))
|
||||
resp2, _ := httpClient.Do(req2)
|
||||
```
|
||||
|
||||
### 8. WindowFromPoint + Progman 判断桌面遮挡
|
||||
|
||||
```go
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
**用途**:判断点击位置是否有其他窗口遮挡。只有桌面背景可见时才消费点击事件,避免拦截其他软件的点击。
|
||||
|
||||
---
|
||||
|
||||
## 最终架构
|
||||
|
||||
```
|
||||
WH_MOUSE_LL 全局钩子
|
||||
├── WM_MOUSEMOVE (0x0200)
|
||||
│ └── 检查鼠标是否在按钮 hit rect 内
|
||||
│ ├── 进入 → 按钮添加 hover-active 样式(旋转+放大+高亮)
|
||||
│ └── 离开 → 移除样式
|
||||
├── WM_LBUTTONDOWN (0x0201)
|
||||
│ ├── WindowFromPoint 检查是否桌面背景可见
|
||||
│ ├── ScreenToClient 转换到 wvHwnd 坐标
|
||||
│ └── 命中按钮 rect → pushKnowledgeLoading + goroutine 刷新
|
||||
└── 其他事件 → CallNextHookEx 传递
|
||||
```
|
||||
|
||||
**hit rect 来源**:JS 端 `getBoundingClientRect() * DPR`,仅覆盖刷新按钮(+8px 扩展)。
|
||||
|
||||
**冷却机制**:30s 内重复点击回退显示缓存内容,不停留在"加载中"。
|
||||
|
||||
---
|
||||
|
||||
## 关键文件
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `win32.go` | 鼠标钩子、WindowFromPoint 桌面判断、Progman 句柄 |
|
||||
| `knowledge.go` | deepseek-v4-pro 模型、质量检查、冷却回退 |
|
||||
| `KnowledgeCard.vue` | 按钮 hit rect、hover-active 样式 |
|
||||
| `systray.go` | setInteractiveRect float64 参数 |
|
||||
|
||||
---
|
||||
|
||||
## 速查:Win32 API 要点
|
||||
|
||||
| API | 陷阱 | 正确做法 |
|
||||
|-----|------|----------|
|
||||
| `SetWindowLongPtrW` | 负数索引常量 | `^uintptr(3)` 表示 -4 |
|
||||
| `windows.NewCallback` | GC 不跟踪外部引用 | 存包级变量 |
|
||||
| `WindowFromPoint` | POINT 按值非指针 | 打包为 uintptr |
|
||||
| `SetCursor` (WH_MOUSE_LL) | 被后续 WM_SETCURSOR 覆盖 | 不可靠,放弃 |
|
||||
| `ScreenToClient` | 物理像素 | JS 侧乘 DPR |
|
||||
| `go-webview2 Bind` | JSON number = float64 | 参数用 float64 |
|
||||
Reference in New Issue
Block a user