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