| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510 |
- //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,避免残留记录文件。
|