|
@@ -1,24 +1,34 @@
|
|
|
//go:build windows
|
|
//go:build windows
|
|
|
|
|
|
|
|
// ============================================================================
|
|
// ============================================================================
|
|
|
-// 系统级鼠标提醒(Windows 原生实现)
|
|
|
|
|
|
|
+// 系统级鼠标提醒(Windows 原生实现)—— 可靠方案 v2
|
|
|
//
|
|
//
|
|
|
-// 需求:平时鼠标是正常样式;有消息来(通过 API 触发)时,鼠标图标闪烁,
|
|
|
|
|
-// 且光标下方浮出简短的半透明提示文字,可覆盖任意窗口(系统级)。
|
|
|
|
|
|
|
+// 需求:平时鼠标是正常样式;有消息来(通过 API 触发)时,鼠标旁边浮出
|
|
|
|
|
+// 半透明气泡提示文字,跟随鼠标移动、闪烁,可覆盖任意窗口(系统级),
|
|
|
|
|
+// 默认 8 秒自动消退。
|
|
|
//
|
|
//
|
|
|
-// 实现原理:
|
|
|
|
|
-// 1. 全局低层鼠标钩子 WH_MOUSE_LL:捕获系统级鼠标移动,获取屏幕坐标。
|
|
|
|
|
-// 2. 一个置顶、透明、点击穿透的覆盖层窗口(WS_EX_LAYERED|WS_EX_TRANSPARENT
|
|
|
|
|
-// |WS_EX_TOOLWINDOW|WS_EX_NOACTIVATE),用 GDI UpdateLayeredWindow 在鼠标
|
|
|
|
|
-// 位置绘制闪烁光标环与下方半透明文字。
|
|
|
|
|
-// 3. 定时器驱动闪烁:普通光标样式 <-> 提醒光标样式交替。
|
|
|
|
|
-// 4. 提醒可自动消退(默认 8 秒),也可再调 API 停止。
|
|
|
|
|
|
|
+// v1 的失败原因:全局低层鼠标钩子 WH_MOUSE_LL 的回调由系统在非 Go 线程
|
|
|
|
|
+// 上调用,经 syscall.NewCallback 进入 Go runtime 时会导致进程静默退出;
|
|
|
|
|
+// 且 DIB 自绘 + UpdateLayeredWindow 尺寸不匹配时绘制失效。v2 全部移除:
|
|
|
|
|
+//
|
|
|
|
|
+// 1. 不装任何钩子(去掉唯一危险源),位置跟随改为窗口线程定时器轮询
|
|
|
|
|
+// GetCursorPos(GetMessage 消息循环所在线程,安全)。
|
|
|
|
|
+// 2. 用 SetLayeredWindowAttributes(LWA_COLORKEY) 把背景色抠成透明(背景透明,
|
|
|
|
|
+// 只留白色文字),WM_PAINT 用普通 GDI(FillRect + TextOutW)绘制气泡,
|
|
|
|
|
+// 不再用 DIB/UpdateLayeredWindow。
|
|
|
|
|
+// 3. 窗口过程 WndProc 的 syscall.NewCallback 由 DispatchMessageW 在同线程
|
|
|
|
|
+// 同步调用,Go runtime 完全支持(安全)。
|
|
|
|
|
+// 4. 超长文字:TextOutW 单行绘制 + 定时器推进 scrollX 偏移,实现水平循环滚动。
|
|
|
//
|
|
//
|
|
|
// 说明:本文件仅在 Windows 编译。其他平台见 mousealert_other.go 空实现。
|
|
// 说明:本文件仅在 Windows 编译。其他平台见 mousealert_other.go 空实现。
|
|
|
// ============================================================================
|
|
// ============================================================================
|
|
|
package main
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
import (
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "runtime"
|
|
|
|
|
+ "strings"
|
|
|
"sync"
|
|
"sync"
|
|
|
"syscall"
|
|
"syscall"
|
|
|
"time"
|
|
"time"
|
|
@@ -27,14 +37,13 @@ import (
|
|
|
|
|
|
|
|
// Windows API 常量
|
|
// Windows API 常量
|
|
|
const (
|
|
const (
|
|
|
- whMouseLL = 14 // WH_MOUSE_LL 全局低层鼠标钩子
|
|
|
|
|
-
|
|
|
|
|
wmTimer = 0x0113
|
|
wmTimer = 0x0113
|
|
|
wmDestroy = 0x0002
|
|
wmDestroy = 0x0002
|
|
|
wmNcDestroy = 0x0082
|
|
wmNcDestroy = 0x0082
|
|
|
|
|
+ wmPaint = 0x000F
|
|
|
|
|
+ wmRefresh = 0x8001 // 自定义:立即刷新(trigger 唤醒窗口线程)
|
|
|
|
|
|
|
|
wndExLayered = 0x00080000 // WS_EX_LAYERED 分层窗口(支持透明)
|
|
wndExLayered = 0x00080000 // WS_EX_LAYERED 分层窗口(支持透明)
|
|
|
- wndExTrans = 0x00000020 // WS_EX_TRANSPARENT 点击穿透
|
|
|
|
|
wndExTool = 0x00000080 // WS_EX_TOOLWINDOW 不占任务栏
|
|
wndExTool = 0x00000080 // WS_EX_TOOLWINDOW 不占任务栏
|
|
|
wndExTopMost = 0x00000008 // WS_EX_TOPMOST 置顶
|
|
wndExTopMost = 0x00000008 // WS_EX_TOPMOST 置顶
|
|
|
wndExNoActivate = 0x08000000 // WS_EX_NOACTIVATE 不抢焦点
|
|
wndExNoActivate = 0x08000000 // WS_EX_NOACTIVATE 不抢焦点
|
|
@@ -42,27 +51,68 @@ const (
|
|
|
swpNoActivate = 0x0010 // SWP_NOACTIVATE
|
|
swpNoActivate = 0x0010 // SWP_NOACTIVATE
|
|
|
swpShowWindow = 0x0040 // SWP_SHOWWINDOW
|
|
swpShowWindow = 0x0040 // SWP_SHOWWINDOW
|
|
|
|
|
|
|
|
- biRgb = 0 // BI_RGB
|
|
|
|
|
|
|
+ lwaAlpha = 0x2 // LWA_ALPHA:SetLayeredWindowAttributes 设置整体 alpha
|
|
|
|
|
+ lwaColorKey = 0x1 // LWA_COLORKEY:把指定颜色像素抠成透明(背景透明)
|
|
|
|
|
|
|
|
- timerFlash = 1 // 闪烁定时器 ID
|
|
|
|
|
|
|
+ timerFollow = 1 // 位置跟随 + 闪烁定时器 ID
|
|
|
|
|
|
|
|
- overlayWidth = 220
|
|
|
|
|
- overlayHeight = 90
|
|
|
|
|
- textTopY = 26 // 文字起始 Y 坐标(覆盖层内),用于区分半透明文字区
|
|
|
|
|
|
|
+ overlayWidth = 200
|
|
|
|
|
+ overlayHeight = 54
|
|
|
|
|
|
|
|
alertDuration = 8 * time.Second
|
|
alertDuration = 8 * time.Second
|
|
|
- flashIntervalMs = 250 // 闪烁周期 250ms
|
|
|
|
|
-)
|
|
|
|
|
-
|
|
|
|
|
-// 光标提示样式:普通状态 vs 提醒状态(闪烁交替)
|
|
|
|
|
-const (
|
|
|
|
|
- flashColorNormal = 0x00FFFFFF // 普通:白
|
|
|
|
|
- flashColorAlert = 0x00FF5050 // 提醒:红
|
|
|
|
|
|
|
+ followIntervalMs = 60 // 跟随/闪烁轮询间隔 60ms
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
// hwndTopMost HWND_TOPMOST = (HWND)-1
|
|
// hwndTopMost HWND_TOPMOST = (HWND)-1
|
|
|
const hwndTopMost uintptr = ^uintptr(0)
|
|
const hwndTopMost uintptr = ^uintptr(0)
|
|
|
|
|
|
|
|
|
|
+// 气泡背景色(深蓝黑):COLORREF = 0x00BBGGRR(LWA_COLORKEY 抠成透明,只留文字)
|
|
|
|
|
+const bgColorRef = 0x001E1E3C
|
|
|
|
|
+
|
|
|
|
|
+// cfgDurationOrDefault 返回配置的显示时长(秒),非法时用默认 8 秒
|
|
|
|
|
+func cfgDurationOrDefault(sec int) time.Duration {
|
|
|
|
|
+ if sec < 2 || sec > 60 {
|
|
|
|
|
+ sec = 8
|
|
|
|
|
+ }
|
|
|
|
|
+ return time.Duration(sec) * time.Second
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// estimateTextWidth 估算文字像素宽度(微软雅黑:全角/中文≈字号,半角/ASCII≈0.55*字号)。
|
|
|
|
|
+// 用于在触发时预判文字是否超长,从而智能延长显示时长。
|
|
|
|
|
+func estimateTextWidth(text string, fontSize int32) int32 {
|
|
|
|
|
+ if text == "" || fontSize <= 0 {
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+ var w float32
|
|
|
|
|
+ for _, r := range text {
|
|
|
|
|
+ if r < 0x2E80 { // 半角 / ASCII / 拉丁
|
|
|
|
|
+ w += 0.55 * float32(fontSize)
|
|
|
|
|
+ } else { // 全角 / 中日韩
|
|
|
|
|
+ w += float32(fontSize)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return int32(w)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// hexColorToRef 把 #RRGGBB 或 #RGB 颜色字符串转 COLORREF(0x00BBGGRR)。
|
|
|
|
|
+// 解析失败时回退 fallback。
|
|
|
|
|
+func hexColorToRef(s string, fallback uintptr) uintptr {
|
|
|
|
|
+ s = strings.TrimSpace(s)
|
|
|
|
|
+ if len(s) == 7 && s[0] == '#' {
|
|
|
|
|
+ var r, g, b uint32
|
|
|
|
|
+ if _, err := fmt.Sscanf(s, "#%02x%02x%02x", &r, &g, &b); err == nil {
|
|
|
|
|
+ return uintptr(r | g<<8 | b<<16)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(s) == 4 && s[0] == '#' {
|
|
|
|
|
+ var r, g, b uint32
|
|
|
|
|
+ if _, err := fmt.Sscanf(s, "#%1x%1x%1x", &r, &g, &b); err == nil {
|
|
|
|
|
+ return uintptr(r*17 | g*17<<8 | b*17<<16)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return fallback
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------
|
|
|
// Windows 结构体与函数指针声明(用 syscall 直接调用,避免引入额外依赖)
|
|
// Windows 结构体与函数指针声明(用 syscall 直接调用,避免引入额外依赖)
|
|
|
// ------------------------------------------------------------------------
|
|
// ------------------------------------------------------------------------
|
|
@@ -72,7 +122,7 @@ type point struct {
|
|
|
Y int32
|
|
Y int32
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// rect Windows RECT 结构(GetClientRect 输出)
|
|
|
|
|
|
|
+// rect Windows RECT 结构(GetWindowRect / FillRect 输出)
|
|
|
type rect struct {
|
|
type rect struct {
|
|
|
Left int32
|
|
Left int32
|
|
|
Top int32
|
|
Top int32
|
|
@@ -90,14 +140,6 @@ type msg struct {
|
|
|
LPrivate uint32
|
|
LPrivate uint32
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type msllhookstruct struct {
|
|
|
|
|
- Pt point
|
|
|
|
|
- MouseData uint32
|
|
|
|
|
- Flags uint32
|
|
|
|
|
- Time uint32
|
|
|
|
|
- DWExtraInfo uintptr
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
type wndclassex struct {
|
|
type wndclassex struct {
|
|
|
CbSize uint32
|
|
CbSize uint32
|
|
|
Style uint32
|
|
Style uint32
|
|
@@ -113,25 +155,20 @@ type wndclassex struct {
|
|
|
HIconSm uintptr
|
|
HIconSm uintptr
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type bitmapinfoheader struct {
|
|
|
|
|
- BiSize uint32
|
|
|
|
|
- BiWidth int32
|
|
|
|
|
- BiHeight int32
|
|
|
|
|
- BiPlanes uint16
|
|
|
|
|
- BiBitCount uint16
|
|
|
|
|
- BiCompression uint32
|
|
|
|
|
- BiSizeImage uint32
|
|
|
|
|
- BiXPelsPerMeter int32
|
|
|
|
|
- BiYPelsPerMeter int32
|
|
|
|
|
- BiClrUsed uint32
|
|
|
|
|
- BiClrImportant uint32
|
|
|
|
|
|
|
+// paintstruct PAINTSTRUCT(BeginPaint / EndPaint 输出)
|
|
|
|
|
+type paintstruct struct {
|
|
|
|
|
+ Hdc uintptr
|
|
|
|
|
+ FErase int32
|
|
|
|
|
+ RcPaint rect
|
|
|
|
|
+ FRestore int32
|
|
|
|
|
+ FIncUpdate int32
|
|
|
|
|
+ RGBReserved [32]byte
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-type blendfunction struct {
|
|
|
|
|
- BlendOp byte
|
|
|
|
|
- BlendFlags byte
|
|
|
|
|
- SourceConstantAlpha byte
|
|
|
|
|
- AlphaFormat byte
|
|
|
|
|
|
|
+// size SIZE 结构(GetTextExtentPoint32W 输出:文字像素尺寸)
|
|
|
|
|
+type size struct {
|
|
|
|
|
+ Cx int32
|
|
|
|
|
+ Cy int32
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 延迟加载的 user32.dll / gdi32.dll / kernel32.dll 函数
|
|
// 延迟加载的 user32.dll / gdi32.dll / kernel32.dll 函数
|
|
@@ -140,8 +177,6 @@ var (
|
|
|
gdi32 = syscall.NewLazyDLL("gdi32.dll")
|
|
gdi32 = syscall.NewLazyDLL("gdi32.dll")
|
|
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
|
|
|
|
|
|
- procSetWindowsHookEx = user32.NewProc("SetWindowsHookExW")
|
|
|
|
|
- procUnhookWindowsHook = user32.NewProc("UnhookWindowsHookEx")
|
|
|
|
|
procGetMessage = user32.NewProc("GetMessageW")
|
|
procGetMessage = user32.NewProc("GetMessageW")
|
|
|
procTranslateMessage = user32.NewProc("TranslateMessage")
|
|
procTranslateMessage = user32.NewProc("TranslateMessage")
|
|
|
procDispatchMessage = user32.NewProc("DispatchMessageW")
|
|
procDispatchMessage = user32.NewProc("DispatchMessageW")
|
|
@@ -153,25 +188,28 @@ var (
|
|
|
procKillTimer = user32.NewProc("KillTimer")
|
|
procKillTimer = user32.NewProc("KillTimer")
|
|
|
procShowWindow = user32.NewProc("ShowWindow")
|
|
procShowWindow = user32.NewProc("ShowWindow")
|
|
|
procSetWindowPos = user32.NewProc("SetWindowPos")
|
|
procSetWindowPos = user32.NewProc("SetWindowPos")
|
|
|
- procUpdateLayeredWindow = user32.NewProc("UpdateLayeredWindow")
|
|
|
|
|
- procGetDC = user32.NewProc("GetDC")
|
|
|
|
|
- procReleaseDC = user32.NewProc("ReleaseDC")
|
|
|
|
|
procGetCursorPos = user32.NewProc("GetCursorPos")
|
|
procGetCursorPos = user32.NewProc("GetCursorPos")
|
|
|
procPostMessage = user32.NewProc("PostMessageW")
|
|
procPostMessage = user32.NewProc("PostMessageW")
|
|
|
- procGetClientRect = user32.NewProc("GetClientRect")
|
|
|
|
|
- procGetWindowRect = user32.NewProc("GetWindowRect")
|
|
|
|
|
-
|
|
|
|
|
- procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
|
|
|
|
|
- procDeleteDC = gdi32.NewProc("DeleteDC")
|
|
|
|
|
- procCreateDIBSection = gdi32.NewProc("CreateDIBSection")
|
|
|
|
|
- procSelectObject = gdi32.NewProc("SelectObject")
|
|
|
|
|
- procDeleteObject = gdi32.NewProc("DeleteObject")
|
|
|
|
|
- procSetBkMode = gdi32.NewProc("SetBkMode")
|
|
|
|
|
- procSetTextColor = gdi32.NewProc("SetTextColor")
|
|
|
|
|
- procSetTextAlign = gdi32.NewProc("SetTextAlign")
|
|
|
|
|
- procTextOutW = gdi32.NewProc("TextOutW")
|
|
|
|
|
- procCreateFontW = gdi32.NewProc("CreateFontW")
|
|
|
|
|
- procSetPixel = gdi32.NewProc("SetPixel")
|
|
|
|
|
|
|
+ procSetLayeredWindowAttr = user32.NewProc("SetLayeredWindowAttributes")
|
|
|
|
|
+ procBeginPaint = user32.NewProc("BeginPaint")
|
|
|
|
|
+ procEndPaint = user32.NewProc("EndPaint")
|
|
|
|
|
+ procInvalidateRect = user32.NewProc("InvalidateRect")
|
|
|
|
|
+ procFillRect = user32.NewProc("FillRect") // 注意:FillRect 在 user32.dll,不在 gdi32.dll
|
|
|
|
|
+ procDrawTextW = user32.NewProc("DrawTextW")
|
|
|
|
|
+
|
|
|
|
|
+ procCreateSolidBrush = gdi32.NewProc("CreateSolidBrush")
|
|
|
|
|
+ procDeleteObject = gdi32.NewProc("DeleteObject")
|
|
|
|
|
+ procSetBkMode = gdi32.NewProc("SetBkMode")
|
|
|
|
|
+ procSetTextColor = gdi32.NewProc("SetTextColor")
|
|
|
|
|
+ procGetStockObject = gdi32.NewProc("GetStockObject")
|
|
|
|
|
+ procSelectObject = gdi32.NewProc("SelectObject")
|
|
|
|
|
+ procCreateFontW = gdi32.NewProc("CreateFontW")
|
|
|
|
|
+ procTextOutW = gdi32.NewProc("TextOutW") // 单行文字输出(超长滚动用)
|
|
|
|
|
+ procGetTextExtentPoint32W = gdi32.NewProc("GetTextExtentPoint32W") // 测量文字宽度(判断是否超长)
|
|
|
|
|
+ procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC") // 双缓冲:创建内存 DC
|
|
|
|
|
+ procCreateCompatibleBitmap = gdi32.NewProc("CreateCompatibleBitmap") // 双缓冲:创建兼容位图
|
|
|
|
|
+ procBitBlt = gdi32.NewProc("BitBlt") // 双缓冲:整块拷贝上屏
|
|
|
|
|
+ procDeleteDC = gdi32.NewProc("DeleteDC") // 双缓冲:删除内存 DC
|
|
|
|
|
|
|
|
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
|
|
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
|
|
|
)
|
|
)
|
|
@@ -184,42 +222,93 @@ var (
|
|
|
type mouseAlertState struct {
|
|
type mouseAlertState struct {
|
|
|
mu sync.Mutex
|
|
mu sync.Mutex
|
|
|
|
|
|
|
|
- started bool // 工作线程是否已启动(持久化,仅启动一次)
|
|
|
|
|
- hook uintptr
|
|
|
|
|
- overlayWnd uintptr
|
|
|
|
|
- active bool
|
|
|
|
|
- text string
|
|
|
|
|
- flashOn bool
|
|
|
|
|
|
|
+ started bool // 工作线程是否已启动(持久化,仅启动一次)
|
|
|
|
|
+ overlayWnd uintptr
|
|
|
|
|
+ active bool
|
|
|
|
|
+ text string
|
|
|
|
|
+ flashOn bool
|
|
|
lastX, lastY int32
|
|
lastX, lastY int32
|
|
|
- stopTime time.Time
|
|
|
|
|
- hookProc uintptr
|
|
|
|
|
- wndProc uintptr
|
|
|
|
|
- classOK bool
|
|
|
|
|
|
|
+ stopTime time.Time
|
|
|
|
|
+ wndProc uintptr
|
|
|
|
|
+ classOK bool
|
|
|
|
|
+ bgBrush uintptr
|
|
|
|
|
+ scrollX int32 // 超长文字水平滚动偏移(px),定时器推进
|
|
|
|
|
+ textWidth int32 // 当前文字像素宽度(>可用宽度时滚动)
|
|
|
|
|
+ textGap int32 // 头尾相联间隔(两个空格宽度 px),滚动周期 = 文字宽 + 间隔
|
|
|
|
|
+
|
|
|
|
|
+ // 运行时配置(triggerMouseAlert 时从设置加载)
|
|
|
|
|
+ cfgWidth int32 // 气泡窗口宽度(px)
|
|
|
|
|
+ cfgHeight int32 // 气泡窗口高度(px,按字号自适应)
|
|
|
|
|
+ cfgFontSize int32 // 字号(px)
|
|
|
|
|
+ cfgSpeed int // 超长文字滚动速度(px/s)
|
|
|
|
|
+ cfgDuration time.Duration // 显示时长
|
|
|
|
|
+ cfgTextColor uintptr // 文字颜色(COLORREF)
|
|
|
|
|
+ cfgBgColor uintptr // 气泡背景色(COLORKEY 抠透明)
|
|
|
|
|
+ cfgOutlineColor uintptr // 文字描边颜色(COLORREF),形成反差
|
|
|
|
|
+ cfgOutlineWidth int32 // 文字描边宽度(px),0 关闭
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var mouseAlert = &mouseAlertState{}
|
|
var mouseAlert = &mouseAlertState{}
|
|
|
|
|
|
|
|
-// triggerMouseAlert 触发鼠标提醒:显示闪烁光标 + 光标下方透明文字。
|
|
|
|
|
|
|
+// triggerMouseAlert 触发鼠标提醒:鼠标旁浮出半透明气泡提示,跟随鼠标闪烁。
|
|
|
// 文字为空时用默认提示。重复触发会刷新提醒计时并立即显示。
|
|
// 文字为空时用默认提示。重复触发会刷新提醒计时并立即显示。
|
|
|
//
|
|
//
|
|
|
-// 定位策略:主动用 GetCursorPos 读取当前鼠标位置并记录为 lastX/lastY,
|
|
|
|
|
-// 再通过 PostMessage 唤醒窗口线程立即重绘,避免依赖全局钩子回调(钩子
|
|
|
|
|
-// 回调因负担重可能被系统卸载,导致窗口停留在旧位置不跟随鼠标)。
|
|
|
|
|
|
|
+// 定位策略:窗口线程的定时器主动用 GetCursorPos 读取当前鼠标位置并跟随,
|
|
|
|
|
+// 不依赖任何全局钩子回调(钩子回调在系统线程执行,曾导致进程退出)。
|
|
|
func triggerMouseAlert(text string) {
|
|
func triggerMouseAlert(text string) {
|
|
|
if text == "" {
|
|
if text == "" {
|
|
|
text = "有新消息提醒"
|
|
text = "有新消息提醒"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 读取当前鼠标位置,供窗口重绘时定位(不依赖钩子回调)
|
|
|
|
|
|
|
+ // 从设置加载鼠标提醒配置(字号/宽度/速度/颜色/时长)
|
|
|
|
|
+ cfg := loadSettings().MouseAlert
|
|
|
|
|
+
|
|
|
var pt point
|
|
var pt point
|
|
|
curRet, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
|
|
curRet, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
|
|
|
hasCursor := curRet != 0
|
|
hasCursor := curRet != 0
|
|
|
|
|
|
|
|
|
|
+ // 智能延长显示时长:文字超长(需滚动)时,追加滚动一圈所需时间,保证能看完
|
|
|
|
|
+ cfgWidth := int32(cfg.Width)
|
|
|
|
|
+ if cfgWidth <= 0 {
|
|
|
|
|
+ cfgWidth = overlayWidth
|
|
|
|
|
+ }
|
|
|
|
|
+ cfgFontSize := int32(cfg.FontSize)
|
|
|
|
|
+ if cfgFontSize <= 0 {
|
|
|
|
|
+ cfgFontSize = DefaultMouseAlertFontSize
|
|
|
|
|
+ }
|
|
|
|
|
+ estW := estimateTextWidth(text, cfgFontSize)
|
|
|
|
|
+ baseDur := cfgDurationOrDefault(cfg.Duration)
|
|
|
|
|
+ totalDur := baseDur
|
|
|
|
|
+ if estW > cfgWidth-24 && cfg.Speed > 0 {
|
|
|
|
|
+ // 超出可用宽度的部分需滚动,所需时间 = 超出像素 / 速度
|
|
|
|
|
+ extra := estW - (cfgWidth - 24)
|
|
|
|
|
+ need := time.Duration(float64(extra) / float64(cfg.Speed) * float64(time.Second))
|
|
|
|
|
+ totalDur = baseDur + need
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
mouseAlert.mu.Lock()
|
|
mouseAlert.mu.Lock()
|
|
|
mouseAlert.text = text
|
|
mouseAlert.text = text
|
|
|
mouseAlert.active = true
|
|
mouseAlert.active = true
|
|
|
mouseAlert.flashOn = true
|
|
mouseAlert.flashOn = true
|
|
|
- mouseAlert.stopTime = time.Now().Add(alertDuration)
|
|
|
|
|
|
|
+ mouseAlert.stopTime = time.Now().Add(totalDur)
|
|
|
|
|
+ mouseAlert.scrollX = 0 // 新消息从头开始滚动
|
|
|
|
|
+ mouseAlert.textWidth = 0 // 强制重新测量
|
|
|
|
|
+ // 缓存本次提醒的显示配置
|
|
|
|
|
+ mouseAlert.cfgWidth = cfgWidth
|
|
|
|
|
+ mouseAlert.cfgFontSize = cfgFontSize
|
|
|
|
|
+ mouseAlert.cfgSpeed = cfg.Speed
|
|
|
|
|
+ mouseAlert.cfgDuration = totalDur
|
|
|
|
|
+ mouseAlert.cfgTextColor = hexColorToRef(cfg.Color, 0x00FFFFFF) // 默认白字
|
|
|
|
|
+ mouseAlert.cfgBgColor = bgColorRef
|
|
|
|
|
+ mouseAlert.cfgOutlineColor = hexColorToRef(cfg.OutlineColor, 0x00000000) // 默认黑边
|
|
|
|
|
+ mouseAlert.cfgOutlineWidth = int32(cfg.OutlineWidth)
|
|
|
|
|
+ // 头尾相联间隔 = 两个空格宽度(约 0.3*字号 每个),提前缓存供滚动周期使用
|
|
|
|
|
+ mouseAlert.textGap = int32(float32(cfgFontSize) * 0.3 * 2)
|
|
|
|
|
+ if mouseAlert.textGap < 4 {
|
|
|
|
|
+ mouseAlert.textGap = 4
|
|
|
|
|
+ }
|
|
|
|
|
+ // 气泡高度按字号自适应:字号 + 上下留白 + 内边距
|
|
|
|
|
+ mouseAlert.cfgHeight = mouseAlert.cfgFontSize + 34
|
|
|
if hasCursor {
|
|
if hasCursor {
|
|
|
mouseAlert.lastX, mouseAlert.lastY = pt.X, pt.Y
|
|
mouseAlert.lastX, mouseAlert.lastY = pt.X, pt.Y
|
|
|
}
|
|
}
|
|
@@ -233,15 +322,13 @@ func triggerMouseAlert(text string) {
|
|
|
go mouseAlertLoop()
|
|
go mouseAlertLoop()
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- // 已启动:唤醒窗口线程立即重绘(窗口线程负责 SetWindowPos/绘制,
|
|
|
|
|
- // 避免跨线程操作窗口;若窗口尚在初始化,线程建窗后也会主动绘制)
|
|
|
|
|
|
|
+ // 已启动:唤醒窗口线程立即刷新(窗口线程负责 SetWindowPos/绘制)
|
|
|
if overlay != 0 {
|
|
if overlay != 0 {
|
|
|
- procShowWindow.Call(overlay, 5) // SW_SHOW
|
|
|
|
|
- procPostMessage.Call(overlay, wmTimer, timerFlash, 0)
|
|
|
|
|
|
|
+ procPostMessage.Call(overlay, wmRefresh, 0, 0)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// stopMouseAlert 立即停止鼠标提醒(隐藏覆盖层;钩子保持常驻)
|
|
|
|
|
|
|
+// stopMouseAlert 立即停止鼠标提醒(隐藏气泡;线程保持常驻)
|
|
|
func stopMouseAlert() {
|
|
func stopMouseAlert() {
|
|
|
mouseAlert.mu.Lock()
|
|
mouseAlert.mu.Lock()
|
|
|
mouseAlert.active = false
|
|
mouseAlert.active = false
|
|
@@ -252,13 +339,20 @@ func stopMouseAlert() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// mouseAlertLoop 常驻工作线程:注册窗口类、安装全局钩子、创建覆盖层窗口,
|
|
|
|
|
-// 进入消息循环。低层钩子必须有消息循环,故全程运行在独立线程。
|
|
|
|
|
|
|
+// mouseAlertLoop 常驻工作线程:注册窗口类、创建半透明置顶气泡窗口、
|
|
|
|
|
+// 启动跟随/闪烁定时器,进入消息循环。
|
|
|
func mouseAlertLoop() {
|
|
func mouseAlertLoop() {
|
|
|
|
|
+ // 关键:Go 的 goroutine 会在 OS 线程间迁移。窗口消息循环(CreateWindowEx +
|
|
|
|
|
+ // GetMessage + DispatchMessage)必须固定在同一个 OS 线程上,否则 WM_TIMER /
|
|
|
|
|
+ // WM_PAINT 等消息投递到创建窗口的线程,而 GetMessage 在另一线程永远收不到,
|
|
|
|
|
+ // 导致窗口出现后卡死(不跟随鼠标、不重绘、无文字)。LockOSThread 锁定本
|
|
|
|
|
+ // goroutine 到当前 OS 线程,保证消息循环与窗口同线程。
|
|
|
|
|
+ runtime.LockOSThread()
|
|
|
|
|
+
|
|
|
hInst, _, _ := procGetModuleHandleW.Call(0)
|
|
hInst, _, _ := procGetModuleHandleW.Call(0)
|
|
|
|
|
|
|
|
mouseAlert.mu.Lock()
|
|
mouseAlert.mu.Lock()
|
|
|
- className, _ := syscall.UTF16PtrFromString("TengTuMouseAlertOverlay")
|
|
|
|
|
|
|
+ className, _ := syscall.UTF16PtrFromString("TengTuMouseAlertBubble")
|
|
|
wc := wndclassex{
|
|
wc := wndclassex{
|
|
|
CbSize: uint32(unsafe.Sizeof(wndclassex{})),
|
|
CbSize: uint32(unsafe.Sizeof(wndclassex{})),
|
|
|
LpfnWndProc: mouseAlert.wndProcAddr(),
|
|
LpfnWndProc: mouseAlert.wndProcAddr(),
|
|
@@ -269,47 +363,65 @@ func mouseAlertLoop() {
|
|
|
atom, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
|
|
atom, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
|
|
|
if atom != 0 {
|
|
if atom != 0 {
|
|
|
mouseAlert.classOK = true
|
|
mouseAlert.classOK = true
|
|
|
|
|
+ } else {
|
|
|
|
|
+ fmt.Fprintf(os.Stderr, "[mousealert] RegisterClassEx failed (classOK=%v)\n", mouseAlert.classOK)
|
|
|
}
|
|
}
|
|
|
mouseAlert.mu.Unlock()
|
|
mouseAlert.mu.Unlock()
|
|
|
|
|
|
|
|
- // 安装全局低层鼠标钩子
|
|
|
|
|
- hookProc := mouseAlert.hookProcAddr()
|
|
|
|
|
- hHook, _, _ := procSetWindowsHookEx.Call(whMouseLL, hookProc, 0, 0)
|
|
|
|
|
- if hHook != 0 {
|
|
|
|
|
- mouseAlert.mu.Lock()
|
|
|
|
|
- mouseAlert.hook = hHook
|
|
|
|
|
- mouseAlert.mu.Unlock()
|
|
|
|
|
|
|
+ // 创建半透明置顶气泡窗口(不抢焦点、不占任务栏)
|
|
|
|
|
+ // dwStyle 必须含 WS_POPUP(0x80000000):无边框无标题的弹出窗口,
|
|
|
|
|
+ // 否则 CreateWindowEx 可能失败或窗口不可见。
|
|
|
|
|
+ style := uintptr(wndExLayered | wndExTool | wndExTopMost | wndExNoActivate)
|
|
|
|
|
+ mouseAlert.mu.Lock()
|
|
|
|
|
+ w, h := mouseAlert.cfgWidth, mouseAlert.cfgHeight
|
|
|
|
|
+ if w <= 0 {
|
|
|
|
|
+ w = overlayWidth // 首次触发配置尚未缓存时用默认
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 创建覆盖层窗口(初始 220x90,位置随后由钩子/定时器设置)
|
|
|
|
|
- style := uintptr(wndExLayered | wndExTrans | wndExTool | wndExTopMost | wndExNoActivate)
|
|
|
|
|
|
|
+ if h <= 0 {
|
|
|
|
|
+ h = overlayHeight
|
|
|
|
|
+ }
|
|
|
|
|
+ mouseAlert.mu.Unlock()
|
|
|
overlay, _, _ := procCreateWindowEx.Call(
|
|
overlay, _, _ := procCreateWindowEx.Call(
|
|
|
style,
|
|
style,
|
|
|
uintptr(unsafe.Pointer(className)),
|
|
uintptr(unsafe.Pointer(className)),
|
|
|
0,
|
|
0,
|
|
|
- 0, 0, overlayWidth, overlayHeight, 0,
|
|
|
|
|
|
|
+ uintptr(0x80000000) /* WS_POPUP */, 0, 0, uintptr(w), uintptr(h), 0,
|
|
|
0, 0, hInst, 0,
|
|
0, 0, hInst, 0,
|
|
|
)
|
|
)
|
|
|
|
|
+ if overlay == 0 {
|
|
|
|
|
+ fmt.Fprintf(os.Stderr, "[mousealert] CreateWindowEx failed\n")
|
|
|
|
|
+ }
|
|
|
mouseAlert.mu.Lock()
|
|
mouseAlert.mu.Lock()
|
|
|
mouseAlert.overlayWnd = overlay
|
|
mouseAlert.overlayWnd = overlay
|
|
|
|
|
+ if overlay != 0 {
|
|
|
|
|
+ // 背景透明(LWA_COLORKEY):把背景色 bgColorRef 的像素抠成透明,只留白色
|
|
|
|
|
+ // 文字。WM_PAINT 用普通 GDI 绘制气泡(背景+文字)。
|
|
|
|
|
+ // 注意:不要用 UpdateLayeredWindow——它会接管分层窗口内容,导致
|
|
|
|
|
+ // WM_PAINT 完全失效(这正是当前「完全不显示」的根因)。
|
|
|
|
|
+ bg := mouseAlert.cfgBgColor
|
|
|
|
|
+ if bg == 0 {
|
|
|
|
|
+ bg = bgColorRef
|
|
|
|
|
+ }
|
|
|
|
|
+ brush, _, _ := procCreateSolidBrush.Call(bg)
|
|
|
|
|
+ mouseAlert.bgBrush = brush
|
|
|
|
|
+ // LWA_COLORKEY:crKey=背景色,bAlpha=0(忽略),dwFlags=LWA_COLORKEY
|
|
|
|
|
+ procSetLayeredWindowAttr.Call(overlay, bg, 0, uintptr(lwaColorKey))
|
|
|
|
|
+ }
|
|
|
mouseAlert.mu.Unlock()
|
|
mouseAlert.mu.Unlock()
|
|
|
|
|
|
|
|
- // 启动闪烁定时器
|
|
|
|
|
|
|
+ // 启动跟随/闪烁定时器(窗口线程内轮询鼠标位置)
|
|
|
if overlay != 0 {
|
|
if overlay != 0 {
|
|
|
- procSetTimer.Call(overlay, timerFlash, flashIntervalMs, 0)
|
|
|
|
|
|
|
+ procSetTimer.Call(overlay, timerFollow, followIntervalMs, 0)
|
|
|
|
|
|
|
|
- // 建窗后立即用当前鼠标位置定位并重绘一次,保证首次触发即可见,
|
|
|
|
|
- // 不依赖全局钩子回调(钩子回调在系统线程执行,仅做转发)。
|
|
|
|
|
|
|
+ // 建窗后立即用当前鼠标位置定位并重绘一次,保证首次触发即可见
|
|
|
var pt point
|
|
var pt point
|
|
|
curRet, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
|
|
curRet, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
|
|
|
if curRet != 0 {
|
|
if curRet != 0 {
|
|
|
mouseAlert.mu.Lock()
|
|
mouseAlert.mu.Lock()
|
|
|
if mouseAlert.active {
|
|
if mouseAlert.active {
|
|
|
mouseAlert.lastX, mouseAlert.lastY = pt.X, pt.Y
|
|
mouseAlert.lastX, mouseAlert.lastY = pt.X, pt.Y
|
|
|
- text := mouseAlert.text
|
|
|
|
|
- flashOn := mouseAlert.flashOn
|
|
|
|
|
mouseAlert.mu.Unlock()
|
|
mouseAlert.mu.Unlock()
|
|
|
- mouseAlert.renderOverlay(overlay, text, flashOn, pt.X, pt.Y)
|
|
|
|
|
|
|
+ mouseAlert.positionAndDraw(overlay, pt.X, pt.Y)
|
|
|
} else {
|
|
} else {
|
|
|
mouseAlert.mu.Unlock()
|
|
mouseAlert.mu.Unlock()
|
|
|
}
|
|
}
|
|
@@ -329,27 +441,19 @@ func mouseAlertLoop() {
|
|
|
|
|
|
|
|
// 清理
|
|
// 清理
|
|
|
if overlay != 0 {
|
|
if overlay != 0 {
|
|
|
|
|
+ procKillTimer.Call(overlay, timerFollow)
|
|
|
procDestroyWindow.Call(overlay)
|
|
procDestroyWindow.Call(overlay)
|
|
|
}
|
|
}
|
|
|
- if hHook != 0 {
|
|
|
|
|
- procUnhookWindowsHook.Call(hHook)
|
|
|
|
|
- }
|
|
|
|
|
mouseAlert.mu.Lock()
|
|
mouseAlert.mu.Lock()
|
|
|
- mouseAlert.hook = 0
|
|
|
|
|
|
|
+ if mouseAlert.bgBrush != 0 {
|
|
|
|
|
+ procDeleteObject.Call(mouseAlert.bgBrush)
|
|
|
|
|
+ mouseAlert.bgBrush = 0
|
|
|
|
|
+ }
|
|
|
mouseAlert.overlayWnd = 0
|
|
mouseAlert.overlayWnd = 0
|
|
|
mouseAlert.mu.Unlock()
|
|
mouseAlert.mu.Unlock()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// hookProcAddr 返回全局钩子回调地址(保持 Go 函数引用)
|
|
|
|
|
-func (s *mouseAlertState) hookProcAddr() uintptr {
|
|
|
|
|
- if s.hookProc != 0 {
|
|
|
|
|
- return s.hookProc
|
|
|
|
|
- }
|
|
|
|
|
- s.hookProc = syscall.NewCallback(s.hookCallback)
|
|
|
|
|
- return s.hookProc
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// wndProcAddr 返回覆盖层窗口过程地址
|
|
|
|
|
|
|
+// wndProcAddr 返回覆盖层窗口过程地址(保持 Go 函数引用,防 GC)
|
|
|
func (s *mouseAlertState) wndProcAddr() uintptr {
|
|
func (s *mouseAlertState) wndProcAddr() uintptr {
|
|
|
if s.wndProc != 0 {
|
|
if s.wndProc != 0 {
|
|
|
return s.wndProc
|
|
return s.wndProc
|
|
@@ -358,269 +462,270 @@ func (s *mouseAlertState) wndProcAddr() uintptr {
|
|
|
return s.wndProc
|
|
return s.wndProc
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// hookCallback 全局低层鼠标钩子回调:仅转发给下一钩子,不访问任何 Go 状态。
|
|
|
|
|
-//
|
|
|
|
|
-// 重要:此回调在系统创建的钩子线程上执行,且 CGO 启用时 syscall.NewCallback
|
|
|
|
|
-// 的回调从该线程进入 Go runtime。此前的实现在此处访问 s.mu(sync.Mutex 锁)、
|
|
|
|
|
-// 调用 time.Now()、procShowWindow 等 Go 状态操作,会导致进程在钩子回调触发时
|
|
|
|
|
-// 静默退出(app.Run 返回,exit 0)。故此处只做 CallNextHookEx 转发,不做任何
|
|
|
|
|
-// 其他工作;鼠标位置跟踪与到期判断全部移到窗口线程的定时器(wndProcCallback
|
|
|
|
|
-// 的 wmTimer 分支,用 GetCursorPos 主动读取),避免在钩子线程做 Go 操作。
|
|
|
|
|
-func (s *mouseAlertState) hookCallback(nCode int, wParam uintptr, lParam uintptr) uintptr {
|
|
|
|
|
- ret, _, _ := procCallNextHookEx.Call(0, uintptr(nCode), wParam, lParam)
|
|
|
|
|
- return ret
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// wndProcCallback 气泡窗口过程:处理跟随/闪烁定时器、自定义刷新、绘制与销毁。
|
|
|
|
|
+// 此回调由 DispatchMessageW 在消息循环线程同步调用,是 Go runtime 支持的安全路径。
|
|
|
|
|
+func (s *mouseAlertState) wndProcCallback(hwnd uintptr, msg uint32, wParam uintptr, lParam uintptr) uintptr {
|
|
|
|
|
+ switch msg {
|
|
|
|
|
+ case wmRefresh, wmTimer:
|
|
|
|
|
+ if msg == wmRefresh || uintptr(wParam) == timerFollow {
|
|
|
|
|
+ // 主动用 GetCursorPos 读取当前鼠标位置(窗口线程,安全)
|
|
|
|
|
+ var pt point
|
|
|
|
|
+ curRet, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
|
|
|
|
|
+ s.mu.Lock()
|
|
|
|
|
+ if !s.active {
|
|
|
|
|
+ s.mu.Unlock()
|
|
|
|
|
+ procShowWindow.Call(hwnd, 0) // SW_HIDE
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+ if curRet != 0 {
|
|
|
|
|
+ s.lastX, s.lastY = pt.X, pt.Y
|
|
|
|
|
+ }
|
|
|
|
|
+ // 到期判断
|
|
|
|
|
+ if !s.stopTime.IsZero() && time.Now().After(s.stopTime) {
|
|
|
|
|
+ s.active = false
|
|
|
|
|
+ s.mu.Unlock()
|
|
|
|
|
+ procShowWindow.Call(hwnd, 0) // SW_HIDE
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+ s.flashOn = !s.flashOn
|
|
|
|
|
+ // 超长文字滚动:文字宽度超过可用区时,按配置速度推进偏移,循环滚动
|
|
|
|
|
+ w := s.cfgWidth
|
|
|
|
|
+ if w <= 0 {
|
|
|
|
|
+ w = overlayWidth
|
|
|
|
|
+ }
|
|
|
|
|
+ if s.textWidth > w-24 {
|
|
|
|
|
+ step := int32(s.cfgSpeed * 60 / 1000) // 每 60ms 帧推进(px)
|
|
|
|
|
+ if step < 1 {
|
|
|
|
|
+ step = 1
|
|
|
|
|
+ }
|
|
|
|
|
+ s.scrollX += step
|
|
|
|
|
+ // 头尾相联无缝循环:滚动一个完整周期(文字宽 + 间隔)后归零,
|
|
|
|
|
+ // 下一份正好接上,形成连续跑马灯。
|
|
|
|
|
+ cycle := s.textWidth + s.textGap
|
|
|
|
|
+ if cycle <= 0 {
|
|
|
|
|
+ cycle = s.textWidth + 40
|
|
|
|
|
+ }
|
|
|
|
|
+ if s.scrollX > cycle {
|
|
|
|
|
+ s.scrollX = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ s.scrollX = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ x, y := s.lastX, s.lastY
|
|
|
|
|
+ s.mu.Unlock()
|
|
|
|
|
+ s.positionAndDraw(hwnd, x, y)
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0
|
|
|
|
|
+ case wmPaint:
|
|
|
|
|
+ // WM_PAINT 用普通 GDI 绘制气泡(背景 + 文字)。分层窗口已设 LWA_COLORKEY
|
|
|
|
|
+ // 抠透明,此处只画内容,不调用 UpdateLayeredWindow(会接管窗口)。
|
|
|
|
|
+ // 双缓冲:先在内存 DC(兼容位图)上合成背景+文字,再一次性 BitBlt 上屏,
|
|
|
|
|
+ // 避免多遍描边 + 正文逐笔直接画到屏幕导致滚动时闪烁。
|
|
|
|
|
+ var ps paintstruct
|
|
|
|
|
+ hdc, _, _ := procBeginPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
|
|
|
|
|
+ if hdc != 0 {
|
|
|
|
|
+ s.mu.Lock()
|
|
|
|
|
+ fw, fh := s.cfgWidth, s.cfgHeight
|
|
|
|
|
+ s.mu.Unlock()
|
|
|
|
|
+ if fw <= 0 {
|
|
|
|
|
+ fw = overlayWidth
|
|
|
|
|
+ }
|
|
|
|
|
+ if fh <= 0 {
|
|
|
|
|
+ fh = overlayHeight
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-// renderOverlay 定位覆盖层到鼠标位置并重绘闪烁光标 + 下方透明文字。
|
|
|
|
|
-// 在窗口线程内调用(mouseAlertLoop 建窗后 / wndProcCallback 定时器),
|
|
|
|
|
-// 窗口/DC 操作均为同线程,避免跨线程操作窗口导致绘制失败。
|
|
|
|
|
-func (s *mouseAlertState) renderOverlay(overlay uintptr, text string, flashOn bool, x, y int32) {
|
|
|
|
|
- if overlay == 0 {
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- // 让覆盖层左上角位于鼠标位置偏左上,使光标环中心对准鼠标
|
|
|
|
|
- left := x - 10
|
|
|
|
|
- top := y - 4
|
|
|
|
|
- procSetWindowPos.Call(overlay, hwndTopMost,
|
|
|
|
|
- uintptr(int32(left)), uintptr(int32(top)),
|
|
|
|
|
- overlayWidth, overlayHeight, swpNoActivate|swpShowWindow)
|
|
|
|
|
|
|
+ // 创建内存 DC + 兼容位图作为双缓冲画布
|
|
|
|
|
+ memDC, _, _ := procCreateCompatibleDC.Call(hdc)
|
|
|
|
|
+ memBmp, _, _ := procCreateCompatibleBitmap.Call(hdc, uintptr(fw), uintptr(fh))
|
|
|
|
|
+ oldBmp, _, _ := procSelectObject.Call(memDC, memBmp)
|
|
|
|
|
+ if memDC != 0 && memBmp != 0 {
|
|
|
|
|
+ // 背景:填满整个客户区(LWA_COLORKEY 把该色抠成透明,只留文字)
|
|
|
|
|
+ var full rect
|
|
|
|
|
+ full.Left = 0
|
|
|
|
|
+ full.Top = 0
|
|
|
|
|
+ full.Right = fw
|
|
|
|
|
+ full.Bottom = fh
|
|
|
|
|
+ if s.bgBrush != 0 {
|
|
|
|
|
+ procFillRect.Call(memDC, uintptr(unsafe.Pointer(&full)), s.bgBrush)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 文字:在内存 DC 上绘制(TRANSPARENT 背景模式,不覆盖气泡背景)
|
|
|
|
|
+ s.paintText(memDC)
|
|
|
|
|
+ // 一次性把合成结果拷贝上屏,避免逐笔绘制的闪烁
|
|
|
|
|
+ procBitBlt.Call(hdc, 0, 0, uintptr(fw), uintptr(fh), memDC, 0, 0, 0x00CC0020 /* SRCCOPY */)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- s.drawLayered(overlay, text, flashOn)
|
|
|
|
|
|
|
+ // 清理双缓冲对象
|
|
|
|
|
+ if memDC != 0 {
|
|
|
|
|
+ procSelectObject.Call(memDC, oldBmp)
|
|
|
|
|
+ if memBmp != 0 {
|
|
|
|
|
+ procDeleteObject.Call(memBmp)
|
|
|
|
|
+ }
|
|
|
|
|
+ procDeleteDC.Call(memDC)
|
|
|
|
|
+ }
|
|
|
|
|
+ procEndPaint.Call(hwnd, uintptr(unsafe.Pointer(&ps)))
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0
|
|
|
|
|
+ case wmDestroy, wmNcDestroy:
|
|
|
|
|
+ procKillTimer.Call(hwnd, timerFollow)
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+ ret, _, _ := procDefWindowProc.Call(hwnd, uintptr(msg), wParam, lParam)
|
|
|
|
|
+ return ret
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// drawLayered 用 32 位 DIB + UpdateLayeredWindow 绘制闪烁光标环与半透明文字。
|
|
|
|
|
-func (s *mouseAlertState) drawLayered(overlay uintptr, text string, flashOn bool) {
|
|
|
|
|
- // 用窗口实际总尺寸创建 DIB,并用 GetWindowRect 获取窗口屏幕位置,
|
|
|
|
|
- // 显式传给 UpdateLayeredWindow 的 pptDst/psize,确保 DIB 与窗口完全
|
|
|
|
|
- // 匹配(此前用 GetClientRect 且 psize=nil,当 DIB 尺寸与窗口实际尺寸
|
|
|
|
|
- // 不一致时 UpdateLayeredWindow 绘制失效,导致覆盖层显示为空白)。
|
|
|
|
|
- var wr rect
|
|
|
|
|
- procGetWindowRect.Call(overlay, uintptr(unsafe.Pointer(&wr)))
|
|
|
|
|
- w := int(wr.Right - wr.Left)
|
|
|
|
|
- h := int(wr.Bottom - wr.Top)
|
|
|
|
|
- if w < 8 || h < 8 { // 兜底:窗口尺寸异常时用默认尺寸
|
|
|
|
|
- w, h = overlayWidth, overlayHeight
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- hdcScreen, _, _ := procGetDC.Call(0)
|
|
|
|
|
- if hdcScreen == 0 {
|
|
|
|
|
|
|
+// positionAndDraw 把气泡窗口定位到鼠标旁,设置整体 alpha,并触发 WM_PAINT 重绘。
|
|
|
|
|
+// 窗口线程内调用。绘制本身由 WM_PAINT(普通 GDI)完成。
|
|
|
|
|
+// 不闪烁:固定 alpha=235(此前 60ms 翻转 235↔150 导致闪烁严重,已去掉)。
|
|
|
|
|
+func (s *mouseAlertState) positionAndDraw(overlay uintptr, x, y int32) {
|
|
|
|
|
+ if overlay == 0 {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- defer procReleaseDC.Call(0, hdcScreen)
|
|
|
|
|
-
|
|
|
|
|
- hdcMem, _, _ := procCreateCompatibleDC.Call(hdcScreen)
|
|
|
|
|
- if hdcMem == 0 {
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ s.mu.Lock()
|
|
|
|
|
+ w, h := s.cfgWidth, s.cfgHeight
|
|
|
|
|
+ if w <= 0 {
|
|
|
|
|
+ w = overlayWidth
|
|
|
}
|
|
}
|
|
|
- defer procDeleteDC.Call(hdcMem)
|
|
|
|
|
-
|
|
|
|
|
- // 32 位 DIB 位图(负高度:自顶向下)
|
|
|
|
|
- bi := bitmapinfoheader{}
|
|
|
|
|
- bi.BiSize = 40 // sizeof(BITMAPINFOHEADER)
|
|
|
|
|
- bi.BiWidth = int32(w)
|
|
|
|
|
- bi.BiHeight = int32(-h)
|
|
|
|
|
- bi.BiPlanes = 1
|
|
|
|
|
- bi.BiBitCount = 32
|
|
|
|
|
- bi.BiCompression = biRgb
|
|
|
|
|
-
|
|
|
|
|
- var bits unsafe.Pointer
|
|
|
|
|
- hBitmap, _, _ := procCreateDIBSection.Call(hdcScreen,
|
|
|
|
|
- uintptr(unsafe.Pointer(&bi)), 0, uintptr(unsafe.Pointer(&bits)), 0, 0)
|
|
|
|
|
- if hBitmap == 0 {
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ if h <= 0 {
|
|
|
|
|
+ h = overlayHeight
|
|
|
}
|
|
}
|
|
|
- defer procDeleteObject.Call(hBitmap)
|
|
|
|
|
|
|
+ s.mu.Unlock()
|
|
|
|
|
|
|
|
- oldBmp, _, _ := procSelectObject.Call(hdcMem, hBitmap)
|
|
|
|
|
|
|
+ // 气泡左上角位于鼠标右下偏移,避免遮挡光标
|
|
|
|
|
+ left := x + 20
|
|
|
|
|
+ top := y + 24
|
|
|
|
|
|
|
|
- // 清空为全透明
|
|
|
|
|
- clearPixels(bits, w, h)
|
|
|
|
|
|
|
+ // 背景透明(LWA_COLORKEY)只在创建窗口时设置一次,避免每帧重复设置导致闪烁。
|
|
|
|
|
|
|
|
- // 光标环中心放在覆盖层 (10, 4) 附近,即鼠标位置
|
|
|
|
|
- drawCursorRing(hdcMem, 10, 4, flashOn)
|
|
|
|
|
-
|
|
|
|
|
- // 光标下方半透明文字
|
|
|
|
|
- if text != "" {
|
|
|
|
|
- drawAlertText(hdcMem, text, 10, textTopY)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // GDI 绘制到 DIB 不写 alpha 通道,此处按亮度补全 alpha,
|
|
|
|
|
- // 使图形/文字边缘抗锯齿平滑(文字区半透明)
|
|
|
|
|
- setAlphaFromLuma(bits, w, h)
|
|
|
|
|
-
|
|
|
|
|
- procSelectObject.Call(hdcMem, oldBmp)
|
|
|
|
|
|
|
+ // 定位 + 显示(不激活)
|
|
|
|
|
+ procSetWindowPos.Call(overlay, hwndTopMost,
|
|
|
|
|
+ uintptr(int32(left)), uintptr(int32(top)),
|
|
|
|
|
+ uintptr(w), uintptr(h), swpNoActivate|swpShowWindow)
|
|
|
|
|
|
|
|
- // UpdateLayeredWindow:显式传 pptDst(窗口屏幕左上角)与 psize(窗口尺寸),
|
|
|
|
|
- // 使 DIB 内容精确定位到窗口,避免 psize=nil 时因尺寸不匹配绘制失效。
|
|
|
|
|
- var blend blendfunction
|
|
|
|
|
- blend.BlendOp = 0 // AC_SRC_OVER
|
|
|
|
|
- blend.AlphaFormat = 0x01 // AC_SRC_ALPHA
|
|
|
|
|
- blend.SourceConstantAlpha = 255
|
|
|
|
|
- var dstPt point
|
|
|
|
|
- dstPt.X = wr.Left
|
|
|
|
|
- dstPt.Y = wr.Top
|
|
|
|
|
- var szPt point
|
|
|
|
|
- szPt.X = int32(w)
|
|
|
|
|
- szPt.Y = int32(h)
|
|
|
|
|
- procUpdateLayeredWindow.Call(overlay, hdcScreen,
|
|
|
|
|
- uintptr(unsafe.Pointer(&dstPt)), // pptDst = 窗口左上角屏幕坐标
|
|
|
|
|
- uintptr(unsafe.Pointer(&szPt)), // psize = 窗口尺寸
|
|
|
|
|
- hdcMem, 0, 0, uintptr(unsafe.Pointer(&blend)), 0x00000002 /*ULW_ALPHA*/)
|
|
|
|
|
|
|
+ // 触发重绘(WM_PAINT 绘制背景 + 文字)
|
|
|
|
|
+ procInvalidateRect.Call(overlay, 0, 0)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// clearPixels 把 DIB 位图全部置为透明(ARGB=0)
|
|
|
|
|
-func clearPixels(bits unsafe.Pointer, w, h int) {
|
|
|
|
|
- if bits == nil {
|
|
|
|
|
|
|
+// paintText 在指定 HDC 上绘制白色提示文字(WM_PAINT 内调用,窗口线程)。
|
|
|
|
|
+// 用 DrawTextW + 系统字体 + TRANSPARENT 背景模式:只画字形、不填充背景,
|
|
|
|
|
+// 这样不会覆盖 FillRect 画的气泡背景(此前 OPAQUE 模式导致文字区被填成黑色
|
|
|
|
|
+// 背景而不可见)。
|
|
|
|
|
+func (s *mouseAlertState) paintText(hdc uintptr) {
|
|
|
|
|
+ s.mu.Lock()
|
|
|
|
|
+ text := s.text
|
|
|
|
|
+ scrollX := s.scrollX
|
|
|
|
|
+ fontSize := s.cfgFontSize
|
|
|
|
|
+ textColor := s.cfgTextColor
|
|
|
|
|
+ outlineColor := s.cfgOutlineColor
|
|
|
|
|
+ outlineWidth := s.cfgOutlineWidth
|
|
|
|
|
+ width := s.cfgWidth
|
|
|
|
|
+ s.mu.Unlock()
|
|
|
|
|
+ if text == "" || hdc == 0 {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- total := uintptr(w) * uintptr(h) * 4
|
|
|
|
|
- for i := uintptr(0); i+8 <= total; i += 8 {
|
|
|
|
|
- *(*uint64)(unsafe.Add(bits, i)) = 0
|
|
|
|
|
|
|
+ if fontSize <= 0 {
|
|
|
|
|
+ fontSize = DefaultMouseAlertFontSize // 默认 20px
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// setAlphaFromLuma 按亮度补全 alpha:非透明像素的 alpha = max(r,g,b),
|
|
|
|
|
-// 文字区(y >= textTopY)乘以 3/4 实现半透明。
|
|
|
|
|
-func setAlphaFromLuma(bits unsafe.Pointer, w, h int) {
|
|
|
|
|
- if bits == nil {
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ if textColor == 0 {
|
|
|
|
|
+ textColor = 0x00FFFFFF // 默认白字
|
|
|
}
|
|
}
|
|
|
- for y := 0; y < h; y++ {
|
|
|
|
|
- for x := 0; x < w; x++ {
|
|
|
|
|
- p := (*uint32)(unsafe.Add(bits, uintptr(y*w+x)*4))
|
|
|
|
|
- v := *p
|
|
|
|
|
- b := uint8(v)
|
|
|
|
|
- g := uint8(v >> 8)
|
|
|
|
|
- r := uint8(v >> 16)
|
|
|
|
|
- m := uint32(r)
|
|
|
|
|
- if uint32(g) > m {
|
|
|
|
|
- m = uint32(g)
|
|
|
|
|
- }
|
|
|
|
|
- if uint32(b) > m {
|
|
|
|
|
- m = uint32(b)
|
|
|
|
|
- }
|
|
|
|
|
- if m == 0 {
|
|
|
|
|
- *p = 0 // 保持透明
|
|
|
|
|
- continue
|
|
|
|
|
- }
|
|
|
|
|
- if y >= textTopY {
|
|
|
|
|
- m = m * 3 / 4 // 文字半透明
|
|
|
|
|
- }
|
|
|
|
|
- *p = v&0x00FFFFFF | (m << 24) // 写入 alpha
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if outlineColor == 0 {
|
|
|
|
|
+ outlineColor = 0x00000000 // 默认黑边
|
|
|
|
|
+ }
|
|
|
|
|
+ if width <= 0 {
|
|
|
|
|
+ width = overlayWidth
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
-// drawCursorRing 在覆盖层坐标 (cx,cy) 画闪烁光标环(提醒样式)
|
|
|
|
|
-func drawCursorRing(hdc uintptr, cx, cy int32, flashOn bool) {
|
|
|
|
|
- color := flashColorNormal
|
|
|
|
|
- if flashOn {
|
|
|
|
|
- color = flashColorAlert
|
|
|
|
|
- }
|
|
|
|
|
- const r = 12
|
|
|
|
|
- for angle := 0; angle < 360; angle += 4 {
|
|
|
|
|
- rad := float64(angle) * 3.14159265 / 180.0
|
|
|
|
|
- px := cx + int32(float64(r)*cos(rad))
|
|
|
|
|
- py := cy + int32(float64(r)*sin(rad))
|
|
|
|
|
- procSetPixel.Call(hdc, uintptr(px), uintptr(py), uintptr(color))
|
|
|
|
|
- }
|
|
|
|
|
- // 中心小点
|
|
|
|
|
- procSetPixel.Call(hdc, uintptr(cx), uintptr(cy), uintptr(color))
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ // TRANSPARENT:只画字形,不填背景(保留气泡背景)
|
|
|
|
|
+ procSetBkMode.Call(hdc, 1)
|
|
|
|
|
+ procSetTextColor.Call(hdc, textColor)
|
|
|
|
|
|
|
|
-// drawAlertText 在 (x,y) 绘制白色提示文字(抗锯齿边缘由 setAlphaFromLuma 处理)
|
|
|
|
|
-func drawAlertText(hdc uintptr, text string, x, y int32) {
|
|
|
|
|
- // CreateFontW 参数顺序:nHeight,nWidth,nEscapement,nOrientation,nWeight,
|
|
|
|
|
- // bItalic,bUnderline,bStrikeOut,iCharSet,iOutPrecision,iClipPrecision,
|
|
|
|
|
- // iQuality,iPitchAndFamily,pszFaceName。nHeight=-16 表示 16 像素高。
|
|
|
|
|
- var height int32 = -16
|
|
|
|
|
|
|
+ // 大字体:CreateFontW 按配置字号创建清晰字体
|
|
|
|
|
+ // 参数:h=-fontSize(px), w=0, esc=0, orient=0, weight=FW_NORMAL(400), italic=0,
|
|
|
|
|
+ // underline=0, strikeout=0, charset=DEFAULT_CHARSET(1), outprec=0, clipprec=0,
|
|
|
|
|
+ // quality=ANTIALIASED(4), pitch=0, face=微软雅黑
|
|
|
|
|
+ facePtr, _ := syscall.UTF16PtrFromString("Microsoft YaHei")
|
|
|
|
|
+ var fontH int32 = -fontSize // 负值=像素高度(字号 px),用变量避免常量溢出
|
|
|
hFont, _, _ := procCreateFontW.Call(
|
|
hFont, _, _ := procCreateFontW.Call(
|
|
|
- uintptr(height), 0, 0, 0, 400, // 高16、粗体
|
|
|
|
|
- 0, 0, 0, 134, // 非斜体、GB2312_CHARSET
|
|
|
|
|
- 0, 0, 4, 2, // 抗锯齿、DEFAULT_PITCH
|
|
|
|
|
- 0, // lpszFace=NULL 用系统默认字体
|
|
|
|
|
|
|
+ uintptr(fontH), 0, 0, 0, 400, 0, 0, 0,
|
|
|
|
|
+ 1 /* DEFAULT_CHARSET */, 0, 0, 4 /* ANTIALIASED */, 0,
|
|
|
|
|
+ uintptr(unsafe.Pointer(facePtr)),
|
|
|
)
|
|
)
|
|
|
if hFont == 0 {
|
|
if hFont == 0 {
|
|
|
- return
|
|
|
|
|
|
|
+ // 回退系统字体
|
|
|
|
|
+ hFont, _, _ = procGetStockObject.Call(17) // DEFAULT_GUI_FONT
|
|
|
|
|
+ }
|
|
|
|
|
+ if hFont != 0 {
|
|
|
|
|
+ procSelectObject.Call(hdc, hFont)
|
|
|
}
|
|
}
|
|
|
- defer procDeleteObject.Call(hFont)
|
|
|
|
|
- oldFont, _, _ := procSelectObject.Call(hdc, hFont)
|
|
|
|
|
-
|
|
|
|
|
- procSetBkMode.Call(hdc, 1) // TRANSPARENT
|
|
|
|
|
- procSetTextColor.Call(hdc, 0x00FFFFFF) // 白
|
|
|
|
|
- procSetTextAlign.Call(hdc, 0) // TA_LEFT
|
|
|
|
|
|
|
|
|
|
|
|
+ // 测量文字像素宽度(选中字体下),判断是否需要滚动
|
|
|
textPtr, _ := syscall.UTF16PtrFromString(text)
|
|
textPtr, _ := syscall.UTF16PtrFromString(text)
|
|
|
- procTextOutW.Call(hdc, uintptr(x), uintptr(y), uintptr(unsafe.Pointer(textPtr)), uintptr(len([]rune(text))))
|
|
|
|
|
-
|
|
|
|
|
- procSelectObject.Call(hdc, oldFont)
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// wndProcCallback 覆盖层窗口过程:处理定时器(闪烁)与销毁
|
|
|
|
|
-func (s *mouseAlertState) wndProcCallback(hwnd uintptr, msg uint32, wParam uintptr, lParam uintptr) uintptr {
|
|
|
|
|
- switch msg {
|
|
|
|
|
- case wmTimer:
|
|
|
|
|
- if uintptr(wParam) == timerFlash {
|
|
|
|
|
- // 主动用 GetCursorPos 读取当前鼠标位置(窗口线程,安全)。
|
|
|
|
|
- // 不再依赖全局钩子回调更新 lastX/lastY(钩子回调在系统线程
|
|
|
|
|
- // 执行,任何 Go 状态操作都会导致进程退出,故仅做转发)。
|
|
|
|
|
- var pt point
|
|
|
|
|
- curRet, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
|
|
|
|
|
- s.mu.Lock()
|
|
|
|
|
- if s.active {
|
|
|
|
|
- if curRet != 0 {
|
|
|
|
|
- s.lastX, s.lastY = pt.X, pt.Y
|
|
|
|
|
- }
|
|
|
|
|
- // 到期判断(原先在钩子回调,现移到窗口线程)
|
|
|
|
|
- if !s.stopTime.IsZero() && time.Now().After(s.stopTime) {
|
|
|
|
|
- s.active = false
|
|
|
|
|
- s.mu.Unlock()
|
|
|
|
|
- procShowWindow.Call(hwnd, 0) // SW_HIDE
|
|
|
|
|
- return 0
|
|
|
|
|
- }
|
|
|
|
|
- s.flashOn = !s.flashOn
|
|
|
|
|
- text := s.text
|
|
|
|
|
- flashOn := s.flashOn
|
|
|
|
|
- x, y := s.lastX, s.lastY
|
|
|
|
|
- s.mu.Unlock()
|
|
|
|
|
- s.renderOverlay(hwnd, text, flashOn, x, y)
|
|
|
|
|
- return 0
|
|
|
|
|
- }
|
|
|
|
|
- s.mu.Unlock()
|
|
|
|
|
|
|
+ var ext size
|
|
|
|
|
+ procGetTextExtentPoint32W.Call(hdc, uintptr(unsafe.Pointer(textPtr)),
|
|
|
|
|
+ uintptr(len([]rune(text))), uintptr(unsafe.Pointer(&ext)))
|
|
|
|
|
+ textW := ext.Cx
|
|
|
|
|
+ availW := width - 24 // 左右各 12px 边距
|
|
|
|
|
+
|
|
|
|
|
+ s.mu.Lock()
|
|
|
|
|
+ s.textWidth = textW
|
|
|
|
|
+ s.mu.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // 文字垂直居中:气泡高度 = 字号 + 34,上下留白各约 (34-字号)/2 + 基线偏移
|
|
|
|
|
+ baseY := (fontSize + 10) / 2 // 字形基线 y 坐标(垂直居中)
|
|
|
|
|
+
|
|
|
|
|
+ // 单行绘制(TextOutW)+ 超长时按 scrollX 偏移。文字从左边距开始画,
|
|
|
|
|
+ // 滚动时整体向左平移(x - scrollX),实现文字从右向左循环滚动。
|
|
|
|
|
+ // 文字超长时先画两遍(偏移 和 偏移+周期)实现头尾相联无缝循环。
|
|
|
|
|
+ //
|
|
|
|
|
+ // 描边反差:为避免文字在任意背景(亮/暗)上看不清,先用描边色把文字
|
|
|
|
|
+ // 向 8 个方向各偏移 outlineWidth 各画一遍形成外描边(stroke),最后用
|
|
|
|
|
+ // 正文色在中心画一遍(fill),构成带色边的文字,形成颜色反差。
|
|
|
|
|
+ textLen := uintptr(len([]rune(text)))
|
|
|
|
|
+ leftX := int32(12) - scrollX
|
|
|
|
|
+ if textW > availW {
|
|
|
|
|
+ // 头尾相联:第二份从「文字头 + 文字宽 + 间隔」开始画,间隔为两个空格
|
|
|
|
|
+ s.mu.Lock()
|
|
|
|
|
+ gap := s.textGap
|
|
|
|
|
+ s.mu.Unlock()
|
|
|
|
|
+ if gap < 4 {
|
|
|
|
|
+ gap = 4
|
|
|
}
|
|
}
|
|
|
- return 0
|
|
|
|
|
- case wmDestroy, wmNcDestroy:
|
|
|
|
|
- procKillTimer.Call(hwnd, timerFlash)
|
|
|
|
|
- return 0
|
|
|
|
|
|
|
+ // 第一遍:当前偏移处(文字头)
|
|
|
|
|
+ s.drawOutlinedText(hdc, textPtr, textLen, leftX, baseY, outlineColor, outlineWidth, textColor)
|
|
|
|
|
+ // 第二遍:文字头 + 文字宽 + 间隔,实现头尾相联无缝循环
|
|
|
|
|
+ s.drawOutlinedText(hdc, textPtr, textLen, leftX+textW+gap, baseY, outlineColor, outlineWidth, textColor)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 未超长:左对齐单行
|
|
|
|
|
+ s.drawOutlinedText(hdc, textPtr, textLen, 12, baseY, outlineColor, outlineWidth, textColor)
|
|
|
}
|
|
}
|
|
|
- ret, _, _ := procDefWindowProc.Call(hwnd, uintptr(msg), wParam, lParam)
|
|
|
|
|
- return ret
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// procCallNextHookEx 钩子链传递
|
|
|
|
|
-var procCallNextHookEx = user32.NewProc("CallNextHookEx")
|
|
|
|
|
-
|
|
|
|
|
-// cos / sin 简单实现(避免依赖 math 的浮点精度问题)
|
|
|
|
|
-func cos(rad float64) float64 {
|
|
|
|
|
- r := rad
|
|
|
|
|
- for r > 3.14159265 {
|
|
|
|
|
- r -= 6.28318530
|
|
|
|
|
- }
|
|
|
|
|
- for r < -3.14159265 {
|
|
|
|
|
- r += 6.28318530
|
|
|
|
|
|
|
+// drawOutlinedText 绘制带描边的文字:先用 outlineColor 在 8 方向各偏移
|
|
|
|
|
+// outlineWidth 描一圈形成外轮廓,再在中心用 textColor 画正文字形。
|
|
|
|
|
+// outlineWidth<=0 时不描边,直接画正文(等同旧行为)。
|
|
|
|
|
+func (s *mouseAlertState) drawOutlinedText(hdc uintptr, textPtr *uint16, textLen uintptr, x, y int32, outlineColor uintptr, outlineWidth int32, textColor uintptr) {
|
|
|
|
|
+ if textLen == 0 {
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
- r2 := r * r
|
|
|
|
|
- return 1 - r2/2 + r2*r2/24 - r2*r2*r2/720
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-func sin(rad float64) float64 {
|
|
|
|
|
- r := rad
|
|
|
|
|
- for r > 3.14159265 {
|
|
|
|
|
- r -= 6.28318530
|
|
|
|
|
|
|
+ draw := func(dx, dy int32, color uintptr) {
|
|
|
|
|
+ procSetTextColor.Call(hdc, color)
|
|
|
|
|
+ procTextOutW.Call(hdc, uintptr(x+dx), uintptr(y+dy), uintptr(unsafe.Pointer(textPtr)), textLen)
|
|
|
}
|
|
}
|
|
|
- for r < -3.14159265 {
|
|
|
|
|
- r += 6.28318530
|
|
|
|
|
|
|
+ if outlineWidth > 0 {
|
|
|
|
|
+ // 8 方向外描边(每像素偏移),形成环绕轮廓,避免不同背景下文字难辨认
|
|
|
|
|
+ for dy := -outlineWidth; dy <= outlineWidth; dy++ {
|
|
|
|
|
+ for dx := -outlineWidth; dx <= outlineWidth; dx++ {
|
|
|
|
|
+ if dx == 0 && dy == 0 {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ draw(dx, dy, outlineColor)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- r2 := r * r
|
|
|
|
|
- return r - r2*r/6 + r2*r2*r/120 - r2*r2*r2*r/5040
|
|
|
|
|
|
|
+ // 中心正文字形
|
|
|
|
|
+ draw(0, 0, textColor)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// renderBubbleToDib 已移除:回退到 WM_PAINT 普通 GDI 绘制方案(见 wmPaint/paintText)。
|
|
|
|
|
+// 分层窗口用 LWA_ALPHA 整体半透明,不再用 DIB + UpdateLayeredWindow。
|