|
@@ -0,0 +1,1510 @@
|
|
|
|
|
+//go:build windows
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// ledwindow_windows.go — LED 点阵滚动文字(Windows 原生实现)
|
|
|
|
|
+//
|
|
|
|
|
+// 需求:输入文字(含中文),转换为 LED 点阵并在独立的无边框、透明、置顶、
|
|
|
|
|
+// 可拖动窗口中横向循环滚动显示。
|
|
|
|
|
+//
|
|
|
|
|
+// 为何不用 Wails WebView2:WebView2 的默认背景不透明问题无法通过
|
|
|
|
|
+// PutDefaultBackgroundColor(0,0,0,0) 根治——组合宿主(DComp)下视觉无
|
|
|
|
|
+// SetAlphaMode,HWND 宿主下 WebView2 默认背景在未聚焦/聚焦时仍是白/蓝色。
|
|
|
|
|
+// 因此本窗口完全用 Win32 API 原生实现,不经过 Wails:
|
|
|
|
|
+//
|
|
|
|
|
+// 1. 分层窗口 WS_EX_LAYERED + 32 位 DIB section + UpdateLayeredWindow(ULW_ALPHA),
|
|
|
|
|
+// 逐像素 alpha 实现真正透明:背景 alpha=0(全透明),点阵 alpha=255(不透明)。
|
|
|
|
|
+// (SetLayeredWindowAttributes(LWA_COLORKEY) 在本环境调用虽成功但实际失效,
|
|
|
|
|
+// 故改用 UpdateLayeredWindow 像素级透明。)
|
|
|
|
|
+// 2. 点阵直接写入 DIB 内存(BGRA),每帧用 UpdateLayeredWindow 提交到屏幕。
|
|
|
|
|
+// 3. 拖动:WM_LBUTTONDOWN → SendMessage(WM_NCLBUTTONDOWN, HTCAPTION),整窗可拖动。
|
|
|
|
|
+// 4. 置顶:WS_EX_TOPMOST;不占任务栏:WS_EX_TOOLWINDOW;不抢焦点:ShowWindow(SW_SHOWNOACTIVATE)。
|
|
|
|
|
+// 5. 文字点阵:数字/字母/符号用内置 5x7 字模放大 2 倍(10x14),
|
|
|
|
|
+// 汉字用 GDI 字体渲染到内存位图后按网格采样为 24x32 点阵(按 RGB 亮度采样,
|
|
|
|
|
+// 因 GDI TextOutW 不写 32 位 DIB 的 alpha 通道)。
|
|
|
|
|
+// 6. 横向循环滚动:窗口线程定时器按速度推进 offset,循环回绕。
|
|
|
|
|
+//
|
|
|
|
|
+// 说明:本文件仅在 Windows 编译。其他平台见 ledwindow_other.go 空实现。
|
|
|
|
|
+// 共享的 Win32 常量/proc/结构体(wmTimer、wndExLayered、procGetMessage、
|
|
|
|
|
+// wndclassex 等)复用 mousealert_windows.go 的定义,避免重复声明冲突。
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+package main
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "runtime"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "sync"
|
|
|
|
|
+ "syscall"
|
|
|
|
|
+ "time"
|
|
|
|
|
+ "unsafe"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+// Windows API 常量(LED 专属;共享常量见 mousealert_windows.go)
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+const (
|
|
|
|
|
+ wmNcHitTest = 0x0084
|
|
|
|
|
+ wmClose = 0x0010
|
|
|
|
|
+ wmNcLButtonDown = 0x00A1
|
|
|
|
|
+ wmLButtonDown = 0x0201
|
|
|
|
|
+ wmLButtonUp = 0x0202
|
|
|
|
|
+ wmMouseMove = 0x0200
|
|
|
|
|
+ wmMouseLeave = 0x02A3
|
|
|
|
|
+ wmMouseWheel = 0x020A
|
|
|
|
|
+
|
|
|
|
|
+ timerScroll = 1 // 滚动定时器 ID
|
|
|
|
|
+
|
|
|
|
|
+ // 窗口尺寸(逻辑像素)
|
|
|
|
|
+ ledWinWidth = 720
|
|
|
|
|
+ ledWinHeight = 260
|
|
|
|
|
+
|
|
|
|
|
+ // 点距/边距:窗口随缩放动态变化(见 ledResizeWindow)。
|
|
|
|
|
+ // ledBaseDot 基础点距(汉字 32 行 × 6px ≈ 192px,加边距约 260 高)。
|
|
|
|
|
+ ledBaseDot = 6
|
|
|
|
|
+ ledPadV = 40 // 垂直边距(容纳边框+按钮,上下各 40)
|
|
|
|
|
+ ledPadH = 20 // 水平边距(容纳边框,左右各 20)
|
|
|
|
|
+
|
|
|
|
|
+ // 点阵参数
|
|
|
|
|
+ ledCharGap = 2 // 字符内点距(点)
|
|
|
|
|
+ ledGap = 3 // 字符间距(点)
|
|
|
|
|
+
|
|
|
|
|
+ // 颜色键背景(纯黑):COLORREF = 0x00BBGGRR
|
|
|
|
|
+ ledBgColor = 0x00000000
|
|
|
|
|
+
|
|
|
|
|
+ // UpdateLayeredWindow 参数
|
|
|
|
|
+ ulwAlpha = 0x2 // ULW_ALPHA:使用 per-pixel alpha
|
|
|
|
|
+ acSrcAlpha = 0x1 // AC_SRC_ALPHA:像素包含 alpha 通道(BGRA)
|
|
|
|
|
+
|
|
|
|
|
+ // 悬停交互 UI 尺寸
|
|
|
|
|
+ ledBorderW = 2 // 悬停边框宽度(px)
|
|
|
|
|
+ ledBtnSize = 26 // 按钮直径(px)
|
|
|
|
|
+ ledBtnMargin = 6 // 按钮距边框内边距(px)
|
|
|
|
|
+ ledBtnCloseTop = ledBorderW + 4 // 关闭按钮上缘
|
|
|
|
|
+ ledBtnZoomTop = ledBorderW + 4 // 缩放按钮上缘
|
|
|
|
|
+
|
|
|
|
|
+ // 滚轮缩放档位
|
|
|
|
|
+ ledZoomMin = 0.6
|
|
|
|
|
+ ledZoomMax = 2.0
|
|
|
|
|
+ ledZoomStep = 0.1
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// htCaption HTCAPTION:WM_NCHITTEST 返回后整窗可拖动
|
|
|
|
|
+const htCaption = 2
|
|
|
|
|
+
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+// Windows 结构体(LED 专属;point/rect/msg/wndclassex/paintstruct 复用共享)
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+// ledBitmapInfoHeader BITMAPINFOHEADER(用于 CreateDIBSection 采样汉字点阵)
|
|
|
|
|
+type ledBitmapInfoHeader struct {
|
|
|
|
|
+ BiSize uint32
|
|
|
|
|
+ BiWidth int32
|
|
|
|
|
+ BiHeight int32 // 负值=自顶向下
|
|
|
|
|
+ BiPlanes uint16
|
|
|
|
|
+ BiBitCount uint16
|
|
|
|
|
+ BiCompression uint32
|
|
|
|
|
+ BiSizeImage uint32
|
|
|
|
|
+ BiXPelsPerMeter int32
|
|
|
|
|
+ BiYPelsPerMeter int32
|
|
|
|
|
+ BiClrUsed uint32
|
|
|
|
|
+ BiClrImportant uint32
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type ledBitmapInfo struct {
|
|
|
|
|
+ BmiHeader ledBitmapInfoHeader
|
|
|
|
|
+ BmiColors [3]uint32
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// blendfunction BLENDFUNCTION(UpdateLayeredWindow 用)
|
|
|
|
|
+type blendfunction struct {
|
|
|
|
|
+ BlendOp byte
|
|
|
|
|
+ BlendFlags byte
|
|
|
|
|
+ SourceConstantAlpha byte
|
|
|
|
|
+ AlphaFormat byte
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// trackMouseEvent TRACKMOUSEEVENT(TrackMouseEvent 用,检测鼠标离开)
|
|
|
|
|
+type trackMouseEvent struct {
|
|
|
|
|
+ CbSize uint32
|
|
|
|
|
+ DwFlags uint32
|
|
|
|
|
+ HwndTrack uintptr
|
|
|
|
|
+ DwHoverTime uint32
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const (
|
|
|
|
|
+ tmeLeave = 0x00000002 // TME_LEAVE
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+// LED 专属 Win32 函数(其余共享 proc 在 mousealert_windows.go 定义,复用)
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+var (
|
|
|
|
|
+ ledGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
|
|
|
|
+ ledPostQuitMessage = user32.NewProc("PostQuitMessage")
|
|
|
|
|
+ procUpdateLayeredWindow = user32.NewProc("UpdateLayeredWindow")
|
|
|
|
|
+ ledGetDC = user32.NewProc("GetDC")
|
|
|
|
|
+ ledReleaseDC = user32.NewProc("ReleaseDC")
|
|
|
|
|
+
|
|
|
|
|
+ ledSetBkColor = gdi32.NewProc("SetBkColor")
|
|
|
|
|
+ ledCreateDIBSection = gdi32.NewProc("CreateDIBSection")
|
|
|
|
|
+ procGetPixel = gdi32.NewProc("GetPixel")
|
|
|
|
|
+ procPatBlt = gdi32.NewProc("PatBlt")
|
|
|
|
|
+
|
|
|
|
|
+ ledSendMessageW = user32.NewProc("SendMessageW")
|
|
|
|
|
+
|
|
|
|
|
+ procTrackMouseEvent = user32.NewProc("TrackMouseEvent")
|
|
|
|
|
+
|
|
|
|
|
+ procGetWindowRect = user32.NewProc("GetWindowRect")
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+// 5x7 点阵字模(每字符 7 行,每行 5 位,高位在左)
|
|
|
|
|
+// 与前端 led.js 的 FONT_5x7 完全一致。
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+var ledFont5x7 = map[rune][7]string{
|
|
|
|
|
+ '0': {"01110", "10001", "10011", "10101", "11001", "10001", "01110"},
|
|
|
|
|
+ '1': {"00100", "01100", "00100", "00100", "00100", "00100", "01110"},
|
|
|
|
|
+ '2': {"01110", "10001", "00001", "00010", "00100", "01000", "11111"},
|
|
|
|
|
+ '3': {"11110", "00001", "00001", "01110", "00001", "00001", "11110"},
|
|
|
|
|
+ '4': {"00010", "00110", "01010", "10010", "11111", "00010", "00010"},
|
|
|
|
|
+ '5': {"11111", "10000", "11110", "00001", "00001", "10001", "01110"},
|
|
|
|
|
+ '6': {"00110", "01000", "10000", "11110", "10001", "10001", "01110"},
|
|
|
|
|
+ '7': {"11111", "00001", "00010", "00100", "01000", "01000", "01000"},
|
|
|
|
|
+ '8': {"01110", "10001", "10001", "01110", "10001", "10001", "01110"},
|
|
|
|
|
+ '9': {"01110", "10001", "10001", "01111", "00001", "00010", "01100"},
|
|
|
|
|
+ 'A': {"01110", "10001", "10001", "11111", "10001", "10001", "10001"},
|
|
|
|
|
+ 'B': {"11110", "10001", "10001", "11110", "10001", "10001", "11110"},
|
|
|
|
|
+ 'C': {"01110", "10001", "10000", "10000", "10000", "10001", "01110"},
|
|
|
|
|
+ 'D': {"11100", "10010", "10001", "10001", "10001", "10010", "11100"},
|
|
|
|
|
+ 'E': {"11111", "10000", "10000", "11110", "10000", "10000", "11111"},
|
|
|
|
|
+ 'F': {"11111", "10000", "10000", "11110", "10000", "10000", "10000"},
|
|
|
|
|
+ 'G': {"01110", "10001", "10000", "10111", "10001", "10001", "01111"},
|
|
|
|
|
+ 'H': {"10001", "10001", "10001", "11111", "10001", "10001", "10001"},
|
|
|
|
|
+ 'I': {"01110", "00100", "00100", "00100", "00100", "00100", "01110"},
|
|
|
|
|
+ 'J': {"00111", "00010", "00010", "00010", "00010", "10010", "01100"},
|
|
|
|
|
+ 'K': {"10001", "10010", "10100", "11000", "10100", "10010", "10001"},
|
|
|
|
|
+ 'L': {"10000", "10000", "10000", "10000", "10000", "10000", "11111"},
|
|
|
|
|
+ 'M': {"10001", "11011", "10101", "10101", "10001", "10001", "10001"},
|
|
|
|
|
+ 'N': {"10001", "11001", "10101", "10011", "10001", "10001", "10001"},
|
|
|
|
|
+ 'O': {"01110", "10001", "10001", "10001", "10001", "10001", "01110"},
|
|
|
|
|
+ 'P': {"11110", "10001", "10001", "11110", "10000", "10000", "10000"},
|
|
|
|
|
+ 'Q': {"01110", "10001", "10001", "10001", "10101", "10010", "01101"},
|
|
|
|
|
+ 'R': {"11110", "10001", "10001", "11110", "10100", "10010", "10001"},
|
|
|
|
|
+ 'S': {"01111", "10000", "10000", "01110", "00001", "00001", "11110"},
|
|
|
|
|
+ 'T': {"11111", "00100", "00100", "00100", "00100", "00100", "00100"},
|
|
|
|
|
+ 'U': {"10001", "10001", "10001", "10001", "10001", "10001", "01110"},
|
|
|
|
|
+ 'V': {"10001", "10001", "10001", "10001", "10001", "01010", "00100"},
|
|
|
|
|
+ 'W': {"10001", "10001", "10001", "10101", "10101", "10101", "01010"},
|
|
|
|
|
+ 'X': {"10001", "10001", "01010", "00100", "01010", "10001", "10001"},
|
|
|
|
|
+ 'Y': {"10001", "10001", "01010", "00100", "00100", "00100", "00100"},
|
|
|
|
|
+ 'Z': {"11111", "00001", "00010", "00100", "01000", "10000", "11111"},
|
|
|
|
|
+ 'a': {"00000", "00000", "01110", "00001", "01111", "10001", "01111"},
|
|
|
|
|
+ 'b': {"10000", "10000", "11110", "10001", "10001", "10001", "11110"},
|
|
|
|
|
+ 'c': {"00000", "00000", "01110", "10001", "10000", "10001", "01110"},
|
|
|
|
|
+ 'd': {"00001", "00001", "01111", "10001", "10001", "10001", "01111"},
|
|
|
|
|
+ 'e': {"00000", "00000", "01110", "10001", "11111", "10000", "01110"},
|
|
|
|
|
+ 'f': {"00110", "01001", "01000", "11100", "01000", "01000", "01000"},
|
|
|
|
|
+ 'g': {"00000", "01111", "10001", "10001", "01111", "00001", "01110"},
|
|
|
|
|
+ 'h': {"10000", "10000", "11110", "10001", "10001", "10001", "10001"},
|
|
|
|
|
+ 'i': {"00100", "00000", "01100", "00100", "00100", "00100", "01110"},
|
|
|
|
|
+ 'j': {"00010", "00000", "00110", "00010", "00010", "10010", "01100"},
|
|
|
|
|
+ 'k': {"10000", "10000", "10010", "10100", "11000", "10100", "10010"},
|
|
|
|
|
+ 'l': {"01100", "00100", "00100", "00100", "00100", "00100", "01110"},
|
|
|
|
|
+ 'm': {"00000", "00000", "11010", "10101", "10101", "10101", "10101"},
|
|
|
|
|
+ 'n': {"00000", "00000", "11110", "10001", "10001", "10001", "10001"},
|
|
|
|
|
+ 'o': {"00000", "00000", "01110", "10001", "10001", "10001", "01110"},
|
|
|
|
|
+ 'p': {"00000", "00000", "11110", "10001", "11110", "10000", "10000"},
|
|
|
|
|
+ 'q': {"00000", "00000", "01111", "10001", "01111", "00001", "00001"},
|
|
|
|
|
+ 'r': {"00000", "00000", "10110", "11001", "10000", "10000", "10000"},
|
|
|
|
|
+ 's': {"00000", "00000", "01111", "10000", "01110", "00001", "11110"},
|
|
|
|
|
+ 't': {"01000", "01000", "11100", "01000", "01000", "01001", "00110"},
|
|
|
|
|
+ 'u': {"00000", "00000", "10001", "10001", "10001", "10011", "01101"},
|
|
|
|
|
+ 'v': {"00000", "00000", "10001", "10001", "10001", "01010", "00100"},
|
|
|
|
|
+ 'w': {"00000", "00000", "10001", "10001", "10101", "10101", "01010"},
|
|
|
|
|
+ 'x': {"00000", "00000", "10001", "01010", "00100", "01010", "10001"},
|
|
|
|
|
+ 'y': {"00000", "00000", "10001", "10001", "01111", "00001", "01110"},
|
|
|
|
|
+ 'z': {"00000", "00000", "11111", "00010", "00100", "01000", "11111"},
|
|
|
|
|
+ ' ': {"00000", "00000", "00000", "00000", "00000", "00000", "00000"},
|
|
|
|
|
+ '.': {"00000", "00000", "00000", "00000", "00000", "00110", "00110"},
|
|
|
|
|
+ ',': {"00000", "00000", "00000", "00000", "00000", "00110", "00100"},
|
|
|
|
|
+ ':': {"00000", "00110", "00110", "00000", "00110", "00110", "00000"},
|
|
|
|
|
+ ';': {"00000", "00110", "00110", "00000", "00110", "00100", "01000"},
|
|
|
|
|
+ '-': {"00000", "00000", "00000", "11111", "00000", "00000", "00000"},
|
|
|
|
|
+ '_': {"00000", "00000", "00000", "00000", "00000", "00000", "11111"},
|
|
|
|
|
+ '+': {"00000", "00100", "00100", "11111", "00100", "00100", "00000"},
|
|
|
|
|
+ '=': {"00000", "00000", "11111", "00000", "11111", "00000", "00000"},
|
|
|
|
|
+ '/': {"00001", "00010", "00010", "00100", "01000", "01000", "10000"},
|
|
|
|
|
+ '\\': {"10000", "01000", "01000", "00100", "00010", "00010", "00001"},
|
|
|
|
|
+ '!': {"00100", "00100", "00100", "00100", "00100", "00000", "00100"},
|
|
|
|
|
+ '?': {"01110", "10001", "00001", "00010", "00100", "00000", "00100"},
|
|
|
|
|
+ '@': {"01110", "10001", "10111", "10101", "10111", "10000", "01110"},
|
|
|
|
|
+ '#': {"01010", "01010", "11111", "01010", "11111", "01010", "01010"},
|
|
|
|
|
+ '$': {"00100", "01111", "10100", "01110", "00101", "11110", "00100"},
|
|
|
|
|
+ '%': {"11001", "11010", "00010", "00100", "01000", "01011", "10011"},
|
|
|
|
|
+ '&': {"01100", "10010", "10100", "01000", "10101", "10010", "01101"},
|
|
|
|
|
+ '*': {"00000", "00100", "10101", "01110", "10101", "00100", "00000"},
|
|
|
|
|
+ '(': {"00010", "00100", "01000", "01000", "01000", "00100", "00010"},
|
|
|
|
|
+ ')': {"01000", "00100", "00010", "00010", "00010", "00100", "01000"},
|
|
|
|
|
+ '[': {"01110", "01000", "01000", "01000", "01000", "01000", "01110"},
|
|
|
|
|
+ ']': {"01110", "00010", "00010", "00010", "00010", "00010", "01110"},
|
|
|
|
|
+ '{': {"00010", "00100", "00100", "01000", "00100", "00100", "00010"},
|
|
|
|
|
+ '}': {"01000", "00100", "00100", "00010", "00100", "00100", "01000"},
|
|
|
|
|
+ '<': {"00010", "00100", "01000", "10000", "01000", "00100", "00010"},
|
|
|
|
|
+ '>': {"01000", "00100", "00010", "00001", "00010", "00100", "01000"},
|
|
|
|
|
+ '|': {"00100", "00100", "00100", "00100", "00100", "00100", "00100"},
|
|
|
|
|
+ '~': {"00000", "01000", "10101", "00010", "00000", "00000", "00000"},
|
|
|
|
|
+ '^': {"00100", "01010", "10001", "00000", "00000", "00000", "00000"},
|
|
|
|
|
+ '。': {"00000", "00000", "00000", "00000", "00110", "00110", "00000"},
|
|
|
|
|
+ ',': {"00000", "00000", "00000", "00000", "00110", "00100", "01000"},
|
|
|
|
|
+ '、': {"00100", "00100", "00100", "00100", "00100", "00000", "00000"},
|
|
|
|
|
+ ':': {"00000", "00110", "00110", "00000", "00110", "00110", "00000"},
|
|
|
|
|
+ ';': {"00000", "00110", "00110", "00000", "00110", "00100", "01000"},
|
|
|
|
|
+ '?': {"01110", "10001", "00001", "00010", "00100", "00000", "00100"},
|
|
|
|
|
+ '!': {"00100", "00100", "00100", "00100", "00100", "00000", "00100"},
|
|
|
|
|
+ '(': {"00010", "00100", "01000", "01000", "01000", "00100", "00010"},
|
|
|
|
|
+ ')': {"01000", "00100", "00010", "00010", "00010", "00100", "01000"},
|
|
|
|
|
+ '《': {"00010", "00100", "01000", "10000", "01000", "00100", "00010"},
|
|
|
|
|
+ '》': {"01000", "00100", "00010", "00001", "00010", "00100", "01000"},
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+// 点阵数据模型
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+// ledGlyph 单个字符的点阵(on[row][col]=true 表示点亮)
|
|
|
|
|
+type ledGlyph struct {
|
|
|
|
|
+ w, h int
|
|
|
|
|
+ on [][]bool
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledChar 字符 + 其点阵
|
|
|
|
|
+type ledChar struct {
|
|
|
|
|
+ ch rune
|
|
|
|
|
+ glyph ledGlyph
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// scaleGlyph5x7 把 5x7 字模放大 scale 倍(每点变 scale×scale 块)
|
|
|
|
|
+func scaleGlyph5x7(rows [7]string, scale int) ledGlyph {
|
|
|
|
|
+ w := 5 * scale
|
|
|
|
|
+ h := 7 * scale
|
|
|
|
|
+ on := make([][]bool, h)
|
|
|
|
|
+ for y := 0; y < h; y++ {
|
|
|
|
|
+ on[y] = make([]bool, w)
|
|
|
|
|
+ for x := 0; x < w; x++ {
|
|
|
|
|
+ on[y][x] = rows[y/scale][x/scale] == '1'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return ledGlyph{w: w, h: h, on: on}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// charToGlyph 把单个字符渲染为点阵:优先内置 5x7 字模(放大 2 倍=10x14),
|
|
|
|
|
+// 汉字/其他用 GDI 渲染到内存位图后按 24x32 网格采样。
|
|
|
|
|
+// hdc 用于汉字采样(窗口线程持有)。
|
|
|
|
|
+func charToGlyph(hdc uintptr, ch rune) ledGlyph {
|
|
|
|
|
+ upper := ch
|
|
|
|
|
+ if ch >= 'a' && ch <= 'z' {
|
|
|
|
|
+ upper = ch - ('a' - 'A')
|
|
|
|
|
+ }
|
|
|
|
|
+ if rows, ok := ledFont5x7[ch]; ok {
|
|
|
|
|
+ return scaleGlyph5x7(rows, 2)
|
|
|
|
|
+ }
|
|
|
|
|
+ if rows, ok := ledFont5x7[upper]; ok {
|
|
|
|
|
+ return scaleGlyph5x7(rows, 2)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 汉字/其他:GDI 渲染 24x32,按网格采样
|
|
|
|
|
+ return renderHanziGlyph(hdc, ch)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// renderHanziGlyph 用 GDI 把汉字画到 24x32 内存位图,按 alpha 通道采样为点阵。
|
|
|
|
|
+// 用 CreateDIBSection 得到像素缓冲,TextOutW 绘制后读 alpha 位。
|
|
|
|
|
+func renderHanziGlyph(hdc uintptr, ch rune) ledGlyph {
|
|
|
|
|
+ const gw, gh = 24, 32
|
|
|
|
|
+
|
|
|
|
|
+ // 32 位自顶向下 DIB
|
|
|
|
|
+ bmi := ledBitmapInfo{}
|
|
|
|
|
+ bmi.BmiHeader.BiSize = uint32(unsafe.Sizeof(ledBitmapInfoHeader{}))
|
|
|
|
|
+ bmi.BmiHeader.BiWidth = gw
|
|
|
|
|
+ bmi.BmiHeader.BiHeight = -gh
|
|
|
|
|
+ bmi.BmiHeader.BiPlanes = 1
|
|
|
|
|
+ bmi.BmiHeader.BiBitCount = 32
|
|
|
|
|
+ bmi.BmiHeader.BiCompression = 0 // BI_RGB
|
|
|
|
|
+
|
|
|
|
|
+ var bits unsafe.Pointer
|
|
|
|
|
+ hbmp, _, _ := ledCreateDIBSection.Call(
|
|
|
|
|
+ 0,
|
|
|
|
|
+ uintptr(unsafe.Pointer(&bmi)),
|
|
|
|
|
+ 0, // DIB_RGB_COLORS
|
|
|
|
|
+ uintptr(unsafe.Pointer(&bits)),
|
|
|
|
|
+ 0, 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ if hbmp == 0 || bits == nil {
|
|
|
|
|
+ return emptyGlyph(gw, gh)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer procDeleteObject.Call(hbmp)
|
|
|
|
|
+
|
|
|
|
|
+ // 内存 DC + 选入位图
|
|
|
|
|
+ memDC, _, _ := procCreateCompatibleDC.Call(hdc)
|
|
|
|
|
+ if memDC == 0 {
|
|
|
|
|
+ return emptyGlyph(gw, gh)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer procDeleteDC.Call(memDC)
|
|
|
|
|
+ oldBmp, _, _ := procSelectObject.Call(memDC, hbmp)
|
|
|
|
|
+
|
|
|
|
|
+ // 白色前景,黑色背景(黑=透明,白=点亮)
|
|
|
|
|
+ procSetBkMode.Call(memDC, 1) // TRANSPARENT
|
|
|
|
|
+ ledSetBkColor.Call(memDC, 0x00000000)
|
|
|
|
|
+ procSetTextColor.Call(memDC, 0x00FFFFFF)
|
|
|
|
|
+ // 清黑:PatBlt BLACKNESS(0x00000042),确保底为纯黑(GDI 读亮度判定)
|
|
|
|
|
+ procPatBlt.Call(memDC, 0, 0, uintptr(gw), uintptr(gh), 0x00000042)
|
|
|
|
|
+
|
|
|
|
|
+ // 创建字体:高约 gh*0.9 像素,微软雅黑
|
|
|
|
|
+ facePtr, _ := syscall.UTF16PtrFromString("Microsoft YaHei")
|
|
|
|
|
+ fontH := int32(gh) * 9 / 10 // gh*0.9,用整数避免常量折叠
|
|
|
|
|
+ hFont, _, _ := procCreateFontW.Call(
|
|
|
|
|
+ uintptr(fontH), 0, 0, 0, 400, 0, 0, 0,
|
|
|
|
|
+ 1 /* DEFAULT_CHARSET */, 0, 0, 4 /* ANTIALIASED */, 0,
|
|
|
|
|
+ uintptr(unsafe.Pointer(facePtr)),
|
|
|
|
|
+ )
|
|
|
|
|
+ if hFont != 0 {
|
|
|
|
|
+ procSelectObject.Call(memDC, hFont)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ hFont, _, _ = procGetStockObject.Call(17) // DEFAULT_GUI_FONT
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制字符(居中):TextOutW 用当前字体,位置在网格中心
|
|
|
|
|
+ chStr := string(ch)
|
|
|
|
|
+ chPtr, _ := syscall.UTF16PtrFromString(chStr)
|
|
|
|
|
+ procTextOutW.Call(memDC, 2, 3, uintptr(unsafe.Pointer(chPtr)), uintptr(len(chStr)))
|
|
|
|
|
+
|
|
|
|
|
+ // 采样 RGB 亮度(GDI TextOutW 不写 32 位 DIB 的 alpha 通道,故按亮度判定点亮)。
|
|
|
|
|
+ // 白字黑底:字=0xFFFFFF(亮),底=0x000000(暗)。
|
|
|
|
|
+ px := uintptr(bits)
|
|
|
|
|
+ on := make([][]bool, gh)
|
|
|
|
|
+ for y := 0; y < gh; y++ {
|
|
|
|
|
+ on[y] = make([]bool, gw)
|
|
|
|
|
+ row := px + uintptr(y*gw*4)
|
|
|
|
|
+ for x := 0; x < gw; x++ {
|
|
|
|
|
+ b := *(*byte)(unsafe.Pointer(row + uintptr(x*4) + 0))
|
|
|
|
|
+ g := *(*byte)(unsafe.Pointer(row + uintptr(x*4) + 1))
|
|
|
|
|
+ r := *(*byte)(unsafe.Pointer(row + uintptr(x*4) + 2))
|
|
|
|
|
+ // 亮度 > 100(0-255 单通道)视为笔画点亮
|
|
|
|
|
+ on[y][x] = (int(r)+int(g)+int(b))/3 > 100
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 恢复并清理
|
|
|
|
|
+ if memDC != 0 {
|
|
|
|
|
+ procSelectObject.Call(memDC, oldBmp)
|
|
|
|
|
+ }
|
|
|
|
|
+ if hFont != 0 && hFont != uintptr(17) {
|
|
|
|
|
+ procDeleteObject.Call(hFont)
|
|
|
|
|
+ }
|
|
|
|
|
+ return ledGlyph{w: gw, h: gh, on: on}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// emptyGlyph 返回全灭的 w×h 点阵(采样失败时的占位)
|
|
|
|
|
+func emptyGlyph(w, h int) ledGlyph {
|
|
|
|
|
+ on := make([][]bool, h)
|
|
|
|
|
+ for y := range on {
|
|
|
|
|
+ on[y] = make([]bool, w)
|
|
|
|
|
+ }
|
|
|
|
|
+ return ledGlyph{w: w, h: h, on: on}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// textToLedSequence 把整串文字拆成字符点阵序列
|
|
|
|
|
+func textToLedSequence(hdc uintptr, text string) []ledChar {
|
|
|
|
|
+ seq := make([]ledChar, 0, len([]rune(text)))
|
|
|
|
|
+ for _, ch := range text {
|
|
|
|
|
+ seq = append(seq, ledChar{ch: ch, glyph: charToGlyph(hdc, ch)})
|
|
|
|
|
+ }
|
|
|
|
|
+ return seq
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledSequenceMaxH 返回序列中最大的点阵高(行数)
|
|
|
|
|
+func ledSequenceMaxH(seq []ledChar) int {
|
|
|
|
|
+ maxH := 0
|
|
|
|
|
+ for _, c := range seq {
|
|
|
|
|
+ if c.glyph.h > maxH {
|
|
|
|
|
+ maxH = c.glyph.h
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return maxH
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledTotalPxWidth 计算整行文字的像素宽(含字符间与末尾间隙)
|
|
|
|
|
+func ledTotalPxWidth(seq []ledChar, dot int) int {
|
|
|
|
|
+ cell := dot + ledCharGap
|
|
|
|
|
+ w := 0
|
|
|
|
|
+ for _, c := range seq {
|
|
|
|
|
+ w += c.glyph.w*cell + ledGap*dot
|
|
|
|
|
+ }
|
|
|
|
|
+ return w
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledParseColor 把 #RRGGBB / #RGB 转 COLORREF(0x00BBGGRR),失败回退红。
|
|
|
|
|
+func ledParseColor(s string) 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 0x00004D4D // 默认红 #ff4d4d
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+// LED 窗口运行时状态(单实例)
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+// ledWindowState LED 窗口的运行时状态(单实例,互斥保护)
|
|
|
|
|
+type ledWindowState struct {
|
|
|
|
|
+ mu sync.Mutex
|
|
|
|
|
+
|
|
|
|
|
+ started bool // 窗口线程是否已启动(幂等,仅启动一次)
|
|
|
|
|
+ hwnd uintptr // 窗口句柄
|
|
|
|
|
+ active bool // 窗口是否已创建并显示
|
|
|
|
|
+ closed bool // 是否已请求关闭(窗口线程退出)
|
|
|
|
|
+
|
|
|
|
|
+ text string // 要显示的文字
|
|
|
|
|
+ color uintptr // LED 点阵颜色(COLORREF)
|
|
|
|
|
+ speed int // 滚动速度(px/s)
|
|
|
|
|
+ seq []ledChar // 点阵序列(文字解析结果)
|
|
|
|
|
+ maxH int // 序列最大高(行数)
|
|
|
|
|
+ offset int // 当前水平偏移(px,向左滚为负累加)
|
|
|
|
|
+ period int // 循环周期(px)
|
|
|
|
|
+ prevTs int64 // 上次绘制时间戳(ms,用于 dt 计算)
|
|
|
|
|
+
|
|
|
|
|
+ // 交互状态
|
|
|
|
|
+ hover bool // 鼠标是否悬停在窗口上(显示边框+按钮)
|
|
|
|
|
+ zoom float64 // 缩放倍率(滚轮调整,影响点距)
|
|
|
|
|
+ dragging bool // 是否正在拖动窗口
|
|
|
|
|
+ closeHot bool // 鼠标是否悬停关闭按钮
|
|
|
|
|
+ zoomHot bool // 鼠标是否悬停缩放按钮
|
|
|
|
|
+ lastMouse point // 最近鼠标位置(窗口客户区坐标)
|
|
|
|
|
+ closeRect rect // 关闭按钮区域(客户区坐标)
|
|
|
|
|
+ zoomRect rect // 缩放按钮区域(客户区坐标)
|
|
|
|
|
+ tracking bool // 是否已注册 TrackMouseEvent
|
|
|
|
|
+
|
|
|
|
|
+ wndProc uintptr // 窗口过程回调地址(防 GC)
|
|
|
|
|
+
|
|
|
|
|
+ // UpdateLayeredWindow 像素级透明所需的 DIB 资源(窗口线程创建/持有)
|
|
|
|
|
+ memDC uintptr // 内存 DC(DIB 被选中)
|
|
|
|
|
+ dibBmp uintptr // 32 位 DIB section 的 HBITMAP
|
|
|
|
|
+ dibPtr unsafe.Pointer // DIB 像素内存指针(BGRA 每像素 4 字节)
|
|
|
|
|
+ dibW int // DIB 宽
|
|
|
|
|
+ dibH int // DIB 高
|
|
|
|
|
+
|
|
|
|
|
+ classSeq int // 窗口类名序号(保证类名唯一,避免重复注册同名类)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+var ledWinState = &ledWindowState{}
|
|
|
|
|
+
|
|
|
|
|
+// OpenLEDWindow 打开(或更新)LED 点阵滚动窗口并显示指定文字。
|
|
|
|
|
+// text 为空时不更新内容(保留上次文字),但窗口未开则忽略。
|
|
|
|
|
+// color 非空时保存为点阵颜色并持久化到设置。
|
|
|
|
|
+// speed > 0 时更新滚动速度(px/s)。
|
|
|
|
|
+// 返回是否成功打开(供前端提示)。
|
|
|
|
|
+func (s *SettingsService) OpenLEDWindow(text string, color string, speed int) bool {
|
|
|
|
|
+ if color != "" {
|
|
|
|
|
+ ledSaveColor(color)
|
|
|
|
|
+ }
|
|
|
|
|
+ if speed > 0 {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.speed = speed
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if text == "" {
|
|
|
|
|
+ // 空文字:窗口已开则保留,否则不创建
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ open := ledWinState.active
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ return open
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新文字与颜色
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.text = text
|
|
|
|
|
+ if color != "" {
|
|
|
|
|
+ ledWinState.color = ledParseColor(color)
|
|
|
|
|
+ } else if ledWinState.color == 0 {
|
|
|
|
|
+ ledWinState.color = ledParseColor(ledLoadColor())
|
|
|
|
|
+ }
|
|
|
|
|
+ if ledWinState.speed <= 0 {
|
|
|
|
|
+ ledWinState.speed = 60
|
|
|
|
|
+ }
|
|
|
|
|
+ ledWinState.seq = nil // 强制重新解析(窗口线程重建序列)
|
|
|
|
|
+ started := ledWinState.started
|
|
|
|
|
+ ledWinState.started = true
|
|
|
|
|
+ open := ledWinState.active
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ if !started {
|
|
|
|
|
+ // 首次:启动窗口线程(建窗、消息循环)
|
|
|
|
|
+ go ledWindowLoop()
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ // 已启动:唤醒窗口线程立即刷新
|
|
|
|
|
+ if open {
|
|
|
|
|
+ ledPostRefresh()
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// CloseLEDWindow 关闭 LED 滚动窗口(窗口已关则忽略)。
|
|
|
|
|
+func (s *SettingsService) CloseLEDWindow() {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ hwnd := ledWinState.hwnd
|
|
|
|
|
+ ledWinState.closed = true
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ if hwnd != 0 {
|
|
|
|
|
+ procPostMessage.Call(hwnd, wmClose, 0, 0)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// SetLEDTransparent 保留空实现:原生分层窗口透明由 SetLayeredWindowAttributes
|
|
|
|
|
+// 在建窗时设置,无需前端补设。兼容前端 bindings 调用(不再报错)。
|
|
|
|
|
+func (s *SettingsService) SetLEDTransparent() {
|
|
|
|
|
+ // 原生窗口透明已由 LWA_COLORKEY 在建窗时实现,此处无操作。
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// GetLEDText 返回当前要显示的 LED 文字。
|
|
|
|
|
+func (s *SettingsService) GetLEDText() string {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ defer ledWinState.mu.Unlock()
|
|
|
|
|
+ return ledWinState.text
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// GetLEDColor 返回当前 LED 点阵颜色字符串(默认 #ff4d4d)。
|
|
|
|
|
+func (s *SettingsService) GetLEDColor() string {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ c := ledWinState.color
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ if c == 0 {
|
|
|
|
|
+ c = ledParseColor(ledLoadColor())
|
|
|
|
|
+ }
|
|
|
|
|
+ return ledFormatColor(c)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// GetLEDSpeed 返回当前 LED 滚动速度(px/s),<=0 用默认 60。
|
|
|
|
|
+func (s *SettingsService) GetLEDSpeed() int {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ defer ledWinState.mu.Unlock()
|
|
|
|
|
+ if ledWinState.speed <= 0 {
|
|
|
|
|
+ ledWinState.speed = 60
|
|
|
|
|
+ }
|
|
|
|
|
+ return ledWinState.speed
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledSaveColor 持久化 LED 颜色到设置(颜色非空时)。
|
|
|
|
|
+func ledSaveColor(color string) {
|
|
|
|
|
+ if color == "" {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ settings := loadSettings()
|
|
|
|
|
+ settings.LEDColor = color
|
|
|
|
|
+ _ = saveSettings(settings)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledLoadColor 从设置读取已保存的 LED 颜色。
|
|
|
|
|
+func ledLoadColor() string {
|
|
|
|
|
+ c := loadSettings().LEDColor
|
|
|
|
|
+ if c == "" {
|
|
|
|
|
+ c = "#ff4d4d"
|
|
|
|
|
+ }
|
|
|
|
|
+ return c
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledFormatColor 把 COLORREF 转回 #RRGGBB 字符串。
|
|
|
|
|
+func ledFormatColor(c uintptr) string {
|
|
|
|
|
+ r := (c >> 0) & 0xFF
|
|
|
|
|
+ g := (c >> 8) & 0xFF
|
|
|
|
|
+ b := (c >> 16) & 0xFF
|
|
|
|
|
+ return fmt.Sprintf("#%02x%02x%02x", r, g, b)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledPostRefresh 向窗口线程投递自定义刷新消息(窗口已建时)。
|
|
|
|
|
+func ledPostRefresh() {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ hwnd := ledWinState.hwnd
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ if hwnd != 0 {
|
|
|
|
|
+ procPostMessage.Call(hwnd, wmRefresh, 0, 0)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+// 窗口创建 / 消息循环
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+// ledWindowLoop 窗口线程:注册窗口类、建窗、显示、跑消息循环。
|
|
|
|
|
+// 所有窗口操作必须在同一线程(GUI 线程),Win32 要求。
|
|
|
|
|
+func ledWindowLoop() {
|
|
|
|
|
+ runtime.LockOSThread()
|
|
|
|
|
+ defer runtime.UnlockOSThread()
|
|
|
|
|
+
|
|
|
|
|
+ hinst, _, _ := procGetModuleHandleW.Call(0)
|
|
|
|
|
+
|
|
|
|
|
+ // 注册窗口类:类名唯一(序号递增),避免重复注册同名类失败
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.classSeq++
|
|
|
|
|
+ clsName := fmt.Sprintf("LedScrollClass_%d", ledWinState.classSeq)
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ className, _ := syscall.UTF16PtrFromString(clsName)
|
|
|
|
|
+ wc := wndclassex{}
|
|
|
|
|
+ wc.CbSize = uint32(unsafe.Sizeof(wndclassex{}))
|
|
|
|
|
+ wc.Style = 0
|
|
|
|
|
+ wc.LpfnWndProc = ledWinState.wndProcAddr()
|
|
|
|
|
+ wc.HInstance = hinst
|
|
|
|
|
+ wc.HCursor = 0
|
|
|
|
|
+ wc.HbrBackground = 0 // 无背景(WM_ERASEBKGND 自己填充黑,避免闪烁)
|
|
|
|
|
+ wc.LpszClassName = className
|
|
|
|
|
+
|
|
|
|
|
+ atom, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
|
|
|
|
|
+ if atom == 0 {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.started = false
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建窗口:分层 + 置顶 + 工具窗
|
|
|
|
|
+ // 注意:不加 WS_EX_NOACTIVATE——某些 Win11 版本下 NOACTIVATE 与 LAYERED 组合
|
|
|
|
|
+ // 会导致窗口不渲染(不可见)。避免激活改用 ShowWindow(SW_SHOWNOACTIVATE)。
|
|
|
|
|
+ exStyle := uintptr(wndExLayered | wndExTool | wndExTopMost)
|
|
|
|
|
+ style := uintptr(0x80000000) // WS_POPUP(无边框、无标题、无最小化)
|
|
|
|
|
+ hwnd, _, _ := procCreateWindowEx.Call(
|
|
|
|
|
+ exStyle,
|
|
|
|
|
+ uintptr(unsafe.Pointer(className)),
|
|
|
|
|
+ 0, // 无标题
|
|
|
|
|
+ style,
|
|
|
|
|
+ 0, 0, // 位置:初始 0,0,随后居中
|
|
|
|
|
+ ledWinWidth, ledWinHeight,
|
|
|
|
|
+ 0, 0, hinst, 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ if hwnd == 0 {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.started = false
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.hwnd = hwnd
|
|
|
|
|
+ ledWinState.active = true
|
|
|
|
|
+ ledWinState.closed = false
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // 像素级透明:创建 32 位 DIB section,用 UpdateLayeredWindow 逐像素提交。
|
|
|
|
|
+ // 背景像素 alpha=0(全透明),点阵像素 alpha=255(不透明)。
|
|
|
|
|
+ // 注意:不再用 SetLayeredWindowAttributes(LWA_COLORKEY) —— 本环境调用虽返回
|
|
|
|
|
+ // 成功但实际不生效(窗口显示为不透明白色矩形,自检已证实)。
|
|
|
|
|
+ if !ledEnsureDIB(ledWinWidth, ledWinHeight) {
|
|
|
|
|
+ procSetLayeredWindowAttr.Call(hwnd, 0, 200, lwaAlpha) // 兜底
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 居中显示(不激活,避免抢焦点)
|
|
|
|
|
+ ledCenterWindow(hwnd, ledWinWidth, ledWinHeight)
|
|
|
|
|
+ procShowWindow.Call(hwnd, 4) // SW_SHOWNOACTIVATE
|
|
|
|
|
+
|
|
|
|
|
+ // 置顶 + 不激活
|
|
|
|
|
+ procSetWindowPos.Call(hwnd, hwndTopMost, 0, 0, 0, 0,
|
|
|
|
|
+ swpNoActivate|swpShowWindow|0x0001 /* SWP_NOMOVE */|0x0002 /* SWP_NOSIZE */)
|
|
|
|
|
+
|
|
|
|
|
+ // 滚动定时器(约 16ms)
|
|
|
|
|
+ procSetTimer.Call(hwnd, timerScroll, 16, 0)
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化按钮命中区域(否则 closeRect/zoomRect 全 0,按钮点击失效)
|
|
|
|
|
+ ledInitButtonRects()
|
|
|
|
|
+
|
|
|
|
|
+ // 首次建窗后立即解析点阵序列(否则 seq=nil → 只画透明背景)
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ if ledWinState.zoom <= 0 {
|
|
|
|
|
+ ledWinState.zoom = 1.0 // 缩放倍率默认 1.0(新建窗口重置)
|
|
|
|
|
+ }
|
|
|
|
|
+ ledWinState.hover = false
|
|
|
|
|
+ ledWinState.closeHot = false
|
|
|
|
|
+ ledWinState.zoomHot = false
|
|
|
|
|
+ ledWinState.tracking = false
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ ledRebuildSequence()
|
|
|
|
|
+ // 建窗后按实际内容调整窗口尺寸(否则固定 720×260,长文字或缩放后可能裁剪)
|
|
|
|
|
+ ledResizeWindow(hwnd)
|
|
|
|
|
+ ledRenderFrame(hwnd) // 首帧提交
|
|
|
|
|
+
|
|
|
|
|
+ // 消息循环
|
|
|
|
|
+ var msg msg
|
|
|
|
|
+ for {
|
|
|
|
|
+ ret, _, _ := procGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
|
|
|
|
|
+ if int32(ret) == 0 || int32(ret) == -1 {
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
|
|
|
|
+ procDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 退出清理
|
|
|
|
|
+ procKillTimer.Call(hwnd, timerScroll)
|
|
|
|
|
+ ledReleaseDIB() // 释放 DIB section / memDC
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.hwnd = 0
|
|
|
|
|
+ ledWinState.active = false
|
|
|
|
|
+ ledWinState.started = false
|
|
|
|
|
+ ledWinState.seq = nil
|
|
|
|
|
+ ledWinState.offset = 0
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// wndProcAddr 返回 LED 窗口过程地址(保持 Go 函数引用,防 GC)。
|
|
|
|
|
+// 必须在窗口线程内、RegisterClassEx 之前惰性初始化——与 mousealert_windows.go
|
|
|
|
|
+// 的 wndProcAddr() 一致。不能在全局 init() 里 NewCallback(绑定时机错误,会导致
|
|
|
|
|
+// RegisterClassEx 拒绝注册)。
|
|
|
|
|
+func (st *ledWindowState) wndProcAddr() uintptr {
|
|
|
|
|
+ if st.wndProc != 0 {
|
|
|
|
|
+ return st.wndProc
|
|
|
|
|
+ }
|
|
|
|
|
+ st.wndProc = syscall.NewCallback(ledWndProc)
|
|
|
|
|
+ return st.wndProc
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledCenterWindow 把窗口移到屏幕水平居中、垂直约 1/4 处。
|
|
|
|
|
+func ledCenterWindow(hwnd uintptr, w, h int) {
|
|
|
|
|
+ sw, _, _ := ledGetSystemMetrics.Call(0) // SM_CXSCREEN
|
|
|
|
|
+ sh, _, _ := ledGetSystemMetrics.Call(1) // SM_CYSCREEN
|
|
|
|
|
+ x := (int32(sw) - int32(w)) / 2
|
|
|
|
|
+ y := (int32(sh) - int32(h)) / 4
|
|
|
|
|
+ // SWP_NOSIZE(0x0001)|SWP_NOZORDER(0x0004)|SWP_NOACTIVATE(0x0010):只移动,不改大小/层级/激活
|
|
|
|
|
+ // 必须含 SWP_NOSIZE,否则 cx=cy=0 会把窗口尺寸设成 0×0(客户区为 0,画面空白)
|
|
|
|
|
+ procSetWindowPos.Call(hwnd, 0, uintptr(x), uintptr(y), 0, 0, 0x0001|0x0004|0x0010)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledInitButtonRects 初始化关闭/缩放按钮的命中区域(客户区坐标)。
|
|
|
|
|
+// 使用当前窗口实际尺寸(ledResizeWindow 缩放后调用,按钮随窗口动态移动)。
|
|
|
|
|
+func ledInitButtonRects() {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ w := ledWinState.dibW
|
|
|
|
|
+ if w <= 0 {
|
|
|
|
|
+ w = ledWinWidth
|
|
|
|
|
+ }
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ closeCx := w - ledBorderW - ledBtnMargin - ledBtnSize/2
|
|
|
|
|
+ closeCy := ledBtnCloseTop + ledBtnSize/2
|
|
|
|
|
+ zoomCx := ledBorderW + ledBtnMargin + ledBtnSize/2
|
|
|
|
|
+ zoomCy := ledBtnZoomTop + ledBtnSize/2
|
|
|
|
|
+ r := ledBtnSize / 2
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.closeRect = rect{
|
|
|
|
|
+ Left: int32(closeCx - r), Top: int32(closeCy - r),
|
|
|
|
|
+ Right: int32(closeCx + r), Bottom: int32(closeCy + r),
|
|
|
|
|
+ }
|
|
|
|
|
+ ledWinState.zoomRect = rect{
|
|
|
|
|
+ Left: int32(zoomCx - r), Top: int32(zoomCy - r),
|
|
|
|
|
+ Right: int32(zoomCx + r), Bottom: int32(zoomCy + r),
|
|
|
|
|
+ }
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledTrackLeave 注册 TrackMouseEvent,鼠标离开窗口时收到 WM_MOUSELEAVE。
|
|
|
|
|
+func ledTrackLeave(hwnd uintptr) {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ if ledWinState.tracking {
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ ledWinState.tracking = true
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ tme := trackMouseEvent{
|
|
|
|
|
+ CbSize: uint32(unsafe.Sizeof(trackMouseEvent{})),
|
|
|
|
|
+ DwFlags: tmeLeave,
|
|
|
|
|
+ HwndTrack: hwnd,
|
|
|
|
|
+ DwHoverTime: 0,
|
|
|
|
|
+ }
|
|
|
|
|
+ procTrackMouseEvent.Call(uintptr(unsafe.Pointer(&tme)))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+// 窗口过程
|
|
|
|
|
+// ------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+// ledWndProc 窗口过程。wParam/lParam 按消息解释。
|
|
|
|
|
+// 返回 0 表示已处理,非 0 由 DefWindowProc 语义。
|
|
|
|
|
+func ledWndProc(hwnd uintptr, msg uint32, wParam uintptr, lParam uintptr) uintptr {
|
|
|
|
|
+ switch msg {
|
|
|
|
|
+ case wmPaint:
|
|
|
|
|
+ // 分层窗口用 UpdateLayeredWindow 提交,无需 WM_PAINT GDI 绘制;
|
|
|
|
|
+ // 但窗口仍可能收到 WM_PAINT(如系统要求重绘),直接渲染一帧即可。
|
|
|
|
|
+ ledRenderFrame(hwnd)
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ case wmTimer:
|
|
|
|
|
+ if uintptr(wParam) == timerScroll {
|
|
|
|
|
+ ledAdvanceScroll()
|
|
|
|
|
+ ledRenderFrame(hwnd) // 每帧直接提交到屏幕
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ case wmRefresh:
|
|
|
|
|
+ // 文字已更新:重新解析点阵 + 重置偏移 + 窗口尺寸随内容调整 + 渲染一帧
|
|
|
|
|
+ ledRebuildSequence()
|
|
|
|
|
+ ledResizeWindow(hwnd)
|
|
|
|
|
+ ledRenderFrame(hwnd)
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ case wmNcHitTest:
|
|
|
|
|
+ // 返回 HTCLIENT(默认),不用 HTCAPTION —— 对 WS_EX_LAYERED+WS_EX_NOACTIVATE
|
|
|
|
|
+ // 窗口,HTCAPTION 会让系统进入"标题栏移动"状态,阻断 WM_PAINT 派发(画面空白)。
|
|
|
|
|
+ // 拖动改由 WM_LBUTTONDOWN 触发 SendMessage(WM_NCLBUTTONDOWN, HTCAPTION)。
|
|
|
|
|
+ r2, _, _ := procDefWindowProc.Call(hwnd, uintptr(msg), wParam, lParam)
|
|
|
|
|
+ return r2
|
|
|
|
|
+
|
|
|
|
|
+ case wmLButtonDown:
|
|
|
|
|
+ // 判断是否点中关闭/缩放按钮;否则整窗拖动(标题栏移动)
|
|
|
|
|
+ x := int16(uint16(lParam & 0xFFFF))
|
|
|
|
|
+ y := int16(uint16((lParam >> 16) & 0xFFFF))
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ closeRect := ledWinState.closeRect
|
|
|
|
|
+ zoomRect := ledWinState.zoomRect
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ cx := int32(x)
|
|
|
|
|
+ cy := int32(y)
|
|
|
|
|
+ inClose := cx >= closeRect.Left && cx <= closeRect.Right && cy >= closeRect.Top && cy <= closeRect.Bottom
|
|
|
|
|
+ inZoom := cx >= zoomRect.Left && cx <= zoomRect.Right && cy >= zoomRect.Top && cy <= zoomRect.Bottom
|
|
|
|
|
+
|
|
|
|
|
+ if inClose {
|
|
|
|
|
+ // 关闭按钮:销毁窗口
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.hover = false
|
|
|
|
|
+ ledWinState.closeHot = false
|
|
|
|
|
+ ledWinState.zoomHot = false
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ procDestroyWindow.Call(hwnd)
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+ if inZoom {
|
|
|
|
|
+ // 缩放按钮:切换缩放(1.0 → 1.5 → 0.6 → 1.0),并重建周期
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ switch ledWinState.zoom {
|
|
|
|
|
+ case 1.0:
|
|
|
|
|
+ ledWinState.zoom = 1.5
|
|
|
|
|
+ case 1.5:
|
|
|
|
|
+ ledWinState.zoom = 0.6
|
|
|
|
|
+ default:
|
|
|
|
|
+ ledWinState.zoom = 1.0
|
|
|
|
|
+ }
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ ledRebuildSequence()
|
|
|
|
|
+ ledResizeWindow(hwnd)
|
|
|
|
|
+ ledRenderFrame(hwnd)
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+ // 普通区域:整窗拖动(标题栏移动,不抢焦点)
|
|
|
|
|
+ ledSendMessageW.Call(hwnd, wmNcLButtonDown, htCaption, lParam)
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ case wmLButtonUp:
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ case wmMouseMove:
|
|
|
|
|
+ // 记录鼠标位置,注册离开检测,更新按钮悬停,重绘(悬停边框)
|
|
|
|
|
+ x := int16(uint16(lParam & 0xFFFF))
|
|
|
|
|
+ y := int16(uint16((lParam >> 16) & 0xFFFF))
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.lastMouse = point{X: int32(x), Y: int32(y)}
|
|
|
|
|
+ wasHover := ledWinState.hover
|
|
|
|
|
+ ledWinState.hover = true
|
|
|
|
|
+ closeRect := ledWinState.closeRect
|
|
|
|
|
+ zoomRect := ledWinState.zoomRect
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ if !wasHover {
|
|
|
|
|
+ // 首次悬停:注册 TrackMouseEvent(离开时收 WM_MOUSELEAVE)
|
|
|
|
|
+ ledTrackLeave(hwnd)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新按钮悬停状态
|
|
|
|
|
+ cx := int32(x)
|
|
|
|
|
+ cy := int32(y)
|
|
|
|
|
+ inClose := cx >= closeRect.Left && cx <= closeRect.Right && cy >= closeRect.Top && cy <= closeRect.Bottom
|
|
|
|
|
+ inZoom := cx >= zoomRect.Left && cx <= zoomRect.Right && cy >= zoomRect.Top && cy <= zoomRect.Bottom
|
|
|
|
|
+ changed := false
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ if ledWinState.closeHot != inClose {
|
|
|
|
|
+ ledWinState.closeHot = inClose
|
|
|
|
|
+ changed = true
|
|
|
|
|
+ }
|
|
|
|
|
+ if ledWinState.zoomHot != inZoom {
|
|
|
|
|
+ ledWinState.zoomHot = inZoom
|
|
|
|
|
+ changed = true
|
|
|
|
|
+ }
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ // 首次悬停或按钮悬停变化时重绘(显示/更新边框与按钮)
|
|
|
|
|
+ if changed || !wasHover {
|
|
|
|
|
+ ledRenderFrame(hwnd)
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ case wmMouseLeave:
|
|
|
|
|
+ // 鼠标离开:隐藏边框与按钮
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.hover = false
|
|
|
|
|
+ ledWinState.closeHot = false
|
|
|
|
|
+ ledWinState.zoomHot = false
|
|
|
|
|
+ ledWinState.tracking = false
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ ledRenderFrame(hwnd)
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ case wmMouseWheel:
|
|
|
|
|
+ // 滚轮缩放:调整点距倍率
|
|
|
|
|
+ delta := int16(uint16(wParam >> 16)) // HIWORD = 滚轮增量
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ z := ledWinState.zoom
|
|
|
|
|
+ if z <= 0 {
|
|
|
|
|
+ z = 1
|
|
|
|
|
+ }
|
|
|
|
|
+ if delta > 0 {
|
|
|
|
|
+ z += ledZoomStep
|
|
|
|
|
+ } else {
|
|
|
|
|
+ z -= ledZoomStep
|
|
|
|
|
+ }
|
|
|
|
|
+ if z < ledZoomMin {
|
|
|
|
|
+ z = ledZoomMin
|
|
|
|
|
+ }
|
|
|
|
|
+ if z > ledZoomMax {
|
|
|
|
|
+ z = ledZoomMax
|
|
|
|
|
+ }
|
|
|
|
|
+ ledWinState.zoom = z
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ // 缩放改变点距 → 重建周期(平铺宽度变化)
|
|
|
|
|
+ ledRebuildSequence()
|
|
|
|
|
+ // 窗口尺寸随点距/内容动态变化,保证放大后点阵不被裁剪
|
|
|
|
|
+ ledResizeWindow(hwnd)
|
|
|
|
|
+ ledRenderFrame(hwnd)
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ case wmClose:
|
|
|
|
|
+ procDestroyWindow.Call(hwnd)
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ case wmDestroy:
|
|
|
|
|
+ ledPostQuitMessage.Call(0)
|
|
|
|
|
+ return 0
|
|
|
|
|
+ }
|
|
|
|
|
+ r, _, _ := procDefWindowProc.Call(hwnd, uintptr(msg), wParam, lParam)
|
|
|
|
|
+ return r
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledRebuildSequence 重建点阵序列(窗口线程内调用)。
|
|
|
|
|
+func ledRebuildSequence() {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ text := ledWinState.text
|
|
|
|
|
+ color := ledWinState.color
|
|
|
|
|
+ if color == 0 {
|
|
|
|
|
+ color = ledParseColor(ledLoadColor())
|
|
|
|
|
+ }
|
|
|
|
|
+ hwnd := ledWinState.hwnd
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // 用窗口 DC 采样汉字
|
|
|
|
|
+ hdc, _, _ := ledGetDC.Call(hwnd)
|
|
|
|
|
+ // 文字末尾追加两个空格:无限滚动循环时形成断句分隔,
|
|
|
|
|
+ // 避免前一句末字与后一句首字粘连、观看者无法断句。
|
|
|
|
|
+ text = text + " "
|
|
|
|
|
+ seq := textToLedSequence(hdc, text)
|
|
|
|
|
+ if hdc != 0 {
|
|
|
|
|
+ ledReleaseDC.Call(hwnd, hdc)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ maxH := ledSequenceMaxH(seq)
|
|
|
|
|
+
|
|
|
|
|
+ // 无缝无限滚动:把序列平铺两份(seq + seq),offset 在 [-单份宽, 0) 内回绕,
|
|
|
|
|
+ // 这样任何时候窗口内都有连续文字,循环处无缝衔接。
|
|
|
|
|
+ tiled := make([]ledChar, 0, len(seq)*2)
|
|
|
|
|
+ tiled = append(tiled, seq...)
|
|
|
|
|
+ tiled = append(tiled, seq...)
|
|
|
|
|
+
|
|
|
|
|
+ // 单份序列的总像素宽(平铺周期)
|
|
|
|
|
+ tileW := ledTotalPxWidth(seq, ledWinState.dot())
|
|
|
|
|
+
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.seq = tiled
|
|
|
|
|
+ ledWinState.maxH = maxH
|
|
|
|
|
+ // 循环周期 = 单份序列宽(无限平铺回绕);周期需 > 0 才回绕
|
|
|
|
|
+ if tileW > 0 {
|
|
|
|
|
+ ledWinState.period = tileW
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ledWinState.period = ledWinWidth
|
|
|
|
|
+ }
|
|
|
|
|
+ ledWinState.offset = 0
|
|
|
|
|
+ ledWinState.prevTs = 0
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// dot 计算当前点距(点阵单元像素大小)。
|
|
|
|
|
+// 基础点距固定为 ledBaseDot,再乘以滚轮缩放倍率。
|
|
|
|
|
+// 窗口高度不再参与计算——尺寸随 dot 动态变化(见 ledResizeWindow),
|
|
|
|
|
+// 避免"高度变化→点距变化→窗口再变化"的耦合振荡。
|
|
|
|
|
+func (st *ledWindowState) dot() int {
|
|
|
|
|
+ d := ledBaseDot
|
|
|
|
|
+ z := st.zoom
|
|
|
|
|
+ if z <= 0 {
|
|
|
|
|
+ z = 1
|
|
|
|
|
+ }
|
|
|
|
|
+ return int(float64(d) * z)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledTargetSize 根据当前点距、点阵高与单份序列宽计算目标窗口尺寸。
|
|
|
|
|
+// 高度容纳点阵 + 上下边距(边框+按钮);宽度容纳一个完整循环周期。
|
|
|
|
|
+func ledTargetSize(dot, maxH, tileW int) (int, int) {
|
|
|
|
|
+ if maxH <= 0 {
|
|
|
|
|
+ maxH = 32
|
|
|
|
|
+ }
|
|
|
|
|
+ lineH := maxH * dot
|
|
|
|
|
+ h := lineH + 2*ledPadV
|
|
|
|
|
+ // 至少容纳悬停按钮与边框
|
|
|
|
|
+ minH := ledBtnCloseTop + ledBtnSize + ledBtnMargin + ledBorderW
|
|
|
|
|
+ if h < minH {
|
|
|
|
|
+ h = minH
|
|
|
|
|
+ }
|
|
|
|
|
+ w := tileW + 2*ledPadH
|
|
|
|
|
+ // 上限:不超屏幕宽的 9/10(点阵再长也受限,避免窗口占满屏幕)
|
|
|
|
|
+ if sw, _, _ := ledGetSystemMetrics.Call(0); int32(sw) > 0 {
|
|
|
|
|
+ maxW := int(int32(sw)) * 9 / 10
|
|
|
|
|
+ if w > maxW {
|
|
|
|
|
+ w = maxW
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 下限:保持基础宽,避免纯字母小字串窗口过窄
|
|
|
|
|
+ if w < ledWinWidth {
|
|
|
|
|
+ w = ledWinWidth
|
|
|
|
|
+ }
|
|
|
|
|
+ return w, h
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledResizeWindow 按当前内容(点距/点阵高/序列宽)调整窗口尺寸并重建 DIB。
|
|
|
|
|
+// 必须在窗口线程内调用(缩放、文字更新后)。
|
|
|
|
|
+func ledResizeWindow(hwnd uintptr) {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ dot := ledWinState.dot()
|
|
|
|
|
+ maxH := ledWinState.maxH
|
|
|
|
|
+ tile := ledWinState.period
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ if tile <= 0 {
|
|
|
|
|
+ tile = ledWinWidth
|
|
|
|
|
+ }
|
|
|
|
|
+ w, h := ledTargetSize(dot, maxH, tile)
|
|
|
|
|
+
|
|
|
|
|
+ // 调整窗口尺寸:保持左上角位置(SWP_NOMOVE),不改层级/激活
|
|
|
|
|
+ procSetWindowPos.Call(hwnd, 0, 0, 0, uintptr(w), uintptr(h),
|
|
|
|
|
+ swpNoActivate|0x0001 /* SWP_NOMOVE */|0x0004 /* SWP_NOZORDER */)
|
|
|
|
|
+
|
|
|
|
|
+ // 重建 DIB 匹配新尺寸(释放旧 + 创建新)
|
|
|
|
|
+ if !ledEnsureDIB(w, h) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ // 按钮命中区域随窗口宽高动态更新
|
|
|
|
|
+ ledInitButtonRects()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledAdvanceScroll 按经过时间推进滚动偏移(窗口线程内调用)。
|
|
|
|
|
+func ledAdvanceScroll() {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ now := time.Now().UnixMilli()
|
|
|
|
|
+ if ledWinState.prevTs == 0 {
|
|
|
|
|
+ ledWinState.prevTs = now
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ dt := now - ledWinState.prevTs
|
|
|
|
|
+ ledWinState.prevTs = now
|
|
|
|
|
+ // 鼠标悬停时停止滚动:可放大/缩小/关闭查看点阵细节
|
|
|
|
|
+ if ledWinState.hover {
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ spd := ledWinState.speed
|
|
|
|
|
+ if spd <= 0 {
|
|
|
|
|
+ spd = 60
|
|
|
|
|
+ }
|
|
|
|
|
+ // 向左滚:offset 递减
|
|
|
|
|
+ ledWinState.offset -= int(float32(spd) * float32(dt) / 1000.0)
|
|
|
|
|
+ // 无限回绕:把 offset 归一到 (-period, 0],保证平铺无缝衔接。
|
|
|
|
|
+ // 注意 seq 已平铺两份,period=单份宽,窗口宽 < period+单份宽,任意时刻有连续文字。
|
|
|
|
|
+ if ledWinState.period > 0 {
|
|
|
|
|
+ p := ledWinState.period
|
|
|
|
|
+ // 归一:offset 在 (-p, 0]
|
|
|
|
|
+ for ledWinState.offset <= -p {
|
|
|
|
|
+ ledWinState.offset += p
|
|
|
|
|
+ }
|
|
|
|
|
+ for ledWinState.offset > 0 {
|
|
|
|
|
+ ledWinState.offset -= p
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledEnsureDIB 创建 32 位自顶向下 DIB section 并选入内存 DC。
|
|
|
|
|
+// 必须窗口线程内调用。成功后 dibPtr 指向 BGRA 像素缓冲。
|
|
|
|
|
+// w/h 为动态尺寸(随缩放变化);非法尺寸回退默认 720×260。
|
|
|
|
|
+func ledEnsureDIB(w, h int) bool {
|
|
|
|
|
+ if w <= 0 || h <= 0 {
|
|
|
|
|
+ w, h = ledWinWidth, ledWinHeight
|
|
|
|
|
+ }
|
|
|
|
|
+ // 先释放旧 DIB(幂等:首次调用 memDC=0,无操作)
|
|
|
|
|
+ ledReleaseDIB()
|
|
|
|
|
+
|
|
|
|
|
+ bmi := ledBitmapInfo{}
|
|
|
|
|
+ bmi.BmiHeader.BiSize = uint32(unsafe.Sizeof(ledBitmapInfoHeader{}))
|
|
|
|
|
+ bmi.BmiHeader.BiWidth = int32(w)
|
|
|
|
|
+ bmi.BmiHeader.BiHeight = -int32(h) // 自顶向下
|
|
|
|
|
+ bmi.BmiHeader.BiPlanes = 1
|
|
|
|
|
+ bmi.BmiHeader.BiBitCount = 32
|
|
|
|
|
+ bmi.BmiHeader.BiCompression = 0 // BI_RGB
|
|
|
|
|
+
|
|
|
|
|
+ var bits unsafe.Pointer
|
|
|
|
|
+ hbmp, _, _ := ledCreateDIBSection.Call(
|
|
|
|
|
+ 0,
|
|
|
|
|
+ uintptr(unsafe.Pointer(&bmi)),
|
|
|
|
|
+ 0, // DIB_RGB_COLORS
|
|
|
|
|
+ uintptr(unsafe.Pointer(&bits)),
|
|
|
|
|
+ 0, 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ if hbmp == 0 || bits == nil {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ memDC, _, _ := procCreateCompatibleDC.Call(0)
|
|
|
|
|
+ if memDC == 0 {
|
|
|
|
|
+ procDeleteObject.Call(hbmp)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ procSelectObject.Call(memDC, hbmp)
|
|
|
|
|
+
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ ledWinState.memDC = memDC
|
|
|
|
|
+ ledWinState.dibBmp = hbmp
|
|
|
|
|
+ ledWinState.dibPtr = bits
|
|
|
|
|
+ ledWinState.dibW = w
|
|
|
|
|
+ ledWinState.dibH = h
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ return true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledReleaseDIB 释放 DIB section 与内存 DC(窗口线程内调用)。
|
|
|
|
|
+func ledReleaseDIB() {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ memDC := ledWinState.memDC
|
|
|
|
|
+ hbmp := ledWinState.dibBmp
|
|
|
|
|
+ ledWinState.memDC = 0
|
|
|
|
|
+ ledWinState.dibBmp = 0
|
|
|
|
|
+ ledWinState.dibPtr = nil
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+ if memDC != 0 {
|
|
|
|
|
+ procDeleteDC.Call(memDC)
|
|
|
|
|
+ }
|
|
|
|
|
+ if hbmp != 0 {
|
|
|
|
|
+ procDeleteObject.Call(hbmp)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledRenderFrame 渲染一帧到 DIB 并调 UpdateLayeredWindow 提交到屏幕。
|
|
|
|
|
+// 背景像素 alpha=0(全透明),点阵像素 alpha=255(不透明,LED 颜色)。
|
|
|
|
|
+// 悬停时绘制半透明边框与按钮(关闭 / 缩放指示)。
|
|
|
|
|
+func ledRenderFrame(hwnd uintptr) {
|
|
|
|
|
+ ledWinState.mu.Lock()
|
|
|
|
|
+ memDC := ledWinState.memDC
|
|
|
|
|
+ base := ledWinState.dibPtr
|
|
|
|
|
+ w := ledWinState.dibW
|
|
|
|
|
+ h := ledWinState.dibH
|
|
|
|
|
+ seq := ledWinState.seq
|
|
|
|
|
+ offset := ledWinState.offset
|
|
|
|
|
+ maxH := ledWinState.maxH
|
|
|
|
|
+ color := ledWinState.color
|
|
|
|
|
+ dot := ledWinState.dot()
|
|
|
|
|
+ hover := ledWinState.hover
|
|
|
|
|
+ closeHot := ledWinState.closeHot
|
|
|
|
|
+ zoomHot := ledWinState.zoomHot
|
|
|
|
|
+ zoom := ledWinState.zoom
|
|
|
|
|
+ ledWinState.mu.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ if memDC == 0 || base == nil || w <= 0 || h <= 0 {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if color == 0 {
|
|
|
|
|
+ color = ledParseColor(ledLoadColor())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 1) 清空为全透明(B=0,G=0,R=0,A=0)
|
|
|
|
|
+ p := uintptr(base)
|
|
|
|
|
+ for i := 0; i < w*h*4; i += 4 {
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(p + uintptr(i))) = 0
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(p + uintptr(i) + 1)) = 0
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(p + uintptr(i) + 2)) = 0
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(p + uintptr(i) + 3)) = 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2) 画点阵:圆点(抗锯齿)+ 无限平铺
|
|
|
|
|
+ if len(seq) > 0 && maxH > 0 {
|
|
|
|
|
+ cell := dot + ledCharGap
|
|
|
|
|
+ lineH := maxH * dot
|
|
|
|
|
+ startY := (h - lineH) / 2
|
|
|
|
|
+ if startY < 0 {
|
|
|
|
|
+ startY = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ // 颜色拆分(COLORREF 0x00BBGGRR)
|
|
|
|
|
+ cr := byte(color & 0xFF)
|
|
|
|
|
+ cg := byte((color >> 8) & 0xFF)
|
|
|
|
|
+ cb := byte((color >> 16) & 0xFF)
|
|
|
|
|
+
|
|
|
|
|
+ // 平铺周期(单份序列宽),避免重复绘制超出窗口的字符
|
|
|
|
|
+ tile := ledWinState.period
|
|
|
|
|
+ if tile <= 0 {
|
|
|
|
|
+ tile = w
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ penX := offset
|
|
|
|
|
+ for _, c := range seq {
|
|
|
|
|
+ glyph := c.glyph
|
|
|
|
|
+ // 垂直居中:每个字符按其自身高度在整行高度内居中,
|
|
|
|
|
+ // 使字母(低)与汉字(高)的视觉中线对齐(避免字母顶对齐偏上)
|
|
|
|
|
+ charTop := startY + (maxH-glyph.h)/2*dot
|
|
|
|
|
+ for gy := 0; gy < glyph.h; gy++ {
|
|
|
|
|
+ for gx := 0; gx < glyph.w; gx++ {
|
|
|
|
|
+ if !glyph.on[gy][gx] {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ px := penX + gx*cell
|
|
|
|
|
+ py := charTop + gy*dot
|
|
|
|
|
+ // 只画可能与窗口相交的圆点(含一个平铺周期的余量)
|
|
|
|
|
+ if px > w || px+dot < 0 || py+dot < 0 || py > h {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ ledDrawDot(base, w, h, px, py, dot, cr, cg, cb)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ penX += glyph.w*cell + ledGap*dot
|
|
|
|
|
+ // 提前结束:当前字符已远超窗口右缘 + 平铺余量
|
|
|
|
|
+ if penX > w+tile+cell {
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3) 悬停:绘制边框 + 按钮
|
|
|
|
|
+ if hover {
|
|
|
|
|
+ ledDrawHoverUI(base, w, h, closeHot, zoomHot, zoom)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4) UpdateLayeredWindow 提交
|
|
|
|
|
+ screenDC, _, _ := ledGetDC.Call(0)
|
|
|
|
|
+ if screenDC == 0 {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ defer ledReleaseDC.Call(0, screenDC)
|
|
|
|
|
+
|
|
|
|
|
+ var srcPt point
|
|
|
|
|
+ var srcSize size
|
|
|
|
|
+ srcSize.Cx = int32(w)
|
|
|
|
|
+ srcSize.Cy = int32(h)
|
|
|
|
|
+ var dstPt point
|
|
|
|
|
+ var wr rect
|
|
|
|
|
+ procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&wr)))
|
|
|
|
|
+ dstPt.X = wr.Left
|
|
|
|
|
+ dstPt.Y = wr.Top
|
|
|
|
|
+
|
|
|
|
|
+ blend := blendfunction{
|
|
|
|
|
+ BlendOp: 0,
|
|
|
|
|
+ BlendFlags: 0,
|
|
|
|
|
+ SourceConstantAlpha: 255,
|
|
|
|
|
+ AlphaFormat: acSrcAlpha,
|
|
|
|
|
+ }
|
|
|
|
|
+ ret, _, _ := procUpdateLayeredWindow.Call(
|
|
|
|
|
+ hwnd,
|
|
|
|
|
+ screenDC,
|
|
|
|
|
+ uintptr(unsafe.Pointer(&dstPt)),
|
|
|
|
|
+ uintptr(unsafe.Pointer(&srcSize)),
|
|
|
|
|
+ memDC,
|
|
|
|
|
+ uintptr(unsafe.Pointer(&srcPt)),
|
|
|
|
|
+ 0,
|
|
|
|
|
+ uintptr(unsafe.Pointer(&blend)),
|
|
|
|
|
+ uintptr(ulwAlpha),
|
|
|
|
|
+ )
|
|
|
|
|
+ _ = ret
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledDrawDot 画一个圆点(抗锯齿):在 dot×dot 单元内以距离平方判断圆形。
|
|
|
|
|
+// 用 2x 超采样 + 距离软边实现平滑边缘。
|
|
|
|
|
+func ledDrawDot(base unsafe.Pointer, w, h, x0, y0, dot int, cr, cg, cb byte) {
|
|
|
|
|
+ if dot <= 0 {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ // 圆心与半径(留 1px 间隙让点阵更透气)
|
|
|
|
|
+ center := float64(dot) / 2.0
|
|
|
|
|
+ radius := float64(dot)/2.0 - 0.5
|
|
|
|
|
+ if radius < 1 {
|
|
|
|
|
+ radius = 1
|
|
|
|
|
+ }
|
|
|
|
|
+ r2 := radius * radius
|
|
|
|
|
+ // 内半径(全亮),外半径(全透明),之间线性过渡 → 抗锯齿
|
|
|
|
|
+ innerR2 := (radius - 1.0) * (radius - 1.0)
|
|
|
|
|
+ if innerR2 < 0 {
|
|
|
|
|
+ innerR2 = 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 颜色(预乘 alpha 不需要,AC_SRC_ALPHA 下 alpha 即不透明度)
|
|
|
|
|
+ for dy := 0; dy < dot; dy++ {
|
|
|
|
|
+ for dx := 0; dx < dot; dx++ {
|
|
|
|
|
+ xx := x0 + dx
|
|
|
|
|
+ yy := y0 + dy
|
|
|
|
|
+ if xx < 0 || xx >= w || yy < 0 || yy >= h {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ // 到圆心的距离平方
|
|
|
|
|
+ dxc := float64(dx) + 0.5 - center
|
|
|
|
|
+ dyc := float64(dy) + 0.5 - center
|
|
|
|
|
+ dist2 := dxc*dxc + dyc*dyc
|
|
|
|
|
+ var alpha float64
|
|
|
|
|
+ if dist2 <= innerR2 {
|
|
|
|
|
+ alpha = 1.0
|
|
|
|
|
+ } else if dist2 >= r2 {
|
|
|
|
|
+ alpha = 0.0
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 线性软边
|
|
|
|
|
+ alpha = (r2 - dist2) / (r2 - innerR2)
|
|
|
|
|
+ if alpha < 0 {
|
|
|
|
|
+ alpha = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ if alpha > 1 {
|
|
|
|
|
+ alpha = 1
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if alpha <= 0 {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ a := byte(alpha * 255)
|
|
|
|
|
+ off := (yy*w + xx) * 4
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = cb
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = cg
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = cr
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = a
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ledDrawHoverUI 绘制悬停边框 + 关闭按钮 + 缩放指示按钮。
|
|
|
|
|
+func ledDrawHoverUI(base unsafe.Pointer, w, h int, closeHot, zoomHot bool, zoom float64) {
|
|
|
|
|
+ // 边框:浅蓝半透明(alpha 130),宽 ledBorderW
|
|
|
|
|
+ border := byte(130)
|
|
|
|
|
+ // 边框颜色(浅灰蓝)
|
|
|
|
|
+ br, bg, bb := byte(160), byte(200), byte(255)
|
|
|
|
|
+ for y := 0; y < h; y++ {
|
|
|
|
|
+ for x := 0; x < w; x++ {
|
|
|
|
|
+ on := x < ledBorderW || x >= w-ledBorderW || y < ledBorderW || y >= h-ledBorderW
|
|
|
|
|
+ if !on {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ off := (y*w + x) * 4
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = bb
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = bg
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = br
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = border
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 关闭按钮(右上角圆形 ×)
|
|
|
|
|
+ closeCx := w - ledBorderW - ledBtnMargin - ledBtnSize/2
|
|
|
|
|
+ closeCy := ledBtnCloseTop + ledBtnSize/2
|
|
|
|
|
+ drawCircleButton(base, w, h, closeCx, closeCy, ledBtnSize/2,
|
|
|
|
|
+ 200, 80, 80, // 红
|
|
|
|
|
+ closeHot, func(px, py int) {
|
|
|
|
|
+ // 画 ×
|
|
|
|
|
+ r := ledBtnSize / 4
|
|
|
|
|
+ if abs(px-closeCx)+abs(py-closeCy) <= r {
|
|
|
|
|
+ off := (py*w + px) * 4
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = 255
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = 255
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = 255
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = 255
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 缩放按钮(左上角圆形,显示 +/-)
|
|
|
|
|
+ zoomCx := ledBorderW + ledBtnMargin + ledBtnSize/2
|
|
|
|
|
+ zoomCy := ledBtnZoomTop + ledBtnSize/2
|
|
|
|
|
+ drawCircleButton(base, w, h, zoomCx, zoomCy, ledBtnSize/2,
|
|
|
|
|
+ 80, 160, 200, // 蓝
|
|
|
|
|
+ zoomHot, func(px, py int) {
|
|
|
|
|
+ // 画放大镜:中心圆 + 手柄
|
|
|
|
|
+ off := (py*w + px) * 4
|
|
|
|
|
+ // 手柄(对角线)
|
|
|
|
|
+ if abs(px-zoomCx)+abs(py-zoomCy) > ledBtnSize/5 && abs(px-zoomCx)+abs(py-zoomCy) <= ledBtnSize/3 {
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = 255
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = 255
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = 255
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = 255
|
|
|
|
|
+ }
|
|
|
|
|
+ // 中间横杠
|
|
|
|
|
+ if abs(py-zoomCy) <= 1 && abs(px-zoomCx) <= ledBtnSize/6 {
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = 255
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = 255
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = 255
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = 255
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 缩放倍率文本(右上角下方,简单显示 zoom)
|
|
|
|
|
+ _ = zoom
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// drawCircleButton 画一个圆形按钮(实心 + 悬停高亮),中心 (cx,cy) 半径 r。
|
|
|
|
|
+// content 在按钮内部像素上回调,用于画图标。
|
|
|
|
|
+func drawCircleButton(base unsafe.Pointer, w, h, cx, cy, r int, cr, cg, cb byte, hot bool, content func(px, py int)) {
|
|
|
|
|
+ r2 := r * r
|
|
|
|
|
+ for dy := -r; dy <= r; dy++ {
|
|
|
|
|
+ for dx := -r; dx <= r; dx++ {
|
|
|
|
|
+ xx := cx + dx
|
|
|
|
|
+ yy := cy + dy
|
|
|
|
|
+ if xx < 0 || xx >= w || yy < 0 || yy >= h {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ if dx*dx+dy*dy > r2 {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ off := (yy*w + xx) * 4
|
|
|
|
|
+ if hot {
|
|
|
|
|
+ // 悬停:亮一点
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = cb
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = cg
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = cr
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = 230
|
|
|
|
|
+ } else {
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = cb
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = cg
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = cr
|
|
|
|
|
+ *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = 180
|
|
|
|
|
+ }
|
|
|
|
|
+ if content != nil {
|
|
|
|
|
+ content(xx, yy)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// abs 返回绝对值。
|
|
|
|
|
+func abs(x int) int {
|
|
|
|
|
+ if x < 0 {
|
|
|
|
|
+ return -x
|
|
|
|
|
+ }
|
|
|
|
|
+ return x
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 调试日志已移除:不再写 led_transparent.log,避免残留记录文件。
|