mousealert_windows.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. //go:build windows
  2. // ============================================================================
  3. // 系统级鼠标提醒(Windows 原生实现)
  4. //
  5. // 需求:平时鼠标是正常样式;有消息来(通过 API 触发)时,鼠标图标闪烁,
  6. // 且光标下方浮出简短的半透明提示文字,可覆盖任意窗口(系统级)。
  7. //
  8. // 实现原理:
  9. // 1. 全局低层鼠标钩子 WH_MOUSE_LL:捕获系统级鼠标移动,获取屏幕坐标。
  10. // 2. 一个置顶、透明、点击穿透的覆盖层窗口(WS_EX_LAYERED|WS_EX_TRANSPARENT
  11. // |WS_EX_TOOLWINDOW|WS_EX_NOACTIVATE),用 GDI UpdateLayeredWindow 在鼠标
  12. // 位置绘制闪烁光标环与下方半透明文字。
  13. // 3. 定时器驱动闪烁:普通光标样式 <-> 提醒光标样式交替。
  14. // 4. 提醒可自动消退(默认 8 秒),也可再调 API 停止。
  15. //
  16. // 说明:本文件仅在 Windows 编译。其他平台见 mousealert_other.go 空实现。
  17. // ============================================================================
  18. package main
  19. import (
  20. "sync"
  21. "syscall"
  22. "time"
  23. "unsafe"
  24. )
  25. // Windows API 常量
  26. const (
  27. whMouseLL = 14 // WH_MOUSE_LL 全局低层鼠标钩子
  28. wmTimer = 0x0113
  29. wmDestroy = 0x0002
  30. wmNcDestroy = 0x0082
  31. wndExLayered = 0x00080000 // WS_EX_LAYERED 分层窗口(支持透明)
  32. wndExTrans = 0x00000020 // WS_EX_TRANSPARENT 点击穿透
  33. wndExTool = 0x00000080 // WS_EX_TOOLWINDOW 不占任务栏
  34. wndExTopMost = 0x00000008 // WS_EX_TOPMOST 置顶
  35. wndExNoActivate = 0x08000000 // WS_EX_NOACTIVATE 不抢焦点
  36. swpNoActivate = 0x0010 // SWP_NOACTIVATE
  37. swpShowWindow = 0x0040 // SWP_SHOWWINDOW
  38. biRgb = 0 // BI_RGB
  39. timerFlash = 1 // 闪烁定时器 ID
  40. overlayWidth = 220
  41. overlayHeight = 90
  42. textTopY = 26 // 文字起始 Y 坐标(覆盖层内),用于区分半透明文字区
  43. alertDuration = 8 * time.Second
  44. flashIntervalMs = 250 // 闪烁周期 250ms
  45. )
  46. // 光标提示样式:普通状态 vs 提醒状态(闪烁交替)
  47. const (
  48. flashColorNormal = 0x00FFFFFF // 普通:白
  49. flashColorAlert = 0x00FF5050 // 提醒:红
  50. )
  51. // hwndTopMost HWND_TOPMOST = (HWND)-1
  52. const hwndTopMost uintptr = ^uintptr(0)
  53. // ------------------------------------------------------------------------
  54. // Windows 结构体与函数指针声明(用 syscall 直接调用,避免引入额外依赖)
  55. // ------------------------------------------------------------------------
  56. type point struct {
  57. X int32
  58. Y int32
  59. }
  60. // rect Windows RECT 结构(GetClientRect 输出)
  61. type rect struct {
  62. Left int32
  63. Top int32
  64. Right int32
  65. Bottom int32
  66. }
  67. type msg struct {
  68. HWND uintptr
  69. Message uint32
  70. WParam uintptr
  71. LParam uintptr
  72. Time uint32
  73. Pt point
  74. LPrivate uint32
  75. }
  76. type msllhookstruct struct {
  77. Pt point
  78. MouseData uint32
  79. Flags uint32
  80. Time uint32
  81. DWExtraInfo uintptr
  82. }
  83. type wndclassex struct {
  84. CbSize uint32
  85. Style uint32
  86. LpfnWndProc uintptr
  87. CbClsExtra int32
  88. CbWndExtra int32
  89. HInstance uintptr
  90. HIcon uintptr
  91. HCursor uintptr
  92. HbrBackground uintptr
  93. LpszMenuName *uint16
  94. LpszClassName *uint16
  95. HIconSm uintptr
  96. }
  97. type bitmapinfoheader struct {
  98. BiSize uint32
  99. BiWidth int32
  100. BiHeight int32
  101. BiPlanes uint16
  102. BiBitCount uint16
  103. BiCompression uint32
  104. BiSizeImage uint32
  105. BiXPelsPerMeter int32
  106. BiYPelsPerMeter int32
  107. BiClrUsed uint32
  108. BiClrImportant uint32
  109. }
  110. type blendfunction struct {
  111. BlendOp byte
  112. BlendFlags byte
  113. SourceConstantAlpha byte
  114. AlphaFormat byte
  115. }
  116. // 延迟加载的 user32.dll / gdi32.dll / kernel32.dll 函数
  117. var (
  118. user32 = syscall.NewLazyDLL("user32.dll")
  119. gdi32 = syscall.NewLazyDLL("gdi32.dll")
  120. kernel32 = syscall.NewLazyDLL("kernel32.dll")
  121. procSetWindowsHookEx = user32.NewProc("SetWindowsHookExW")
  122. procUnhookWindowsHook = user32.NewProc("UnhookWindowsHookEx")
  123. procGetMessage = user32.NewProc("GetMessageW")
  124. procTranslateMessage = user32.NewProc("TranslateMessage")
  125. procDispatchMessage = user32.NewProc("DispatchMessageW")
  126. procRegisterClassEx = user32.NewProc("RegisterClassExW")
  127. procCreateWindowEx = user32.NewProc("CreateWindowExW")
  128. procDefWindowProc = user32.NewProc("DefWindowProcW")
  129. procDestroyWindow = user32.NewProc("DestroyWindow")
  130. procSetTimer = user32.NewProc("SetTimer")
  131. procKillTimer = user32.NewProc("KillTimer")
  132. procShowWindow = user32.NewProc("ShowWindow")
  133. procSetWindowPos = user32.NewProc("SetWindowPos")
  134. procUpdateLayeredWindow = user32.NewProc("UpdateLayeredWindow")
  135. procGetDC = user32.NewProc("GetDC")
  136. procReleaseDC = user32.NewProc("ReleaseDC")
  137. procGetCursorPos = user32.NewProc("GetCursorPos")
  138. procPostMessage = user32.NewProc("PostMessageW")
  139. procGetClientRect = user32.NewProc("GetClientRect")
  140. procGetWindowRect = user32.NewProc("GetWindowRect")
  141. procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
  142. procDeleteDC = gdi32.NewProc("DeleteDC")
  143. procCreateDIBSection = gdi32.NewProc("CreateDIBSection")
  144. procSelectObject = gdi32.NewProc("SelectObject")
  145. procDeleteObject = gdi32.NewProc("DeleteObject")
  146. procSetBkMode = gdi32.NewProc("SetBkMode")
  147. procSetTextColor = gdi32.NewProc("SetTextColor")
  148. procSetTextAlign = gdi32.NewProc("SetTextAlign")
  149. procTextOutW = gdi32.NewProc("TextOutW")
  150. procCreateFontW = gdi32.NewProc("CreateFontW")
  151. procSetPixel = gdi32.NewProc("SetPixel")
  152. procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
  153. )
  154. // ------------------------------------------------------------------------
  155. // 鼠标提醒运行时状态
  156. // ------------------------------------------------------------------------
  157. // mouseAlertState 鼠标提醒的运行时状态(单实例)
  158. type mouseAlertState struct {
  159. mu sync.Mutex
  160. started bool // 工作线程是否已启动(持久化,仅启动一次)
  161. hook uintptr
  162. overlayWnd uintptr
  163. active bool
  164. text string
  165. flashOn bool
  166. lastX, lastY int32
  167. stopTime time.Time
  168. hookProc uintptr
  169. wndProc uintptr
  170. classOK bool
  171. }
  172. var mouseAlert = &mouseAlertState{}
  173. // triggerMouseAlert 触发鼠标提醒:显示闪烁光标 + 光标下方透明文字。
  174. // 文字为空时用默认提示。重复触发会刷新提醒计时并立即显示。
  175. //
  176. // 定位策略:主动用 GetCursorPos 读取当前鼠标位置并记录为 lastX/lastY,
  177. // 再通过 PostMessage 唤醒窗口线程立即重绘,避免依赖全局钩子回调(钩子
  178. // 回调因负担重可能被系统卸载,导致窗口停留在旧位置不跟随鼠标)。
  179. func triggerMouseAlert(text string) {
  180. if text == "" {
  181. text = "有新消息提醒"
  182. }
  183. // 读取当前鼠标位置,供窗口重绘时定位(不依赖钩子回调)
  184. var pt point
  185. curRet, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
  186. hasCursor := curRet != 0
  187. mouseAlert.mu.Lock()
  188. mouseAlert.text = text
  189. mouseAlert.active = true
  190. mouseAlert.flashOn = true
  191. mouseAlert.stopTime = time.Now().Add(alertDuration)
  192. if hasCursor {
  193. mouseAlert.lastX, mouseAlert.lastY = pt.X, pt.Y
  194. }
  195. started := mouseAlert.started
  196. mouseAlert.started = true
  197. overlay := mouseAlert.overlayWnd
  198. mouseAlert.mu.Unlock()
  199. // 首次触发:启动常驻工作线程(幂等),建窗后由线程主动定位并重绘
  200. if !started {
  201. go mouseAlertLoop()
  202. return
  203. }
  204. // 已启动:唤醒窗口线程立即重绘(窗口线程负责 SetWindowPos/绘制,
  205. // 避免跨线程操作窗口;若窗口尚在初始化,线程建窗后也会主动绘制)
  206. if overlay != 0 {
  207. procShowWindow.Call(overlay, 5) // SW_SHOW
  208. procPostMessage.Call(overlay, wmTimer, timerFlash, 0)
  209. }
  210. }
  211. // stopMouseAlert 立即停止鼠标提醒(隐藏覆盖层;钩子保持常驻)
  212. func stopMouseAlert() {
  213. mouseAlert.mu.Lock()
  214. mouseAlert.active = false
  215. overlay := mouseAlert.overlayWnd
  216. mouseAlert.mu.Unlock()
  217. if overlay != 0 {
  218. procShowWindow.Call(overlay, 0) // SW_HIDE
  219. }
  220. }
  221. // mouseAlertLoop 常驻工作线程:注册窗口类、安装全局钩子、创建覆盖层窗口,
  222. // 进入消息循环。低层钩子必须有消息循环,故全程运行在独立线程。
  223. func mouseAlertLoop() {
  224. hInst, _, _ := procGetModuleHandleW.Call(0)
  225. mouseAlert.mu.Lock()
  226. className, _ := syscall.UTF16PtrFromString("TengTuMouseAlertOverlay")
  227. wc := wndclassex{
  228. CbSize: uint32(unsafe.Sizeof(wndclassex{})),
  229. LpfnWndProc: mouseAlert.wndProcAddr(),
  230. HInstance: hInst,
  231. LpszClassName: className,
  232. HCursor: 0,
  233. }
  234. atom, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
  235. if atom != 0 {
  236. mouseAlert.classOK = true
  237. }
  238. mouseAlert.mu.Unlock()
  239. // 安装全局低层鼠标钩子
  240. hookProc := mouseAlert.hookProcAddr()
  241. hHook, _, _ := procSetWindowsHookEx.Call(whMouseLL, hookProc, 0, 0)
  242. if hHook != 0 {
  243. mouseAlert.mu.Lock()
  244. mouseAlert.hook = hHook
  245. mouseAlert.mu.Unlock()
  246. }
  247. // 创建覆盖层窗口(初始 220x90,位置随后由钩子/定时器设置)
  248. style := uintptr(wndExLayered | wndExTrans | wndExTool | wndExTopMost | wndExNoActivate)
  249. overlay, _, _ := procCreateWindowEx.Call(
  250. style,
  251. uintptr(unsafe.Pointer(className)),
  252. 0,
  253. 0, 0, overlayWidth, overlayHeight, 0,
  254. 0, 0, hInst, 0,
  255. )
  256. mouseAlert.mu.Lock()
  257. mouseAlert.overlayWnd = overlay
  258. mouseAlert.mu.Unlock()
  259. // 启动闪烁定时器
  260. if overlay != 0 {
  261. procSetTimer.Call(overlay, timerFlash, flashIntervalMs, 0)
  262. // 建窗后立即用当前鼠标位置定位并重绘一次,保证首次触发即可见,
  263. // 不依赖全局钩子回调(钩子回调在系统线程执行,仅做转发)。
  264. var pt point
  265. curRet, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
  266. if curRet != 0 {
  267. mouseAlert.mu.Lock()
  268. if mouseAlert.active {
  269. mouseAlert.lastX, mouseAlert.lastY = pt.X, pt.Y
  270. text := mouseAlert.text
  271. flashOn := mouseAlert.flashOn
  272. mouseAlert.mu.Unlock()
  273. mouseAlert.renderOverlay(overlay, text, flashOn, pt.X, pt.Y)
  274. } else {
  275. mouseAlert.mu.Unlock()
  276. }
  277. }
  278. }
  279. // 消息循环(常驻,直到线程退出)
  280. var m msg
  281. for {
  282. ret, _, _ := procGetMessage.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
  283. if ret == 0 {
  284. break // WM_QUIT
  285. }
  286. procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
  287. procDispatchMessage.Call(uintptr(unsafe.Pointer(&m)))
  288. }
  289. // 清理
  290. if overlay != 0 {
  291. procDestroyWindow.Call(overlay)
  292. }
  293. if hHook != 0 {
  294. procUnhookWindowsHook.Call(hHook)
  295. }
  296. mouseAlert.mu.Lock()
  297. mouseAlert.hook = 0
  298. mouseAlert.overlayWnd = 0
  299. mouseAlert.mu.Unlock()
  300. }
  301. // hookProcAddr 返回全局钩子回调地址(保持 Go 函数引用)
  302. func (s *mouseAlertState) hookProcAddr() uintptr {
  303. if s.hookProc != 0 {
  304. return s.hookProc
  305. }
  306. s.hookProc = syscall.NewCallback(s.hookCallback)
  307. return s.hookProc
  308. }
  309. // wndProcAddr 返回覆盖层窗口过程地址
  310. func (s *mouseAlertState) wndProcAddr() uintptr {
  311. if s.wndProc != 0 {
  312. return s.wndProc
  313. }
  314. s.wndProc = syscall.NewCallback(s.wndProcCallback)
  315. return s.wndProc
  316. }
  317. // hookCallback 全局低层鼠标钩子回调:仅转发给下一钩子,不访问任何 Go 状态。
  318. //
  319. // 重要:此回调在系统创建的钩子线程上执行,且 CGO 启用时 syscall.NewCallback
  320. // 的回调从该线程进入 Go runtime。此前的实现在此处访问 s.mu(sync.Mutex 锁)、
  321. // 调用 time.Now()、procShowWindow 等 Go 状态操作,会导致进程在钩子回调触发时
  322. // 静默退出(app.Run 返回,exit 0)。故此处只做 CallNextHookEx 转发,不做任何
  323. // 其他工作;鼠标位置跟踪与到期判断全部移到窗口线程的定时器(wndProcCallback
  324. // 的 wmTimer 分支,用 GetCursorPos 主动读取),避免在钩子线程做 Go 操作。
  325. func (s *mouseAlertState) hookCallback(nCode int, wParam uintptr, lParam uintptr) uintptr {
  326. ret, _, _ := procCallNextHookEx.Call(0, uintptr(nCode), wParam, lParam)
  327. return ret
  328. }
  329. // renderOverlay 定位覆盖层到鼠标位置并重绘闪烁光标 + 下方透明文字。
  330. // 在窗口线程内调用(mouseAlertLoop 建窗后 / wndProcCallback 定时器),
  331. // 窗口/DC 操作均为同线程,避免跨线程操作窗口导致绘制失败。
  332. func (s *mouseAlertState) renderOverlay(overlay uintptr, text string, flashOn bool, x, y int32) {
  333. if overlay == 0 {
  334. return
  335. }
  336. // 让覆盖层左上角位于鼠标位置偏左上,使光标环中心对准鼠标
  337. left := x - 10
  338. top := y - 4
  339. procSetWindowPos.Call(overlay, hwndTopMost,
  340. uintptr(int32(left)), uintptr(int32(top)),
  341. overlayWidth, overlayHeight, swpNoActivate|swpShowWindow)
  342. s.drawLayered(overlay, text, flashOn)
  343. }
  344. // drawLayered 用 32 位 DIB + UpdateLayeredWindow 绘制闪烁光标环与半透明文字。
  345. func (s *mouseAlertState) drawLayered(overlay uintptr, text string, flashOn bool) {
  346. // 用窗口实际总尺寸创建 DIB,并用 GetWindowRect 获取窗口屏幕位置,
  347. // 显式传给 UpdateLayeredWindow 的 pptDst/psize,确保 DIB 与窗口完全
  348. // 匹配(此前用 GetClientRect 且 psize=nil,当 DIB 尺寸与窗口实际尺寸
  349. // 不一致时 UpdateLayeredWindow 绘制失效,导致覆盖层显示为空白)。
  350. var wr rect
  351. procGetWindowRect.Call(overlay, uintptr(unsafe.Pointer(&wr)))
  352. w := int(wr.Right - wr.Left)
  353. h := int(wr.Bottom - wr.Top)
  354. if w < 8 || h < 8 { // 兜底:窗口尺寸异常时用默认尺寸
  355. w, h = overlayWidth, overlayHeight
  356. }
  357. hdcScreen, _, _ := procGetDC.Call(0)
  358. if hdcScreen == 0 {
  359. return
  360. }
  361. defer procReleaseDC.Call(0, hdcScreen)
  362. hdcMem, _, _ := procCreateCompatibleDC.Call(hdcScreen)
  363. if hdcMem == 0 {
  364. return
  365. }
  366. defer procDeleteDC.Call(hdcMem)
  367. // 32 位 DIB 位图(负高度:自顶向下)
  368. bi := bitmapinfoheader{}
  369. bi.BiSize = 40 // sizeof(BITMAPINFOHEADER)
  370. bi.BiWidth = int32(w)
  371. bi.BiHeight = int32(-h)
  372. bi.BiPlanes = 1
  373. bi.BiBitCount = 32
  374. bi.BiCompression = biRgb
  375. var bits unsafe.Pointer
  376. hBitmap, _, _ := procCreateDIBSection.Call(hdcScreen,
  377. uintptr(unsafe.Pointer(&bi)), 0, uintptr(unsafe.Pointer(&bits)), 0, 0)
  378. if hBitmap == 0 {
  379. return
  380. }
  381. defer procDeleteObject.Call(hBitmap)
  382. oldBmp, _, _ := procSelectObject.Call(hdcMem, hBitmap)
  383. // 清空为全透明
  384. clearPixels(bits, w, h)
  385. // 光标环中心放在覆盖层 (10, 4) 附近,即鼠标位置
  386. drawCursorRing(hdcMem, 10, 4, flashOn)
  387. // 光标下方半透明文字
  388. if text != "" {
  389. drawAlertText(hdcMem, text, 10, textTopY)
  390. }
  391. // GDI 绘制到 DIB 不写 alpha 通道,此处按亮度补全 alpha,
  392. // 使图形/文字边缘抗锯齿平滑(文字区半透明)
  393. setAlphaFromLuma(bits, w, h)
  394. procSelectObject.Call(hdcMem, oldBmp)
  395. // UpdateLayeredWindow:显式传 pptDst(窗口屏幕左上角)与 psize(窗口尺寸),
  396. // 使 DIB 内容精确定位到窗口,避免 psize=nil 时因尺寸不匹配绘制失效。
  397. var blend blendfunction
  398. blend.BlendOp = 0 // AC_SRC_OVER
  399. blend.AlphaFormat = 0x01 // AC_SRC_ALPHA
  400. blend.SourceConstantAlpha = 255
  401. var dstPt point
  402. dstPt.X = wr.Left
  403. dstPt.Y = wr.Top
  404. var szPt point
  405. szPt.X = int32(w)
  406. szPt.Y = int32(h)
  407. procUpdateLayeredWindow.Call(overlay, hdcScreen,
  408. uintptr(unsafe.Pointer(&dstPt)), // pptDst = 窗口左上角屏幕坐标
  409. uintptr(unsafe.Pointer(&szPt)), // psize = 窗口尺寸
  410. hdcMem, 0, 0, uintptr(unsafe.Pointer(&blend)), 0x00000002 /*ULW_ALPHA*/)
  411. }
  412. // clearPixels 把 DIB 位图全部置为透明(ARGB=0)
  413. func clearPixels(bits unsafe.Pointer, w, h int) {
  414. if bits == nil {
  415. return
  416. }
  417. total := uintptr(w) * uintptr(h) * 4
  418. for i := uintptr(0); i+8 <= total; i += 8 {
  419. *(*uint64)(unsafe.Add(bits, i)) = 0
  420. }
  421. }
  422. // setAlphaFromLuma 按亮度补全 alpha:非透明像素的 alpha = max(r,g,b),
  423. // 文字区(y >= textTopY)乘以 3/4 实现半透明。
  424. func setAlphaFromLuma(bits unsafe.Pointer, w, h int) {
  425. if bits == nil {
  426. return
  427. }
  428. for y := 0; y < h; y++ {
  429. for x := 0; x < w; x++ {
  430. p := (*uint32)(unsafe.Add(bits, uintptr(y*w+x)*4))
  431. v := *p
  432. b := uint8(v)
  433. g := uint8(v >> 8)
  434. r := uint8(v >> 16)
  435. m := uint32(r)
  436. if uint32(g) > m {
  437. m = uint32(g)
  438. }
  439. if uint32(b) > m {
  440. m = uint32(b)
  441. }
  442. if m == 0 {
  443. *p = 0 // 保持透明
  444. continue
  445. }
  446. if y >= textTopY {
  447. m = m * 3 / 4 // 文字半透明
  448. }
  449. *p = v&0x00FFFFFF | (m << 24) // 写入 alpha
  450. }
  451. }
  452. }
  453. // drawCursorRing 在覆盖层坐标 (cx,cy) 画闪烁光标环(提醒样式)
  454. func drawCursorRing(hdc uintptr, cx, cy int32, flashOn bool) {
  455. color := flashColorNormal
  456. if flashOn {
  457. color = flashColorAlert
  458. }
  459. const r = 12
  460. for angle := 0; angle < 360; angle += 4 {
  461. rad := float64(angle) * 3.14159265 / 180.0
  462. px := cx + int32(float64(r)*cos(rad))
  463. py := cy + int32(float64(r)*sin(rad))
  464. procSetPixel.Call(hdc, uintptr(px), uintptr(py), uintptr(color))
  465. }
  466. // 中心小点
  467. procSetPixel.Call(hdc, uintptr(cx), uintptr(cy), uintptr(color))
  468. }
  469. // drawAlertText 在 (x,y) 绘制白色提示文字(抗锯齿边缘由 setAlphaFromLuma 处理)
  470. func drawAlertText(hdc uintptr, text string, x, y int32) {
  471. // CreateFontW 参数顺序:nHeight,nWidth,nEscapement,nOrientation,nWeight,
  472. // bItalic,bUnderline,bStrikeOut,iCharSet,iOutPrecision,iClipPrecision,
  473. // iQuality,iPitchAndFamily,pszFaceName。nHeight=-16 表示 16 像素高。
  474. var height int32 = -16
  475. hFont, _, _ := procCreateFontW.Call(
  476. uintptr(height), 0, 0, 0, 400, // 高16、粗体
  477. 0, 0, 0, 134, // 非斜体、GB2312_CHARSET
  478. 0, 0, 4, 2, // 抗锯齿、DEFAULT_PITCH
  479. 0, // lpszFace=NULL 用系统默认字体
  480. )
  481. if hFont == 0 {
  482. return
  483. }
  484. defer procDeleteObject.Call(hFont)
  485. oldFont, _, _ := procSelectObject.Call(hdc, hFont)
  486. procSetBkMode.Call(hdc, 1) // TRANSPARENT
  487. procSetTextColor.Call(hdc, 0x00FFFFFF) // 白
  488. procSetTextAlign.Call(hdc, 0) // TA_LEFT
  489. textPtr, _ := syscall.UTF16PtrFromString(text)
  490. procTextOutW.Call(hdc, uintptr(x), uintptr(y), uintptr(unsafe.Pointer(textPtr)), uintptr(len([]rune(text))))
  491. procSelectObject.Call(hdc, oldFont)
  492. }
  493. // wndProcCallback 覆盖层窗口过程:处理定时器(闪烁)与销毁
  494. func (s *mouseAlertState) wndProcCallback(hwnd uintptr, msg uint32, wParam uintptr, lParam uintptr) uintptr {
  495. switch msg {
  496. case wmTimer:
  497. if uintptr(wParam) == timerFlash {
  498. // 主动用 GetCursorPos 读取当前鼠标位置(窗口线程,安全)。
  499. // 不再依赖全局钩子回调更新 lastX/lastY(钩子回调在系统线程
  500. // 执行,任何 Go 状态操作都会导致进程退出,故仅做转发)。
  501. var pt point
  502. curRet, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
  503. s.mu.Lock()
  504. if s.active {
  505. if curRet != 0 {
  506. s.lastX, s.lastY = pt.X, pt.Y
  507. }
  508. // 到期判断(原先在钩子回调,现移到窗口线程)
  509. if !s.stopTime.IsZero() && time.Now().After(s.stopTime) {
  510. s.active = false
  511. s.mu.Unlock()
  512. procShowWindow.Call(hwnd, 0) // SW_HIDE
  513. return 0
  514. }
  515. s.flashOn = !s.flashOn
  516. text := s.text
  517. flashOn := s.flashOn
  518. x, y := s.lastX, s.lastY
  519. s.mu.Unlock()
  520. s.renderOverlay(hwnd, text, flashOn, x, y)
  521. return 0
  522. }
  523. s.mu.Unlock()
  524. }
  525. return 0
  526. case wmDestroy, wmNcDestroy:
  527. procKillTimer.Call(hwnd, timerFlash)
  528. return 0
  529. }
  530. ret, _, _ := procDefWindowProc.Call(hwnd, uintptr(msg), wParam, lParam)
  531. return ret
  532. }
  533. // procCallNextHookEx 钩子链传递
  534. var procCallNextHookEx = user32.NewProc("CallNextHookEx")
  535. // cos / sin 简单实现(避免依赖 math 的浮点精度问题)
  536. func cos(rad float64) float64 {
  537. r := rad
  538. for r > 3.14159265 {
  539. r -= 6.28318530
  540. }
  541. for r < -3.14159265 {
  542. r += 6.28318530
  543. }
  544. r2 := r * r
  545. return 1 - r2/2 + r2*r2/24 - r2*r2*r2/720
  546. }
  547. func sin(rad float64) float64 {
  548. r := rad
  549. for r > 3.14159265 {
  550. r -= 6.28318530
  551. }
  552. for r < -3.14159265 {
  553. r += 6.28318530
  554. }
  555. r2 := r * r
  556. return r - r2*r/6 + r2*r2*r/120 - r2*r2*r2*r/5040
  557. }