ledwindow_windows.go 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510
  1. //go:build windows
  2. // ============================================================================
  3. // ledwindow_windows.go — LED 点阵滚动文字(Windows 原生实现)
  4. //
  5. // 需求:输入文字(含中文),转换为 LED 点阵并在独立的无边框、透明、置顶、
  6. // 可拖动窗口中横向循环滚动显示。
  7. //
  8. // 为何不用 Wails WebView2:WebView2 的默认背景不透明问题无法通过
  9. // PutDefaultBackgroundColor(0,0,0,0) 根治——组合宿主(DComp)下视觉无
  10. // SetAlphaMode,HWND 宿主下 WebView2 默认背景在未聚焦/聚焦时仍是白/蓝色。
  11. // 因此本窗口完全用 Win32 API 原生实现,不经过 Wails:
  12. //
  13. // 1. 分层窗口 WS_EX_LAYERED + 32 位 DIB section + UpdateLayeredWindow(ULW_ALPHA),
  14. // 逐像素 alpha 实现真正透明:背景 alpha=0(全透明),点阵 alpha=255(不透明)。
  15. // (SetLayeredWindowAttributes(LWA_COLORKEY) 在本环境调用虽成功但实际失效,
  16. // 故改用 UpdateLayeredWindow 像素级透明。)
  17. // 2. 点阵直接写入 DIB 内存(BGRA),每帧用 UpdateLayeredWindow 提交到屏幕。
  18. // 3. 拖动:WM_LBUTTONDOWN → SendMessage(WM_NCLBUTTONDOWN, HTCAPTION),整窗可拖动。
  19. // 4. 置顶:WS_EX_TOPMOST;不占任务栏:WS_EX_TOOLWINDOW;不抢焦点:ShowWindow(SW_SHOWNOACTIVATE)。
  20. // 5. 文字点阵:数字/字母/符号用内置 5x7 字模放大 2 倍(10x14),
  21. // 汉字用 GDI 字体渲染到内存位图后按网格采样为 24x32 点阵(按 RGB 亮度采样,
  22. // 因 GDI TextOutW 不写 32 位 DIB 的 alpha 通道)。
  23. // 6. 横向循环滚动:窗口线程定时器按速度推进 offset,循环回绕。
  24. //
  25. // 说明:本文件仅在 Windows 编译。其他平台见 ledwindow_other.go 空实现。
  26. // 共享的 Win32 常量/proc/结构体(wmTimer、wndExLayered、procGetMessage、
  27. // wndclassex 等)复用 mousealert_windows.go 的定义,避免重复声明冲突。
  28. // ============================================================================
  29. package main
  30. import (
  31. "fmt"
  32. "runtime"
  33. "strings"
  34. "sync"
  35. "syscall"
  36. "time"
  37. "unsafe"
  38. )
  39. // ------------------------------------------------------------------------
  40. // Windows API 常量(LED 专属;共享常量见 mousealert_windows.go)
  41. // ------------------------------------------------------------------------
  42. const (
  43. wmNcHitTest = 0x0084
  44. wmClose = 0x0010
  45. wmNcLButtonDown = 0x00A1
  46. wmLButtonDown = 0x0201
  47. wmLButtonUp = 0x0202
  48. wmMouseMove = 0x0200
  49. wmMouseLeave = 0x02A3
  50. wmMouseWheel = 0x020A
  51. timerScroll = 1 // 滚动定时器 ID
  52. // 窗口尺寸(逻辑像素)
  53. ledWinWidth = 720
  54. ledWinHeight = 260
  55. // 点距/边距:窗口随缩放动态变化(见 ledResizeWindow)。
  56. // ledBaseDot 基础点距(汉字 32 行 × 6px ≈ 192px,加边距约 260 高)。
  57. ledBaseDot = 6
  58. ledPadV = 40 // 垂直边距(容纳边框+按钮,上下各 40)
  59. ledPadH = 20 // 水平边距(容纳边框,左右各 20)
  60. // 点阵参数
  61. ledCharGap = 2 // 字符内点距(点)
  62. ledGap = 3 // 字符间距(点)
  63. // 颜色键背景(纯黑):COLORREF = 0x00BBGGRR
  64. ledBgColor = 0x00000000
  65. // UpdateLayeredWindow 参数
  66. ulwAlpha = 0x2 // ULW_ALPHA:使用 per-pixel alpha
  67. acSrcAlpha = 0x1 // AC_SRC_ALPHA:像素包含 alpha 通道(BGRA)
  68. // 悬停交互 UI 尺寸
  69. ledBorderW = 2 // 悬停边框宽度(px)
  70. ledBtnSize = 26 // 按钮直径(px)
  71. ledBtnMargin = 6 // 按钮距边框内边距(px)
  72. ledBtnCloseTop = ledBorderW + 4 // 关闭按钮上缘
  73. ledBtnZoomTop = ledBorderW + 4 // 缩放按钮上缘
  74. // 滚轮缩放档位
  75. ledZoomMin = 0.6
  76. ledZoomMax = 2.0
  77. ledZoomStep = 0.1
  78. )
  79. // htCaption HTCAPTION:WM_NCHITTEST 返回后整窗可拖动
  80. const htCaption = 2
  81. // ------------------------------------------------------------------------
  82. // Windows 结构体(LED 专属;point/rect/msg/wndclassex/paintstruct 复用共享)
  83. // ------------------------------------------------------------------------
  84. // ledBitmapInfoHeader BITMAPINFOHEADER(用于 CreateDIBSection 采样汉字点阵)
  85. type ledBitmapInfoHeader struct {
  86. BiSize uint32
  87. BiWidth int32
  88. BiHeight int32 // 负值=自顶向下
  89. BiPlanes uint16
  90. BiBitCount uint16
  91. BiCompression uint32
  92. BiSizeImage uint32
  93. BiXPelsPerMeter int32
  94. BiYPelsPerMeter int32
  95. BiClrUsed uint32
  96. BiClrImportant uint32
  97. }
  98. type ledBitmapInfo struct {
  99. BmiHeader ledBitmapInfoHeader
  100. BmiColors [3]uint32
  101. }
  102. // blendfunction BLENDFUNCTION(UpdateLayeredWindow 用)
  103. type blendfunction struct {
  104. BlendOp byte
  105. BlendFlags byte
  106. SourceConstantAlpha byte
  107. AlphaFormat byte
  108. }
  109. // trackMouseEvent TRACKMOUSEEVENT(TrackMouseEvent 用,检测鼠标离开)
  110. type trackMouseEvent struct {
  111. CbSize uint32
  112. DwFlags uint32
  113. HwndTrack uintptr
  114. DwHoverTime uint32
  115. }
  116. const (
  117. tmeLeave = 0x00000002 // TME_LEAVE
  118. )
  119. // ------------------------------------------------------------------------
  120. // LED 专属 Win32 函数(其余共享 proc 在 mousealert_windows.go 定义,复用)
  121. // ------------------------------------------------------------------------
  122. var (
  123. ledGetSystemMetrics = user32.NewProc("GetSystemMetrics")
  124. ledPostQuitMessage = user32.NewProc("PostQuitMessage")
  125. procUpdateLayeredWindow = user32.NewProc("UpdateLayeredWindow")
  126. ledGetDC = user32.NewProc("GetDC")
  127. ledReleaseDC = user32.NewProc("ReleaseDC")
  128. ledSetBkColor = gdi32.NewProc("SetBkColor")
  129. ledCreateDIBSection = gdi32.NewProc("CreateDIBSection")
  130. procGetPixel = gdi32.NewProc("GetPixel")
  131. procPatBlt = gdi32.NewProc("PatBlt")
  132. ledSendMessageW = user32.NewProc("SendMessageW")
  133. procTrackMouseEvent = user32.NewProc("TrackMouseEvent")
  134. procGetWindowRect = user32.NewProc("GetWindowRect")
  135. )
  136. // ------------------------------------------------------------------------
  137. // 5x7 点阵字模(每字符 7 行,每行 5 位,高位在左)
  138. // 与前端 led.js 的 FONT_5x7 完全一致。
  139. // ------------------------------------------------------------------------
  140. var ledFont5x7 = map[rune][7]string{
  141. '0': {"01110", "10001", "10011", "10101", "11001", "10001", "01110"},
  142. '1': {"00100", "01100", "00100", "00100", "00100", "00100", "01110"},
  143. '2': {"01110", "10001", "00001", "00010", "00100", "01000", "11111"},
  144. '3': {"11110", "00001", "00001", "01110", "00001", "00001", "11110"},
  145. '4': {"00010", "00110", "01010", "10010", "11111", "00010", "00010"},
  146. '5': {"11111", "10000", "11110", "00001", "00001", "10001", "01110"},
  147. '6': {"00110", "01000", "10000", "11110", "10001", "10001", "01110"},
  148. '7': {"11111", "00001", "00010", "00100", "01000", "01000", "01000"},
  149. '8': {"01110", "10001", "10001", "01110", "10001", "10001", "01110"},
  150. '9': {"01110", "10001", "10001", "01111", "00001", "00010", "01100"},
  151. 'A': {"01110", "10001", "10001", "11111", "10001", "10001", "10001"},
  152. 'B': {"11110", "10001", "10001", "11110", "10001", "10001", "11110"},
  153. 'C': {"01110", "10001", "10000", "10000", "10000", "10001", "01110"},
  154. 'D': {"11100", "10010", "10001", "10001", "10001", "10010", "11100"},
  155. 'E': {"11111", "10000", "10000", "11110", "10000", "10000", "11111"},
  156. 'F': {"11111", "10000", "10000", "11110", "10000", "10000", "10000"},
  157. 'G': {"01110", "10001", "10000", "10111", "10001", "10001", "01111"},
  158. 'H': {"10001", "10001", "10001", "11111", "10001", "10001", "10001"},
  159. 'I': {"01110", "00100", "00100", "00100", "00100", "00100", "01110"},
  160. 'J': {"00111", "00010", "00010", "00010", "00010", "10010", "01100"},
  161. 'K': {"10001", "10010", "10100", "11000", "10100", "10010", "10001"},
  162. 'L': {"10000", "10000", "10000", "10000", "10000", "10000", "11111"},
  163. 'M': {"10001", "11011", "10101", "10101", "10001", "10001", "10001"},
  164. 'N': {"10001", "11001", "10101", "10011", "10001", "10001", "10001"},
  165. 'O': {"01110", "10001", "10001", "10001", "10001", "10001", "01110"},
  166. 'P': {"11110", "10001", "10001", "11110", "10000", "10000", "10000"},
  167. 'Q': {"01110", "10001", "10001", "10001", "10101", "10010", "01101"},
  168. 'R': {"11110", "10001", "10001", "11110", "10100", "10010", "10001"},
  169. 'S': {"01111", "10000", "10000", "01110", "00001", "00001", "11110"},
  170. 'T': {"11111", "00100", "00100", "00100", "00100", "00100", "00100"},
  171. 'U': {"10001", "10001", "10001", "10001", "10001", "10001", "01110"},
  172. 'V': {"10001", "10001", "10001", "10001", "10001", "01010", "00100"},
  173. 'W': {"10001", "10001", "10001", "10101", "10101", "10101", "01010"},
  174. 'X': {"10001", "10001", "01010", "00100", "01010", "10001", "10001"},
  175. 'Y': {"10001", "10001", "01010", "00100", "00100", "00100", "00100"},
  176. 'Z': {"11111", "00001", "00010", "00100", "01000", "10000", "11111"},
  177. 'a': {"00000", "00000", "01110", "00001", "01111", "10001", "01111"},
  178. 'b': {"10000", "10000", "11110", "10001", "10001", "10001", "11110"},
  179. 'c': {"00000", "00000", "01110", "10001", "10000", "10001", "01110"},
  180. 'd': {"00001", "00001", "01111", "10001", "10001", "10001", "01111"},
  181. 'e': {"00000", "00000", "01110", "10001", "11111", "10000", "01110"},
  182. 'f': {"00110", "01001", "01000", "11100", "01000", "01000", "01000"},
  183. 'g': {"00000", "01111", "10001", "10001", "01111", "00001", "01110"},
  184. 'h': {"10000", "10000", "11110", "10001", "10001", "10001", "10001"},
  185. 'i': {"00100", "00000", "01100", "00100", "00100", "00100", "01110"},
  186. 'j': {"00010", "00000", "00110", "00010", "00010", "10010", "01100"},
  187. 'k': {"10000", "10000", "10010", "10100", "11000", "10100", "10010"},
  188. 'l': {"01100", "00100", "00100", "00100", "00100", "00100", "01110"},
  189. 'm': {"00000", "00000", "11010", "10101", "10101", "10101", "10101"},
  190. 'n': {"00000", "00000", "11110", "10001", "10001", "10001", "10001"},
  191. 'o': {"00000", "00000", "01110", "10001", "10001", "10001", "01110"},
  192. 'p': {"00000", "00000", "11110", "10001", "11110", "10000", "10000"},
  193. 'q': {"00000", "00000", "01111", "10001", "01111", "00001", "00001"},
  194. 'r': {"00000", "00000", "10110", "11001", "10000", "10000", "10000"},
  195. 's': {"00000", "00000", "01111", "10000", "01110", "00001", "11110"},
  196. 't': {"01000", "01000", "11100", "01000", "01000", "01001", "00110"},
  197. 'u': {"00000", "00000", "10001", "10001", "10001", "10011", "01101"},
  198. 'v': {"00000", "00000", "10001", "10001", "10001", "01010", "00100"},
  199. 'w': {"00000", "00000", "10001", "10001", "10101", "10101", "01010"},
  200. 'x': {"00000", "00000", "10001", "01010", "00100", "01010", "10001"},
  201. 'y': {"00000", "00000", "10001", "10001", "01111", "00001", "01110"},
  202. 'z': {"00000", "00000", "11111", "00010", "00100", "01000", "11111"},
  203. ' ': {"00000", "00000", "00000", "00000", "00000", "00000", "00000"},
  204. '.': {"00000", "00000", "00000", "00000", "00000", "00110", "00110"},
  205. ',': {"00000", "00000", "00000", "00000", "00000", "00110", "00100"},
  206. ':': {"00000", "00110", "00110", "00000", "00110", "00110", "00000"},
  207. ';': {"00000", "00110", "00110", "00000", "00110", "00100", "01000"},
  208. '-': {"00000", "00000", "00000", "11111", "00000", "00000", "00000"},
  209. '_': {"00000", "00000", "00000", "00000", "00000", "00000", "11111"},
  210. '+': {"00000", "00100", "00100", "11111", "00100", "00100", "00000"},
  211. '=': {"00000", "00000", "11111", "00000", "11111", "00000", "00000"},
  212. '/': {"00001", "00010", "00010", "00100", "01000", "01000", "10000"},
  213. '\\': {"10000", "01000", "01000", "00100", "00010", "00010", "00001"},
  214. '!': {"00100", "00100", "00100", "00100", "00100", "00000", "00100"},
  215. '?': {"01110", "10001", "00001", "00010", "00100", "00000", "00100"},
  216. '@': {"01110", "10001", "10111", "10101", "10111", "10000", "01110"},
  217. '#': {"01010", "01010", "11111", "01010", "11111", "01010", "01010"},
  218. '$': {"00100", "01111", "10100", "01110", "00101", "11110", "00100"},
  219. '%': {"11001", "11010", "00010", "00100", "01000", "01011", "10011"},
  220. '&': {"01100", "10010", "10100", "01000", "10101", "10010", "01101"},
  221. '*': {"00000", "00100", "10101", "01110", "10101", "00100", "00000"},
  222. '(': {"00010", "00100", "01000", "01000", "01000", "00100", "00010"},
  223. ')': {"01000", "00100", "00010", "00010", "00010", "00100", "01000"},
  224. '[': {"01110", "01000", "01000", "01000", "01000", "01000", "01110"},
  225. ']': {"01110", "00010", "00010", "00010", "00010", "00010", "01110"},
  226. '{': {"00010", "00100", "00100", "01000", "00100", "00100", "00010"},
  227. '}': {"01000", "00100", "00100", "00010", "00100", "00100", "01000"},
  228. '<': {"00010", "00100", "01000", "10000", "01000", "00100", "00010"},
  229. '>': {"01000", "00100", "00010", "00001", "00010", "00100", "01000"},
  230. '|': {"00100", "00100", "00100", "00100", "00100", "00100", "00100"},
  231. '~': {"00000", "01000", "10101", "00010", "00000", "00000", "00000"},
  232. '^': {"00100", "01010", "10001", "00000", "00000", "00000", "00000"},
  233. '。': {"00000", "00000", "00000", "00000", "00110", "00110", "00000"},
  234. ',': {"00000", "00000", "00000", "00000", "00110", "00100", "01000"},
  235. '、': {"00100", "00100", "00100", "00100", "00100", "00000", "00000"},
  236. ':': {"00000", "00110", "00110", "00000", "00110", "00110", "00000"},
  237. ';': {"00000", "00110", "00110", "00000", "00110", "00100", "01000"},
  238. '?': {"01110", "10001", "00001", "00010", "00100", "00000", "00100"},
  239. '!': {"00100", "00100", "00100", "00100", "00100", "00000", "00100"},
  240. '(': {"00010", "00100", "01000", "01000", "01000", "00100", "00010"},
  241. ')': {"01000", "00100", "00010", "00010", "00010", "00100", "01000"},
  242. '《': {"00010", "00100", "01000", "10000", "01000", "00100", "00010"},
  243. '》': {"01000", "00100", "00010", "00001", "00010", "00100", "01000"},
  244. }
  245. // ------------------------------------------------------------------------
  246. // 点阵数据模型
  247. // ------------------------------------------------------------------------
  248. // ledGlyph 单个字符的点阵(on[row][col]=true 表示点亮)
  249. type ledGlyph struct {
  250. w, h int
  251. on [][]bool
  252. }
  253. // ledChar 字符 + 其点阵
  254. type ledChar struct {
  255. ch rune
  256. glyph ledGlyph
  257. }
  258. // scaleGlyph5x7 把 5x7 字模放大 scale 倍(每点变 scale×scale 块)
  259. func scaleGlyph5x7(rows [7]string, scale int) ledGlyph {
  260. w := 5 * scale
  261. h := 7 * scale
  262. on := make([][]bool, h)
  263. for y := 0; y < h; y++ {
  264. on[y] = make([]bool, w)
  265. for x := 0; x < w; x++ {
  266. on[y][x] = rows[y/scale][x/scale] == '1'
  267. }
  268. }
  269. return ledGlyph{w: w, h: h, on: on}
  270. }
  271. // charToGlyph 把单个字符渲染为点阵:优先内置 5x7 字模(放大 2 倍=10x14),
  272. // 汉字/其他用 GDI 渲染到内存位图后按 24x32 网格采样。
  273. // hdc 用于汉字采样(窗口线程持有)。
  274. func charToGlyph(hdc uintptr, ch rune) ledGlyph {
  275. upper := ch
  276. if ch >= 'a' && ch <= 'z' {
  277. upper = ch - ('a' - 'A')
  278. }
  279. if rows, ok := ledFont5x7[ch]; ok {
  280. return scaleGlyph5x7(rows, 2)
  281. }
  282. if rows, ok := ledFont5x7[upper]; ok {
  283. return scaleGlyph5x7(rows, 2)
  284. }
  285. // 汉字/其他:GDI 渲染 24x32,按网格采样
  286. return renderHanziGlyph(hdc, ch)
  287. }
  288. // renderHanziGlyph 用 GDI 把汉字画到 24x32 内存位图,按 alpha 通道采样为点阵。
  289. // 用 CreateDIBSection 得到像素缓冲,TextOutW 绘制后读 alpha 位。
  290. func renderHanziGlyph(hdc uintptr, ch rune) ledGlyph {
  291. const gw, gh = 24, 32
  292. // 32 位自顶向下 DIB
  293. bmi := ledBitmapInfo{}
  294. bmi.BmiHeader.BiSize = uint32(unsafe.Sizeof(ledBitmapInfoHeader{}))
  295. bmi.BmiHeader.BiWidth = gw
  296. bmi.BmiHeader.BiHeight = -gh
  297. bmi.BmiHeader.BiPlanes = 1
  298. bmi.BmiHeader.BiBitCount = 32
  299. bmi.BmiHeader.BiCompression = 0 // BI_RGB
  300. var bits unsafe.Pointer
  301. hbmp, _, _ := ledCreateDIBSection.Call(
  302. 0,
  303. uintptr(unsafe.Pointer(&bmi)),
  304. 0, // DIB_RGB_COLORS
  305. uintptr(unsafe.Pointer(&bits)),
  306. 0, 0,
  307. )
  308. if hbmp == 0 || bits == nil {
  309. return emptyGlyph(gw, gh)
  310. }
  311. defer procDeleteObject.Call(hbmp)
  312. // 内存 DC + 选入位图
  313. memDC, _, _ := procCreateCompatibleDC.Call(hdc)
  314. if memDC == 0 {
  315. return emptyGlyph(gw, gh)
  316. }
  317. defer procDeleteDC.Call(memDC)
  318. oldBmp, _, _ := procSelectObject.Call(memDC, hbmp)
  319. // 白色前景,黑色背景(黑=透明,白=点亮)
  320. procSetBkMode.Call(memDC, 1) // TRANSPARENT
  321. ledSetBkColor.Call(memDC, 0x00000000)
  322. procSetTextColor.Call(memDC, 0x00FFFFFF)
  323. // 清黑:PatBlt BLACKNESS(0x00000042),确保底为纯黑(GDI 读亮度判定)
  324. procPatBlt.Call(memDC, 0, 0, uintptr(gw), uintptr(gh), 0x00000042)
  325. // 创建字体:高约 gh*0.9 像素,微软雅黑
  326. facePtr, _ := syscall.UTF16PtrFromString("Microsoft YaHei")
  327. fontH := int32(gh) * 9 / 10 // gh*0.9,用整数避免常量折叠
  328. hFont, _, _ := procCreateFontW.Call(
  329. uintptr(fontH), 0, 0, 0, 400, 0, 0, 0,
  330. 1 /* DEFAULT_CHARSET */, 0, 0, 4 /* ANTIALIASED */, 0,
  331. uintptr(unsafe.Pointer(facePtr)),
  332. )
  333. if hFont != 0 {
  334. procSelectObject.Call(memDC, hFont)
  335. } else {
  336. hFont, _, _ = procGetStockObject.Call(17) // DEFAULT_GUI_FONT
  337. }
  338. // 绘制字符(居中):TextOutW 用当前字体,位置在网格中心
  339. chStr := string(ch)
  340. chPtr, _ := syscall.UTF16PtrFromString(chStr)
  341. procTextOutW.Call(memDC, 2, 3, uintptr(unsafe.Pointer(chPtr)), uintptr(len(chStr)))
  342. // 采样 RGB 亮度(GDI TextOutW 不写 32 位 DIB 的 alpha 通道,故按亮度判定点亮)。
  343. // 白字黑底:字=0xFFFFFF(亮),底=0x000000(暗)。
  344. px := uintptr(bits)
  345. on := make([][]bool, gh)
  346. for y := 0; y < gh; y++ {
  347. on[y] = make([]bool, gw)
  348. row := px + uintptr(y*gw*4)
  349. for x := 0; x < gw; x++ {
  350. b := *(*byte)(unsafe.Pointer(row + uintptr(x*4) + 0))
  351. g := *(*byte)(unsafe.Pointer(row + uintptr(x*4) + 1))
  352. r := *(*byte)(unsafe.Pointer(row + uintptr(x*4) + 2))
  353. // 亮度 > 100(0-255 单通道)视为笔画点亮
  354. on[y][x] = (int(r)+int(g)+int(b))/3 > 100
  355. }
  356. }
  357. // 恢复并清理
  358. if memDC != 0 {
  359. procSelectObject.Call(memDC, oldBmp)
  360. }
  361. if hFont != 0 && hFont != uintptr(17) {
  362. procDeleteObject.Call(hFont)
  363. }
  364. return ledGlyph{w: gw, h: gh, on: on}
  365. }
  366. // emptyGlyph 返回全灭的 w×h 点阵(采样失败时的占位)
  367. func emptyGlyph(w, h int) ledGlyph {
  368. on := make([][]bool, h)
  369. for y := range on {
  370. on[y] = make([]bool, w)
  371. }
  372. return ledGlyph{w: w, h: h, on: on}
  373. }
  374. // textToLedSequence 把整串文字拆成字符点阵序列
  375. func textToLedSequence(hdc uintptr, text string) []ledChar {
  376. seq := make([]ledChar, 0, len([]rune(text)))
  377. for _, ch := range text {
  378. seq = append(seq, ledChar{ch: ch, glyph: charToGlyph(hdc, ch)})
  379. }
  380. return seq
  381. }
  382. // ledSequenceMaxH 返回序列中最大的点阵高(行数)
  383. func ledSequenceMaxH(seq []ledChar) int {
  384. maxH := 0
  385. for _, c := range seq {
  386. if c.glyph.h > maxH {
  387. maxH = c.glyph.h
  388. }
  389. }
  390. return maxH
  391. }
  392. // ledTotalPxWidth 计算整行文字的像素宽(含字符间与末尾间隙)
  393. func ledTotalPxWidth(seq []ledChar, dot int) int {
  394. cell := dot + ledCharGap
  395. w := 0
  396. for _, c := range seq {
  397. w += c.glyph.w*cell + ledGap*dot
  398. }
  399. return w
  400. }
  401. // ledParseColor 把 #RRGGBB / #RGB 转 COLORREF(0x00BBGGRR),失败回退红。
  402. func ledParseColor(s string) uintptr {
  403. s = strings.TrimSpace(s)
  404. if len(s) == 7 && s[0] == '#' {
  405. var r, g, b uint32
  406. if _, err := fmt.Sscanf(s, "#%02x%02x%02x", &r, &g, &b); err == nil {
  407. return uintptr(r | g<<8 | b<<16)
  408. }
  409. }
  410. if len(s) == 4 && s[0] == '#' {
  411. var r, g, b uint32
  412. if _, err := fmt.Sscanf(s, "#%1x%1x%1x", &r, &g, &b); err == nil {
  413. return uintptr(r*17 | g*17<<8 | b*17<<16)
  414. }
  415. }
  416. return 0x00004D4D // 默认红 #ff4d4d
  417. }
  418. // ------------------------------------------------------------------------
  419. // LED 窗口运行时状态(单实例)
  420. // ------------------------------------------------------------------------
  421. // ledWindowState LED 窗口的运行时状态(单实例,互斥保护)
  422. type ledWindowState struct {
  423. mu sync.Mutex
  424. started bool // 窗口线程是否已启动(幂等,仅启动一次)
  425. hwnd uintptr // 窗口句柄
  426. active bool // 窗口是否已创建并显示
  427. closed bool // 是否已请求关闭(窗口线程退出)
  428. text string // 要显示的文字
  429. color uintptr // LED 点阵颜色(COLORREF)
  430. speed int // 滚动速度(px/s)
  431. seq []ledChar // 点阵序列(文字解析结果)
  432. maxH int // 序列最大高(行数)
  433. offset int // 当前水平偏移(px,向左滚为负累加)
  434. period int // 循环周期(px)
  435. prevTs int64 // 上次绘制时间戳(ms,用于 dt 计算)
  436. // 交互状态
  437. hover bool // 鼠标是否悬停在窗口上(显示边框+按钮)
  438. zoom float64 // 缩放倍率(滚轮调整,影响点距)
  439. dragging bool // 是否正在拖动窗口
  440. closeHot bool // 鼠标是否悬停关闭按钮
  441. zoomHot bool // 鼠标是否悬停缩放按钮
  442. lastMouse point // 最近鼠标位置(窗口客户区坐标)
  443. closeRect rect // 关闭按钮区域(客户区坐标)
  444. zoomRect rect // 缩放按钮区域(客户区坐标)
  445. tracking bool // 是否已注册 TrackMouseEvent
  446. wndProc uintptr // 窗口过程回调地址(防 GC)
  447. // UpdateLayeredWindow 像素级透明所需的 DIB 资源(窗口线程创建/持有)
  448. memDC uintptr // 内存 DC(DIB 被选中)
  449. dibBmp uintptr // 32 位 DIB section 的 HBITMAP
  450. dibPtr unsafe.Pointer // DIB 像素内存指针(BGRA 每像素 4 字节)
  451. dibW int // DIB 宽
  452. dibH int // DIB 高
  453. classSeq int // 窗口类名序号(保证类名唯一,避免重复注册同名类)
  454. }
  455. var ledWinState = &ledWindowState{}
  456. // OpenLEDWindow 打开(或更新)LED 点阵滚动窗口并显示指定文字。
  457. // text 为空时不更新内容(保留上次文字),但窗口未开则忽略。
  458. // color 非空时保存为点阵颜色并持久化到设置。
  459. // speed > 0 时更新滚动速度(px/s)。
  460. // 返回是否成功打开(供前端提示)。
  461. func (s *SettingsService) OpenLEDWindow(text string, color string, speed int) bool {
  462. if color != "" {
  463. ledSaveColor(color)
  464. }
  465. if speed > 0 {
  466. ledWinState.mu.Lock()
  467. ledWinState.speed = speed
  468. ledWinState.mu.Unlock()
  469. }
  470. if text == "" {
  471. // 空文字:窗口已开则保留,否则不创建
  472. ledWinState.mu.Lock()
  473. open := ledWinState.active
  474. ledWinState.mu.Unlock()
  475. return open
  476. }
  477. // 更新文字与颜色
  478. ledWinState.mu.Lock()
  479. ledWinState.text = text
  480. if color != "" {
  481. ledWinState.color = ledParseColor(color)
  482. } else if ledWinState.color == 0 {
  483. ledWinState.color = ledParseColor(ledLoadColor())
  484. }
  485. if ledWinState.speed <= 0 {
  486. ledWinState.speed = 60
  487. }
  488. ledWinState.seq = nil // 强制重新解析(窗口线程重建序列)
  489. started := ledWinState.started
  490. ledWinState.started = true
  491. open := ledWinState.active
  492. ledWinState.mu.Unlock()
  493. if !started {
  494. // 首次:启动窗口线程(建窗、消息循环)
  495. go ledWindowLoop()
  496. return true
  497. }
  498. // 已启动:唤醒窗口线程立即刷新
  499. if open {
  500. ledPostRefresh()
  501. }
  502. return true
  503. }
  504. // CloseLEDWindow 关闭 LED 滚动窗口(窗口已关则忽略)。
  505. func (s *SettingsService) CloseLEDWindow() {
  506. ledWinState.mu.Lock()
  507. hwnd := ledWinState.hwnd
  508. ledWinState.closed = true
  509. ledWinState.mu.Unlock()
  510. if hwnd != 0 {
  511. procPostMessage.Call(hwnd, wmClose, 0, 0)
  512. }
  513. }
  514. // SetLEDTransparent 保留空实现:原生分层窗口透明由 SetLayeredWindowAttributes
  515. // 在建窗时设置,无需前端补设。兼容前端 bindings 调用(不再报错)。
  516. func (s *SettingsService) SetLEDTransparent() {
  517. // 原生窗口透明已由 LWA_COLORKEY 在建窗时实现,此处无操作。
  518. }
  519. // GetLEDText 返回当前要显示的 LED 文字。
  520. func (s *SettingsService) GetLEDText() string {
  521. ledWinState.mu.Lock()
  522. defer ledWinState.mu.Unlock()
  523. return ledWinState.text
  524. }
  525. // GetLEDColor 返回当前 LED 点阵颜色字符串(默认 #ff4d4d)。
  526. func (s *SettingsService) GetLEDColor() string {
  527. ledWinState.mu.Lock()
  528. c := ledWinState.color
  529. ledWinState.mu.Unlock()
  530. if c == 0 {
  531. c = ledParseColor(ledLoadColor())
  532. }
  533. return ledFormatColor(c)
  534. }
  535. // GetLEDSpeed 返回当前 LED 滚动速度(px/s),<=0 用默认 60。
  536. func (s *SettingsService) GetLEDSpeed() int {
  537. ledWinState.mu.Lock()
  538. defer ledWinState.mu.Unlock()
  539. if ledWinState.speed <= 0 {
  540. ledWinState.speed = 60
  541. }
  542. return ledWinState.speed
  543. }
  544. // ledSaveColor 持久化 LED 颜色到设置(颜色非空时)。
  545. func ledSaveColor(color string) {
  546. if color == "" {
  547. return
  548. }
  549. settings := loadSettings()
  550. settings.LEDColor = color
  551. _ = saveSettings(settings)
  552. }
  553. // ledLoadColor 从设置读取已保存的 LED 颜色。
  554. func ledLoadColor() string {
  555. c := loadSettings().LEDColor
  556. if c == "" {
  557. c = "#ff4d4d"
  558. }
  559. return c
  560. }
  561. // ledFormatColor 把 COLORREF 转回 #RRGGBB 字符串。
  562. func ledFormatColor(c uintptr) string {
  563. r := (c >> 0) & 0xFF
  564. g := (c >> 8) & 0xFF
  565. b := (c >> 16) & 0xFF
  566. return fmt.Sprintf("#%02x%02x%02x", r, g, b)
  567. }
  568. // ledPostRefresh 向窗口线程投递自定义刷新消息(窗口已建时)。
  569. func ledPostRefresh() {
  570. ledWinState.mu.Lock()
  571. hwnd := ledWinState.hwnd
  572. ledWinState.mu.Unlock()
  573. if hwnd != 0 {
  574. procPostMessage.Call(hwnd, wmRefresh, 0, 0)
  575. }
  576. }
  577. // ------------------------------------------------------------------------
  578. // 窗口创建 / 消息循环
  579. // ------------------------------------------------------------------------
  580. // ledWindowLoop 窗口线程:注册窗口类、建窗、显示、跑消息循环。
  581. // 所有窗口操作必须在同一线程(GUI 线程),Win32 要求。
  582. func ledWindowLoop() {
  583. runtime.LockOSThread()
  584. defer runtime.UnlockOSThread()
  585. hinst, _, _ := procGetModuleHandleW.Call(0)
  586. // 注册窗口类:类名唯一(序号递增),避免重复注册同名类失败
  587. ledWinState.mu.Lock()
  588. ledWinState.classSeq++
  589. clsName := fmt.Sprintf("LedScrollClass_%d", ledWinState.classSeq)
  590. ledWinState.mu.Unlock()
  591. className, _ := syscall.UTF16PtrFromString(clsName)
  592. wc := wndclassex{}
  593. wc.CbSize = uint32(unsafe.Sizeof(wndclassex{}))
  594. wc.Style = 0
  595. wc.LpfnWndProc = ledWinState.wndProcAddr()
  596. wc.HInstance = hinst
  597. wc.HCursor = 0
  598. wc.HbrBackground = 0 // 无背景(WM_ERASEBKGND 自己填充黑,避免闪烁)
  599. wc.LpszClassName = className
  600. atom, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
  601. if atom == 0 {
  602. ledWinState.mu.Lock()
  603. ledWinState.started = false
  604. ledWinState.mu.Unlock()
  605. return
  606. }
  607. // 创建窗口:分层 + 置顶 + 工具窗
  608. // 注意:不加 WS_EX_NOACTIVATE——某些 Win11 版本下 NOACTIVATE 与 LAYERED 组合
  609. // 会导致窗口不渲染(不可见)。避免激活改用 ShowWindow(SW_SHOWNOACTIVATE)。
  610. exStyle := uintptr(wndExLayered | wndExTool | wndExTopMost)
  611. style := uintptr(0x80000000) // WS_POPUP(无边框、无标题、无最小化)
  612. hwnd, _, _ := procCreateWindowEx.Call(
  613. exStyle,
  614. uintptr(unsafe.Pointer(className)),
  615. 0, // 无标题
  616. style,
  617. 0, 0, // 位置:初始 0,0,随后居中
  618. ledWinWidth, ledWinHeight,
  619. 0, 0, hinst, 0,
  620. )
  621. if hwnd == 0 {
  622. ledWinState.mu.Lock()
  623. ledWinState.started = false
  624. ledWinState.mu.Unlock()
  625. return
  626. }
  627. ledWinState.mu.Lock()
  628. ledWinState.hwnd = hwnd
  629. ledWinState.active = true
  630. ledWinState.closed = false
  631. ledWinState.mu.Unlock()
  632. // 像素级透明:创建 32 位 DIB section,用 UpdateLayeredWindow 逐像素提交。
  633. // 背景像素 alpha=0(全透明),点阵像素 alpha=255(不透明)。
  634. // 注意:不再用 SetLayeredWindowAttributes(LWA_COLORKEY) —— 本环境调用虽返回
  635. // 成功但实际不生效(窗口显示为不透明白色矩形,自检已证实)。
  636. if !ledEnsureDIB(ledWinWidth, ledWinHeight) {
  637. procSetLayeredWindowAttr.Call(hwnd, 0, 200, lwaAlpha) // 兜底
  638. }
  639. // 居中显示(不激活,避免抢焦点)
  640. ledCenterWindow(hwnd, ledWinWidth, ledWinHeight)
  641. procShowWindow.Call(hwnd, 4) // SW_SHOWNOACTIVATE
  642. // 置顶 + 不激活
  643. procSetWindowPos.Call(hwnd, hwndTopMost, 0, 0, 0, 0,
  644. swpNoActivate|swpShowWindow|0x0001 /* SWP_NOMOVE */|0x0002 /* SWP_NOSIZE */)
  645. // 滚动定时器(约 16ms)
  646. procSetTimer.Call(hwnd, timerScroll, 16, 0)
  647. // 初始化按钮命中区域(否则 closeRect/zoomRect 全 0,按钮点击失效)
  648. ledInitButtonRects()
  649. // 首次建窗后立即解析点阵序列(否则 seq=nil → 只画透明背景)
  650. ledWinState.mu.Lock()
  651. if ledWinState.zoom <= 0 {
  652. ledWinState.zoom = 1.0 // 缩放倍率默认 1.0(新建窗口重置)
  653. }
  654. ledWinState.hover = false
  655. ledWinState.closeHot = false
  656. ledWinState.zoomHot = false
  657. ledWinState.tracking = false
  658. ledWinState.mu.Unlock()
  659. ledRebuildSequence()
  660. // 建窗后按实际内容调整窗口尺寸(否则固定 720×260,长文字或缩放后可能裁剪)
  661. ledResizeWindow(hwnd)
  662. ledRenderFrame(hwnd) // 首帧提交
  663. // 消息循环
  664. var msg msg
  665. for {
  666. ret, _, _ := procGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
  667. if int32(ret) == 0 || int32(ret) == -1 {
  668. break
  669. }
  670. procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
  671. procDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
  672. }
  673. // 退出清理
  674. procKillTimer.Call(hwnd, timerScroll)
  675. ledReleaseDIB() // 释放 DIB section / memDC
  676. ledWinState.mu.Lock()
  677. ledWinState.hwnd = 0
  678. ledWinState.active = false
  679. ledWinState.started = false
  680. ledWinState.seq = nil
  681. ledWinState.offset = 0
  682. ledWinState.mu.Unlock()
  683. }
  684. // wndProcAddr 返回 LED 窗口过程地址(保持 Go 函数引用,防 GC)。
  685. // 必须在窗口线程内、RegisterClassEx 之前惰性初始化——与 mousealert_windows.go
  686. // 的 wndProcAddr() 一致。不能在全局 init() 里 NewCallback(绑定时机错误,会导致
  687. // RegisterClassEx 拒绝注册)。
  688. func (st *ledWindowState) wndProcAddr() uintptr {
  689. if st.wndProc != 0 {
  690. return st.wndProc
  691. }
  692. st.wndProc = syscall.NewCallback(ledWndProc)
  693. return st.wndProc
  694. }
  695. // ledCenterWindow 把窗口移到屏幕水平居中、垂直约 1/4 处。
  696. func ledCenterWindow(hwnd uintptr, w, h int) {
  697. sw, _, _ := ledGetSystemMetrics.Call(0) // SM_CXSCREEN
  698. sh, _, _ := ledGetSystemMetrics.Call(1) // SM_CYSCREEN
  699. x := (int32(sw) - int32(w)) / 2
  700. y := (int32(sh) - int32(h)) / 4
  701. // SWP_NOSIZE(0x0001)|SWP_NOZORDER(0x0004)|SWP_NOACTIVATE(0x0010):只移动,不改大小/层级/激活
  702. // 必须含 SWP_NOSIZE,否则 cx=cy=0 会把窗口尺寸设成 0×0(客户区为 0,画面空白)
  703. procSetWindowPos.Call(hwnd, 0, uintptr(x), uintptr(y), 0, 0, 0x0001|0x0004|0x0010)
  704. }
  705. // ledInitButtonRects 初始化关闭/缩放按钮的命中区域(客户区坐标)。
  706. // 使用当前窗口实际尺寸(ledResizeWindow 缩放后调用,按钮随窗口动态移动)。
  707. func ledInitButtonRects() {
  708. ledWinState.mu.Lock()
  709. w := ledWinState.dibW
  710. if w <= 0 {
  711. w = ledWinWidth
  712. }
  713. ledWinState.mu.Unlock()
  714. closeCx := w - ledBorderW - ledBtnMargin - ledBtnSize/2
  715. closeCy := ledBtnCloseTop + ledBtnSize/2
  716. zoomCx := ledBorderW + ledBtnMargin + ledBtnSize/2
  717. zoomCy := ledBtnZoomTop + ledBtnSize/2
  718. r := ledBtnSize / 2
  719. ledWinState.mu.Lock()
  720. ledWinState.closeRect = rect{
  721. Left: int32(closeCx - r), Top: int32(closeCy - r),
  722. Right: int32(closeCx + r), Bottom: int32(closeCy + r),
  723. }
  724. ledWinState.zoomRect = rect{
  725. Left: int32(zoomCx - r), Top: int32(zoomCy - r),
  726. Right: int32(zoomCx + r), Bottom: int32(zoomCy + r),
  727. }
  728. ledWinState.mu.Unlock()
  729. }
  730. // ledTrackLeave 注册 TrackMouseEvent,鼠标离开窗口时收到 WM_MOUSELEAVE。
  731. func ledTrackLeave(hwnd uintptr) {
  732. ledWinState.mu.Lock()
  733. if ledWinState.tracking {
  734. ledWinState.mu.Unlock()
  735. return
  736. }
  737. ledWinState.tracking = true
  738. ledWinState.mu.Unlock()
  739. tme := trackMouseEvent{
  740. CbSize: uint32(unsafe.Sizeof(trackMouseEvent{})),
  741. DwFlags: tmeLeave,
  742. HwndTrack: hwnd,
  743. DwHoverTime: 0,
  744. }
  745. procTrackMouseEvent.Call(uintptr(unsafe.Pointer(&tme)))
  746. }
  747. // ------------------------------------------------------------------------
  748. // 窗口过程
  749. // ------------------------------------------------------------------------
  750. // ledWndProc 窗口过程。wParam/lParam 按消息解释。
  751. // 返回 0 表示已处理,非 0 由 DefWindowProc 语义。
  752. func ledWndProc(hwnd uintptr, msg uint32, wParam uintptr, lParam uintptr) uintptr {
  753. switch msg {
  754. case wmPaint:
  755. // 分层窗口用 UpdateLayeredWindow 提交,无需 WM_PAINT GDI 绘制;
  756. // 但窗口仍可能收到 WM_PAINT(如系统要求重绘),直接渲染一帧即可。
  757. ledRenderFrame(hwnd)
  758. return 0
  759. case wmTimer:
  760. if uintptr(wParam) == timerScroll {
  761. ledAdvanceScroll()
  762. ledRenderFrame(hwnd) // 每帧直接提交到屏幕
  763. }
  764. return 0
  765. case wmRefresh:
  766. // 文字已更新:重新解析点阵 + 重置偏移 + 窗口尺寸随内容调整 + 渲染一帧
  767. ledRebuildSequence()
  768. ledResizeWindow(hwnd)
  769. ledRenderFrame(hwnd)
  770. return 0
  771. case wmNcHitTest:
  772. // 返回 HTCLIENT(默认),不用 HTCAPTION —— 对 WS_EX_LAYERED+WS_EX_NOACTIVATE
  773. // 窗口,HTCAPTION 会让系统进入"标题栏移动"状态,阻断 WM_PAINT 派发(画面空白)。
  774. // 拖动改由 WM_LBUTTONDOWN 触发 SendMessage(WM_NCLBUTTONDOWN, HTCAPTION)。
  775. r2, _, _ := procDefWindowProc.Call(hwnd, uintptr(msg), wParam, lParam)
  776. return r2
  777. case wmLButtonDown:
  778. // 判断是否点中关闭/缩放按钮;否则整窗拖动(标题栏移动)
  779. x := int16(uint16(lParam & 0xFFFF))
  780. y := int16(uint16((lParam >> 16) & 0xFFFF))
  781. ledWinState.mu.Lock()
  782. closeRect := ledWinState.closeRect
  783. zoomRect := ledWinState.zoomRect
  784. ledWinState.mu.Unlock()
  785. cx := int32(x)
  786. cy := int32(y)
  787. inClose := cx >= closeRect.Left && cx <= closeRect.Right && cy >= closeRect.Top && cy <= closeRect.Bottom
  788. inZoom := cx >= zoomRect.Left && cx <= zoomRect.Right && cy >= zoomRect.Top && cy <= zoomRect.Bottom
  789. if inClose {
  790. // 关闭按钮:销毁窗口
  791. ledWinState.mu.Lock()
  792. ledWinState.hover = false
  793. ledWinState.closeHot = false
  794. ledWinState.zoomHot = false
  795. ledWinState.mu.Unlock()
  796. procDestroyWindow.Call(hwnd)
  797. return 0
  798. }
  799. if inZoom {
  800. // 缩放按钮:切换缩放(1.0 → 1.5 → 0.6 → 1.0),并重建周期
  801. ledWinState.mu.Lock()
  802. switch ledWinState.zoom {
  803. case 1.0:
  804. ledWinState.zoom = 1.5
  805. case 1.5:
  806. ledWinState.zoom = 0.6
  807. default:
  808. ledWinState.zoom = 1.0
  809. }
  810. ledWinState.mu.Unlock()
  811. ledRebuildSequence()
  812. ledResizeWindow(hwnd)
  813. ledRenderFrame(hwnd)
  814. return 0
  815. }
  816. // 普通区域:整窗拖动(标题栏移动,不抢焦点)
  817. ledSendMessageW.Call(hwnd, wmNcLButtonDown, htCaption, lParam)
  818. return 0
  819. case wmLButtonUp:
  820. return 0
  821. case wmMouseMove:
  822. // 记录鼠标位置,注册离开检测,更新按钮悬停,重绘(悬停边框)
  823. x := int16(uint16(lParam & 0xFFFF))
  824. y := int16(uint16((lParam >> 16) & 0xFFFF))
  825. ledWinState.mu.Lock()
  826. ledWinState.lastMouse = point{X: int32(x), Y: int32(y)}
  827. wasHover := ledWinState.hover
  828. ledWinState.hover = true
  829. closeRect := ledWinState.closeRect
  830. zoomRect := ledWinState.zoomRect
  831. ledWinState.mu.Unlock()
  832. if !wasHover {
  833. // 首次悬停:注册 TrackMouseEvent(离开时收 WM_MOUSELEAVE)
  834. ledTrackLeave(hwnd)
  835. }
  836. // 更新按钮悬停状态
  837. cx := int32(x)
  838. cy := int32(y)
  839. inClose := cx >= closeRect.Left && cx <= closeRect.Right && cy >= closeRect.Top && cy <= closeRect.Bottom
  840. inZoom := cx >= zoomRect.Left && cx <= zoomRect.Right && cy >= zoomRect.Top && cy <= zoomRect.Bottom
  841. changed := false
  842. ledWinState.mu.Lock()
  843. if ledWinState.closeHot != inClose {
  844. ledWinState.closeHot = inClose
  845. changed = true
  846. }
  847. if ledWinState.zoomHot != inZoom {
  848. ledWinState.zoomHot = inZoom
  849. changed = true
  850. }
  851. ledWinState.mu.Unlock()
  852. // 首次悬停或按钮悬停变化时重绘(显示/更新边框与按钮)
  853. if changed || !wasHover {
  854. ledRenderFrame(hwnd)
  855. }
  856. return 0
  857. case wmMouseLeave:
  858. // 鼠标离开:隐藏边框与按钮
  859. ledWinState.mu.Lock()
  860. ledWinState.hover = false
  861. ledWinState.closeHot = false
  862. ledWinState.zoomHot = false
  863. ledWinState.tracking = false
  864. ledWinState.mu.Unlock()
  865. ledRenderFrame(hwnd)
  866. return 0
  867. case wmMouseWheel:
  868. // 滚轮缩放:调整点距倍率
  869. delta := int16(uint16(wParam >> 16)) // HIWORD = 滚轮增量
  870. ledWinState.mu.Lock()
  871. z := ledWinState.zoom
  872. if z <= 0 {
  873. z = 1
  874. }
  875. if delta > 0 {
  876. z += ledZoomStep
  877. } else {
  878. z -= ledZoomStep
  879. }
  880. if z < ledZoomMin {
  881. z = ledZoomMin
  882. }
  883. if z > ledZoomMax {
  884. z = ledZoomMax
  885. }
  886. ledWinState.zoom = z
  887. ledWinState.mu.Unlock()
  888. // 缩放改变点距 → 重建周期(平铺宽度变化)
  889. ledRebuildSequence()
  890. // 窗口尺寸随点距/内容动态变化,保证放大后点阵不被裁剪
  891. ledResizeWindow(hwnd)
  892. ledRenderFrame(hwnd)
  893. return 0
  894. case wmClose:
  895. procDestroyWindow.Call(hwnd)
  896. return 0
  897. case wmDestroy:
  898. ledPostQuitMessage.Call(0)
  899. return 0
  900. }
  901. r, _, _ := procDefWindowProc.Call(hwnd, uintptr(msg), wParam, lParam)
  902. return r
  903. }
  904. // ledRebuildSequence 重建点阵序列(窗口线程内调用)。
  905. func ledRebuildSequence() {
  906. ledWinState.mu.Lock()
  907. text := ledWinState.text
  908. color := ledWinState.color
  909. if color == 0 {
  910. color = ledParseColor(ledLoadColor())
  911. }
  912. hwnd := ledWinState.hwnd
  913. ledWinState.mu.Unlock()
  914. // 用窗口 DC 采样汉字
  915. hdc, _, _ := ledGetDC.Call(hwnd)
  916. // 文字末尾追加两个空格:无限滚动循环时形成断句分隔,
  917. // 避免前一句末字与后一句首字粘连、观看者无法断句。
  918. text = text + " "
  919. seq := textToLedSequence(hdc, text)
  920. if hdc != 0 {
  921. ledReleaseDC.Call(hwnd, hdc)
  922. }
  923. maxH := ledSequenceMaxH(seq)
  924. // 无缝无限滚动:把序列平铺两份(seq + seq),offset 在 [-单份宽, 0) 内回绕,
  925. // 这样任何时候窗口内都有连续文字,循环处无缝衔接。
  926. tiled := make([]ledChar, 0, len(seq)*2)
  927. tiled = append(tiled, seq...)
  928. tiled = append(tiled, seq...)
  929. // 单份序列的总像素宽(平铺周期)
  930. tileW := ledTotalPxWidth(seq, ledWinState.dot())
  931. ledWinState.mu.Lock()
  932. ledWinState.seq = tiled
  933. ledWinState.maxH = maxH
  934. // 循环周期 = 单份序列宽(无限平铺回绕);周期需 > 0 才回绕
  935. if tileW > 0 {
  936. ledWinState.period = tileW
  937. } else {
  938. ledWinState.period = ledWinWidth
  939. }
  940. ledWinState.offset = 0
  941. ledWinState.prevTs = 0
  942. ledWinState.mu.Unlock()
  943. }
  944. // dot 计算当前点距(点阵单元像素大小)。
  945. // 基础点距固定为 ledBaseDot,再乘以滚轮缩放倍率。
  946. // 窗口高度不再参与计算——尺寸随 dot 动态变化(见 ledResizeWindow),
  947. // 避免"高度变化→点距变化→窗口再变化"的耦合振荡。
  948. func (st *ledWindowState) dot() int {
  949. d := ledBaseDot
  950. z := st.zoom
  951. if z <= 0 {
  952. z = 1
  953. }
  954. return int(float64(d) * z)
  955. }
  956. // ledTargetSize 根据当前点距、点阵高与单份序列宽计算目标窗口尺寸。
  957. // 高度容纳点阵 + 上下边距(边框+按钮);宽度容纳一个完整循环周期。
  958. func ledTargetSize(dot, maxH, tileW int) (int, int) {
  959. if maxH <= 0 {
  960. maxH = 32
  961. }
  962. lineH := maxH * dot
  963. h := lineH + 2*ledPadV
  964. // 至少容纳悬停按钮与边框
  965. minH := ledBtnCloseTop + ledBtnSize + ledBtnMargin + ledBorderW
  966. if h < minH {
  967. h = minH
  968. }
  969. w := tileW + 2*ledPadH
  970. // 上限:不超屏幕宽的 9/10(点阵再长也受限,避免窗口占满屏幕)
  971. if sw, _, _ := ledGetSystemMetrics.Call(0); int32(sw) > 0 {
  972. maxW := int(int32(sw)) * 9 / 10
  973. if w > maxW {
  974. w = maxW
  975. }
  976. }
  977. // 下限:保持基础宽,避免纯字母小字串窗口过窄
  978. if w < ledWinWidth {
  979. w = ledWinWidth
  980. }
  981. return w, h
  982. }
  983. // ledResizeWindow 按当前内容(点距/点阵高/序列宽)调整窗口尺寸并重建 DIB。
  984. // 必须在窗口线程内调用(缩放、文字更新后)。
  985. func ledResizeWindow(hwnd uintptr) {
  986. ledWinState.mu.Lock()
  987. dot := ledWinState.dot()
  988. maxH := ledWinState.maxH
  989. tile := ledWinState.period
  990. ledWinState.mu.Unlock()
  991. if tile <= 0 {
  992. tile = ledWinWidth
  993. }
  994. w, h := ledTargetSize(dot, maxH, tile)
  995. // 调整窗口尺寸:保持左上角位置(SWP_NOMOVE),不改层级/激活
  996. procSetWindowPos.Call(hwnd, 0, 0, 0, uintptr(w), uintptr(h),
  997. swpNoActivate|0x0001 /* SWP_NOMOVE */|0x0004 /* SWP_NOZORDER */)
  998. // 重建 DIB 匹配新尺寸(释放旧 + 创建新)
  999. if !ledEnsureDIB(w, h) {
  1000. return
  1001. }
  1002. // 按钮命中区域随窗口宽高动态更新
  1003. ledInitButtonRects()
  1004. }
  1005. // ledAdvanceScroll 按经过时间推进滚动偏移(窗口线程内调用)。
  1006. func ledAdvanceScroll() {
  1007. ledWinState.mu.Lock()
  1008. now := time.Now().UnixMilli()
  1009. if ledWinState.prevTs == 0 {
  1010. ledWinState.prevTs = now
  1011. ledWinState.mu.Unlock()
  1012. return
  1013. }
  1014. dt := now - ledWinState.prevTs
  1015. ledWinState.prevTs = now
  1016. // 鼠标悬停时停止滚动:可放大/缩小/关闭查看点阵细节
  1017. if ledWinState.hover {
  1018. ledWinState.mu.Unlock()
  1019. return
  1020. }
  1021. spd := ledWinState.speed
  1022. if spd <= 0 {
  1023. spd = 60
  1024. }
  1025. // 向左滚:offset 递减
  1026. ledWinState.offset -= int(float32(spd) * float32(dt) / 1000.0)
  1027. // 无限回绕:把 offset 归一到 (-period, 0],保证平铺无缝衔接。
  1028. // 注意 seq 已平铺两份,period=单份宽,窗口宽 < period+单份宽,任意时刻有连续文字。
  1029. if ledWinState.period > 0 {
  1030. p := ledWinState.period
  1031. // 归一:offset 在 (-p, 0]
  1032. for ledWinState.offset <= -p {
  1033. ledWinState.offset += p
  1034. }
  1035. for ledWinState.offset > 0 {
  1036. ledWinState.offset -= p
  1037. }
  1038. }
  1039. ledWinState.mu.Unlock()
  1040. }
  1041. // ledEnsureDIB 创建 32 位自顶向下 DIB section 并选入内存 DC。
  1042. // 必须窗口线程内调用。成功后 dibPtr 指向 BGRA 像素缓冲。
  1043. // w/h 为动态尺寸(随缩放变化);非法尺寸回退默认 720×260。
  1044. func ledEnsureDIB(w, h int) bool {
  1045. if w <= 0 || h <= 0 {
  1046. w, h = ledWinWidth, ledWinHeight
  1047. }
  1048. // 先释放旧 DIB(幂等:首次调用 memDC=0,无操作)
  1049. ledReleaseDIB()
  1050. bmi := ledBitmapInfo{}
  1051. bmi.BmiHeader.BiSize = uint32(unsafe.Sizeof(ledBitmapInfoHeader{}))
  1052. bmi.BmiHeader.BiWidth = int32(w)
  1053. bmi.BmiHeader.BiHeight = -int32(h) // 自顶向下
  1054. bmi.BmiHeader.BiPlanes = 1
  1055. bmi.BmiHeader.BiBitCount = 32
  1056. bmi.BmiHeader.BiCompression = 0 // BI_RGB
  1057. var bits unsafe.Pointer
  1058. hbmp, _, _ := ledCreateDIBSection.Call(
  1059. 0,
  1060. uintptr(unsafe.Pointer(&bmi)),
  1061. 0, // DIB_RGB_COLORS
  1062. uintptr(unsafe.Pointer(&bits)),
  1063. 0, 0,
  1064. )
  1065. if hbmp == 0 || bits == nil {
  1066. return false
  1067. }
  1068. memDC, _, _ := procCreateCompatibleDC.Call(0)
  1069. if memDC == 0 {
  1070. procDeleteObject.Call(hbmp)
  1071. return false
  1072. }
  1073. procSelectObject.Call(memDC, hbmp)
  1074. ledWinState.mu.Lock()
  1075. ledWinState.memDC = memDC
  1076. ledWinState.dibBmp = hbmp
  1077. ledWinState.dibPtr = bits
  1078. ledWinState.dibW = w
  1079. ledWinState.dibH = h
  1080. ledWinState.mu.Unlock()
  1081. return true
  1082. }
  1083. // ledReleaseDIB 释放 DIB section 与内存 DC(窗口线程内调用)。
  1084. func ledReleaseDIB() {
  1085. ledWinState.mu.Lock()
  1086. memDC := ledWinState.memDC
  1087. hbmp := ledWinState.dibBmp
  1088. ledWinState.memDC = 0
  1089. ledWinState.dibBmp = 0
  1090. ledWinState.dibPtr = nil
  1091. ledWinState.mu.Unlock()
  1092. if memDC != 0 {
  1093. procDeleteDC.Call(memDC)
  1094. }
  1095. if hbmp != 0 {
  1096. procDeleteObject.Call(hbmp)
  1097. }
  1098. }
  1099. // ledRenderFrame 渲染一帧到 DIB 并调 UpdateLayeredWindow 提交到屏幕。
  1100. // 背景像素 alpha=0(全透明),点阵像素 alpha=255(不透明,LED 颜色)。
  1101. // 悬停时绘制半透明边框与按钮(关闭 / 缩放指示)。
  1102. func ledRenderFrame(hwnd uintptr) {
  1103. ledWinState.mu.Lock()
  1104. memDC := ledWinState.memDC
  1105. base := ledWinState.dibPtr
  1106. w := ledWinState.dibW
  1107. h := ledWinState.dibH
  1108. seq := ledWinState.seq
  1109. offset := ledWinState.offset
  1110. maxH := ledWinState.maxH
  1111. color := ledWinState.color
  1112. dot := ledWinState.dot()
  1113. hover := ledWinState.hover
  1114. closeHot := ledWinState.closeHot
  1115. zoomHot := ledWinState.zoomHot
  1116. zoom := ledWinState.zoom
  1117. ledWinState.mu.Unlock()
  1118. if memDC == 0 || base == nil || w <= 0 || h <= 0 {
  1119. return
  1120. }
  1121. if color == 0 {
  1122. color = ledParseColor(ledLoadColor())
  1123. }
  1124. // 1) 清空为全透明(B=0,G=0,R=0,A=0)
  1125. p := uintptr(base)
  1126. for i := 0; i < w*h*4; i += 4 {
  1127. *(*byte)(unsafe.Pointer(p + uintptr(i))) = 0
  1128. *(*byte)(unsafe.Pointer(p + uintptr(i) + 1)) = 0
  1129. *(*byte)(unsafe.Pointer(p + uintptr(i) + 2)) = 0
  1130. *(*byte)(unsafe.Pointer(p + uintptr(i) + 3)) = 0
  1131. }
  1132. // 2) 画点阵:圆点(抗锯齿)+ 无限平铺
  1133. if len(seq) > 0 && maxH > 0 {
  1134. cell := dot + ledCharGap
  1135. lineH := maxH * dot
  1136. startY := (h - lineH) / 2
  1137. if startY < 0 {
  1138. startY = 0
  1139. }
  1140. // 颜色拆分(COLORREF 0x00BBGGRR)
  1141. cr := byte(color & 0xFF)
  1142. cg := byte((color >> 8) & 0xFF)
  1143. cb := byte((color >> 16) & 0xFF)
  1144. // 平铺周期(单份序列宽),避免重复绘制超出窗口的字符
  1145. tile := ledWinState.period
  1146. if tile <= 0 {
  1147. tile = w
  1148. }
  1149. penX := offset
  1150. for _, c := range seq {
  1151. glyph := c.glyph
  1152. // 垂直居中:每个字符按其自身高度在整行高度内居中,
  1153. // 使字母(低)与汉字(高)的视觉中线对齐(避免字母顶对齐偏上)
  1154. charTop := startY + (maxH-glyph.h)/2*dot
  1155. for gy := 0; gy < glyph.h; gy++ {
  1156. for gx := 0; gx < glyph.w; gx++ {
  1157. if !glyph.on[gy][gx] {
  1158. continue
  1159. }
  1160. px := penX + gx*cell
  1161. py := charTop + gy*dot
  1162. // 只画可能与窗口相交的圆点(含一个平铺周期的余量)
  1163. if px > w || px+dot < 0 || py+dot < 0 || py > h {
  1164. continue
  1165. }
  1166. ledDrawDot(base, w, h, px, py, dot, cr, cg, cb)
  1167. }
  1168. }
  1169. penX += glyph.w*cell + ledGap*dot
  1170. // 提前结束:当前字符已远超窗口右缘 + 平铺余量
  1171. if penX > w+tile+cell {
  1172. break
  1173. }
  1174. }
  1175. }
  1176. // 3) 悬停:绘制边框 + 按钮
  1177. if hover {
  1178. ledDrawHoverUI(base, w, h, closeHot, zoomHot, zoom)
  1179. }
  1180. // 4) UpdateLayeredWindow 提交
  1181. screenDC, _, _ := ledGetDC.Call(0)
  1182. if screenDC == 0 {
  1183. return
  1184. }
  1185. defer ledReleaseDC.Call(0, screenDC)
  1186. var srcPt point
  1187. var srcSize size
  1188. srcSize.Cx = int32(w)
  1189. srcSize.Cy = int32(h)
  1190. var dstPt point
  1191. var wr rect
  1192. procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&wr)))
  1193. dstPt.X = wr.Left
  1194. dstPt.Y = wr.Top
  1195. blend := blendfunction{
  1196. BlendOp: 0,
  1197. BlendFlags: 0,
  1198. SourceConstantAlpha: 255,
  1199. AlphaFormat: acSrcAlpha,
  1200. }
  1201. ret, _, _ := procUpdateLayeredWindow.Call(
  1202. hwnd,
  1203. screenDC,
  1204. uintptr(unsafe.Pointer(&dstPt)),
  1205. uintptr(unsafe.Pointer(&srcSize)),
  1206. memDC,
  1207. uintptr(unsafe.Pointer(&srcPt)),
  1208. 0,
  1209. uintptr(unsafe.Pointer(&blend)),
  1210. uintptr(ulwAlpha),
  1211. )
  1212. _ = ret
  1213. }
  1214. // ledDrawDot 画一个圆点(抗锯齿):在 dot×dot 单元内以距离平方判断圆形。
  1215. // 用 2x 超采样 + 距离软边实现平滑边缘。
  1216. func ledDrawDot(base unsafe.Pointer, w, h, x0, y0, dot int, cr, cg, cb byte) {
  1217. if dot <= 0 {
  1218. return
  1219. }
  1220. // 圆心与半径(留 1px 间隙让点阵更透气)
  1221. center := float64(dot) / 2.0
  1222. radius := float64(dot)/2.0 - 0.5
  1223. if radius < 1 {
  1224. radius = 1
  1225. }
  1226. r2 := radius * radius
  1227. // 内半径(全亮),外半径(全透明),之间线性过渡 → 抗锯齿
  1228. innerR2 := (radius - 1.0) * (radius - 1.0)
  1229. if innerR2 < 0 {
  1230. innerR2 = 0
  1231. }
  1232. // 颜色(预乘 alpha 不需要,AC_SRC_ALPHA 下 alpha 即不透明度)
  1233. for dy := 0; dy < dot; dy++ {
  1234. for dx := 0; dx < dot; dx++ {
  1235. xx := x0 + dx
  1236. yy := y0 + dy
  1237. if xx < 0 || xx >= w || yy < 0 || yy >= h {
  1238. continue
  1239. }
  1240. // 到圆心的距离平方
  1241. dxc := float64(dx) + 0.5 - center
  1242. dyc := float64(dy) + 0.5 - center
  1243. dist2 := dxc*dxc + dyc*dyc
  1244. var alpha float64
  1245. if dist2 <= innerR2 {
  1246. alpha = 1.0
  1247. } else if dist2 >= r2 {
  1248. alpha = 0.0
  1249. } else {
  1250. // 线性软边
  1251. alpha = (r2 - dist2) / (r2 - innerR2)
  1252. if alpha < 0 {
  1253. alpha = 0
  1254. }
  1255. if alpha > 1 {
  1256. alpha = 1
  1257. }
  1258. }
  1259. if alpha <= 0 {
  1260. continue
  1261. }
  1262. a := byte(alpha * 255)
  1263. off := (yy*w + xx) * 4
  1264. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = cb
  1265. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = cg
  1266. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = cr
  1267. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = a
  1268. }
  1269. }
  1270. }
  1271. // ledDrawHoverUI 绘制悬停边框 + 关闭按钮 + 缩放指示按钮。
  1272. func ledDrawHoverUI(base unsafe.Pointer, w, h int, closeHot, zoomHot bool, zoom float64) {
  1273. // 边框:浅蓝半透明(alpha 130),宽 ledBorderW
  1274. border := byte(130)
  1275. // 边框颜色(浅灰蓝)
  1276. br, bg, bb := byte(160), byte(200), byte(255)
  1277. for y := 0; y < h; y++ {
  1278. for x := 0; x < w; x++ {
  1279. on := x < ledBorderW || x >= w-ledBorderW || y < ledBorderW || y >= h-ledBorderW
  1280. if !on {
  1281. continue
  1282. }
  1283. off := (y*w + x) * 4
  1284. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = bb
  1285. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = bg
  1286. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = br
  1287. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = border
  1288. }
  1289. }
  1290. // 关闭按钮(右上角圆形 ×)
  1291. closeCx := w - ledBorderW - ledBtnMargin - ledBtnSize/2
  1292. closeCy := ledBtnCloseTop + ledBtnSize/2
  1293. drawCircleButton(base, w, h, closeCx, closeCy, ledBtnSize/2,
  1294. 200, 80, 80, // 红
  1295. closeHot, func(px, py int) {
  1296. // 画 ×
  1297. r := ledBtnSize / 4
  1298. if abs(px-closeCx)+abs(py-closeCy) <= r {
  1299. off := (py*w + px) * 4
  1300. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = 255
  1301. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = 255
  1302. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = 255
  1303. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = 255
  1304. }
  1305. })
  1306. // 缩放按钮(左上角圆形,显示 +/-)
  1307. zoomCx := ledBorderW + ledBtnMargin + ledBtnSize/2
  1308. zoomCy := ledBtnZoomTop + ledBtnSize/2
  1309. drawCircleButton(base, w, h, zoomCx, zoomCy, ledBtnSize/2,
  1310. 80, 160, 200, // 蓝
  1311. zoomHot, func(px, py int) {
  1312. // 画放大镜:中心圆 + 手柄
  1313. off := (py*w + px) * 4
  1314. // 手柄(对角线)
  1315. if abs(px-zoomCx)+abs(py-zoomCy) > ledBtnSize/5 && abs(px-zoomCx)+abs(py-zoomCy) <= ledBtnSize/3 {
  1316. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = 255
  1317. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = 255
  1318. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = 255
  1319. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = 255
  1320. }
  1321. // 中间横杠
  1322. if abs(py-zoomCy) <= 1 && abs(px-zoomCx) <= ledBtnSize/6 {
  1323. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = 255
  1324. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = 255
  1325. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = 255
  1326. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = 255
  1327. }
  1328. })
  1329. // 缩放倍率文本(右上角下方,简单显示 zoom)
  1330. _ = zoom
  1331. }
  1332. // drawCircleButton 画一个圆形按钮(实心 + 悬停高亮),中心 (cx,cy) 半径 r。
  1333. // content 在按钮内部像素上回调,用于画图标。
  1334. func drawCircleButton(base unsafe.Pointer, w, h, cx, cy, r int, cr, cg, cb byte, hot bool, content func(px, py int)) {
  1335. r2 := r * r
  1336. for dy := -r; dy <= r; dy++ {
  1337. for dx := -r; dx <= r; dx++ {
  1338. xx := cx + dx
  1339. yy := cy + dy
  1340. if xx < 0 || xx >= w || yy < 0 || yy >= h {
  1341. continue
  1342. }
  1343. if dx*dx+dy*dy > r2 {
  1344. continue
  1345. }
  1346. off := (yy*w + xx) * 4
  1347. if hot {
  1348. // 悬停:亮一点
  1349. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = cb
  1350. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = cg
  1351. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = cr
  1352. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = 230
  1353. } else {
  1354. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 0)) = cb
  1355. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 1)) = cg
  1356. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 2)) = cr
  1357. *(*byte)(unsafe.Pointer(uintptr(base) + uintptr(off) + 3)) = 180
  1358. }
  1359. if content != nil {
  1360. content(xx, yy)
  1361. }
  1362. }
  1363. }
  1364. }
  1365. // abs 返回绝对值。
  1366. func abs(x int) int {
  1367. if x < 0 {
  1368. return -x
  1369. }
  1370. return x
  1371. }
  1372. // 调试日志已移除:不再写 led_transparent.log,避免残留记录文件。