package win32 // 取景框:屏幕上的一个红框,常驻显示当前框选区域。 // // 三个关键点: // 1. 用 WS_EX_LAYERED 分层窗口自己画,框外像素 alpha=0,既不遮挡下方内容, // 也不需要 Fyne 的透明窗口支持(OpenGL 窗口改颜色键并不可靠); // 2. WS_EX_TRANSPARENT + WM_NCHITTEST 返回 HTTRANSPARENT,鼠标事件直接穿透, // 不会挡住底下的点击; // 3. SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE) 让它不出现在截屏和录屏 // 画面里,包括本程序自己抓屏用的 GDI BitBlt(Windows 10 2004+)。 import ( "fmt" "os" "sync" "syscall" "unsafe" "golang.org/x/sys/windows" ) const ( frameWNDClass = "ViewsAgentFrameWnd" wsExLayered = 0x00080000 wsExTransparent = 0x00000020 wsExToolWindow = 0x00000080 wsExNoActivate = 0x08000000 wsPopup = 0x80000000 swShowNoActivate = 4 ulwAlpha = 0x02 acSrcOver = 0x00 acSrcAlpha = 0x01 biRgb = 0x00 dibRgbColors = 0x00 // wdaExcludeFromCapture 让窗口从截屏/录屏里消失(Windows 10 2004+)。 wdaExcludeFromCapture = 0x11 wdaNone = 0x00 wmDestroy = 0x0002 wmEraseBkgnd = 0x0014 wmNCHitTest = 0x0084 wmFrameRedraw = 0x8001 // WM_APP + 1 // htTransparent 让鼠标事件穿透到下层窗口。 htTransparent = ^uintptr(0) ) var ( gdi32 = windows.NewLazySystemDLL("gdi32.dll") procCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC") procCreateDIBSection = gdi32.NewProc("CreateDIBSection") procSelectObject = gdi32.NewProc("SelectObject") procDeleteObject = gdi32.NewProc("DeleteObject") procDeleteDC = gdi32.NewProc("DeleteDC") procGetDC = user32.NewProc("GetDC") procReleaseDC = user32.NewProc("ReleaseDC") procCreateWindowExW = user32.NewProc("CreateWindowExW") procRegisterClassExW = user32.NewProc("RegisterClassExW") procDefWindowProcW = user32.NewProc("DefWindowProcW") procTranslateMessage = user32.NewProc("TranslateMessage") procDispatchMessageW = user32.NewProc("DispatchMessageW") procPostMessageW = user32.NewProc("PostMessageW") procDestroyWindow = user32.NewProc("DestroyWindow") procShowWindow = user32.NewProc("ShowWindow") procUpdateLayered = user32.NewProc("UpdateLayeredWindow") procSetDisplayAffinity = user32.NewProc("SetWindowDisplayAffinity") procGetDisplayAffinity = user32.NewProc("GetWindowDisplayAffinity") procIsWindowVisible = user32.NewProc("IsWindowVisible") ) type bitmapInfoHeader struct { Size uint32 Width int32 Height int32 Planes uint16 BitCount uint16 Compression uint32 SizeImage uint32 XPelsPerMeter int32 YPelsPerMeter int32 ClrUsed uint32 ClrImportant uint32 } type bitmapInfo struct { Header bitmapInfoHeader Colors [1]uint32 } type blendFunction struct { BlendOp byte BlendFlags byte SourceConstantAlpha byte AlphaFormat byte } type winPoint struct { X, Y int32 } type winSize struct { Cx, Cy int32 } type wndClassEx struct { Size uint32 Style uint32 WndProc uintptr ClsExtra int32 WndExtra int32 Instance windows.Handle Icon windows.Handle Cursor windows.Handle Background windows.Handle MenuName *uint16 ClassName *uint16 IconSmall windows.Handle } // Frame 是屏幕上的取景框。内部自带一条消息线程, // 窗口消息必须有人泵,否则窗口不会响应。 type Frame struct { mu sync.Mutex hwnd syscall.Handle region Rect shown bool memDC syscall.Handle dib syscall.Handle oldBmp syscall.Handle bits unsafe.Pointer w, h int buf []uint32 threadID uintptr lastULW uintptr lastErr uintptr ready chan error done chan struct{} } // frames 记录窗口句柄到 Frame 的映射,窗口过程里要用。 var ( framesMu sync.Mutex frames = map[syscall.Handle]*Frame{} ) // NewFrame 创建一个取景框。此时还没有区域,调用 SetRegion 才会显示。 func NewFrame() (*Frame, error) { f := &Frame{ ready: make(chan error, 1), done: make(chan struct{}), } go f.loop() if err := <-f.ready; err != nil { return nil, err } return f, nil } // loop 在独立线程上建窗口并跑消息循环。 func (f *Frame) loop() { className, err := windows.UTF16PtrFromString(frameWNDClass) if err != nil { f.ready <- err close(f.done) return } wc := wndClassEx{ Size: uint32(unsafe.Sizeof(wndClassEx{})), Style: 0x0008, // CS_DBLCLKS 无所谓,主要是别带 CS_HREDRAW/CS_VREDRAW WndProc: windows.NewCallback(frameWndProc), ClassName: className, } if atom, _, _ := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))); atom == 0 { // 1410 = ERROR_CLASS_ALREADY_EXISTS,重复注册不算错。 if code := lastErrorCode(); code != 1410 { err = fmt.Errorf("RegisterClassEx 失败: %w", syscall.Errno(code)) f.ready <- err close(f.done) return } } style := uintptr(wsExLayered | wsExTransparent | wsExToolWindow | wsExNoActivate) hwnd, _, _ := procCreateWindowExW.Call( style, uintptr(unsafe.Pointer(className)), 0, uintptr(wsPopup), 0, 0, 1, 1, 0, 0, 0, 0, ) if hwnd == 0 { f.ready <- fmt.Errorf("CreateWindowEx 失败: %w", syscall.Errno(lastErrorCode())) close(f.done) return } f.mu.Lock() f.hwnd = syscall.Handle(hwnd) f.threadID, _, _ = procGetCurrentThreadID.Call() f.mu.Unlock() framesMu.Lock() frames[syscall.Handle(hwnd)] = f framesMu.Unlock() // 关键:让窗口从截屏/录屏里排除出去。 // 开发自检时可用 VIEWSAGENT_FRAME_NO_EXCLUDE=1 临时关掉,方便截图核对。 if os.Getenv("VIEWSAGENT_FRAME_NO_EXCLUDE") == "" { procSetDisplayAffinity.Call(hwnd, wdaExcludeFromCapture) } // 准备离屏绘图用的 DC。 hdcScreen, _, _ := procGetDC.Call(0) if hdcScreen != 0 { memDC, _, _ := procCreateCompatibleDC.Call(hdcScreen) f.mu.Lock() f.memDC = syscall.Handle(memDC) f.mu.Unlock() procReleaseDC.Call(0, hdcScreen) } f.ready <- nil var msg msg for { r, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0) if int32(r) <= 0 { break } procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg))) procDispatchMessageW.Call(uintptr(unsafe.Pointer(&msg))) } close(f.done) } // SetRegion 把取景框移到指定区域并重画。区域无效时隐藏。 func (f *Frame) SetRegion(r Rect) { f.mu.Lock() f.region = r f.shown = r.Width() > 0 && r.Height() > 0 hwnd := f.hwnd f.mu.Unlock() if hwnd == 0 { return } if r.Width() <= 0 || r.Height() <= 0 { procShowWindow.Call(uintptr(hwnd), 0) // SW_HIDE return } procShowWindow.Call(uintptr(hwnd), swShowNoActivate) procPostMessageW.Call(uintptr(hwnd), wmFrameRedraw, 0, 0) } // Hide 隐藏取景框。 func (f *Frame) Hide() { f.mu.Lock() f.shown = false hwnd := f.hwnd f.mu.Unlock() if hwnd != 0 { procShowWindow.Call(uintptr(hwnd), 0) } } // SetCaptureExclusion 开关某个窗口的"不出现在截屏/录屏里"。 // 关掉之后窗口会被录进去,只用于自检和调试。 func SetCaptureExclusion(hwnd syscall.Handle, on bool) { if hwnd == 0 { return } affinity := uintptr(wdaExcludeFromCapture) if !on { affinity = wdaNone } procSetDisplayAffinity.Call(uintptr(hwnd), affinity) } // SetCaptureExclusion 开关"不出现在截屏/录屏里"。 // 关掉之后取景框会被录进去,只用于自检和调试。 func (f *Frame) SetCaptureExclusion(on bool) { f.mu.Lock() hwnd := f.hwnd f.mu.Unlock() SetCaptureExclusion(hwnd, on) } // FindFrameWindow 按窗口类找到取景框窗口,给外部工具(如自检脚本)用。 func FindFrameWindow() (syscall.Handle, error) { return FindWindowByClass(frameWNDClass) } // IsWindowVisible 返回窗口是否可见。 func IsWindowVisible(hwnd syscall.Handle) bool { r, _, _ := procIsWindowVisible.Call(uintptr(hwnd)) return r != 0 } // DisplayAffinity 返回窗口的截屏亲和性设置。 func DisplayAffinity(hwnd syscall.Handle) uint32 { var v uint32 r, _, _ := procGetDisplayAffinity.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&v))) if r == 0 { return 0 } return v } // Debug 返回取景框的内部状态,用于自检排障。 func (f *Frame) Debug() string { f.mu.Lock() defer f.mu.Unlock() red := 0 if f.bits != nil && len(f.buf) > 0 { for _, v := range unsafe.Slice((*uint32)(f.bits), len(f.buf)) { if v == 0xFFFF0000 { red++ } } } return fmt.Sprintf("w=%d h=%d memDC=%v dib=%v bits=%v buf=%d 红像素=%d 区域=%+v ULW=%v err=%d", f.w, f.h, f.memDC != 0, f.dib != 0, f.bits != nil, len(f.buf), red, f.region, f.lastULW, f.lastErr) } // FrameDebug 返回取景框的内部状态,用于自检排障。 func FrameDebug(hwnd syscall.Handle) string { framesMu.Lock() f := frames[hwnd] framesMu.Unlock() if f == nil { return "找不到取景框实例" } f.mu.Lock() defer f.mu.Unlock() red := 0 if f.bits != nil && len(f.buf) > 0 { for _, v := range unsafe.Slice((*uint32)(f.bits), len(f.buf)) { if v == 0xFFFF0000 { red++ } } } return fmt.Sprintf("w=%d h=%d memDC=%v dib=%v bits=%v buf=%d 红像素=%d", f.w, f.h, f.memDC != 0, f.dib != 0, f.bits != nil, len(f.buf), red) } // Close 销毁取景框。 func (f *Frame) Close() { f.mu.Lock() hwnd := f.hwnd threadID := f.threadID f.hwnd = 0 memDC := f.memDC dib := f.dib oldBmp := f.oldBmp f.memDC, f.dib, f.oldBmp = 0, 0, 0 f.mu.Unlock() if hwnd != 0 { framesMu.Lock() delete(frames, syscall.Handle(hwnd)) framesMu.Unlock() procDestroyWindow.Call(uintptr(hwnd)) // 让消息循环退出,否则这条线程会一直挂着。 if threadID != 0 { procPostThreadMessageW.Call(threadID, wmQuit, 0, 0) } } if memDC != 0 { if oldBmp != 0 { procSelectObject.Call(uintptr(memDC), uintptr(oldBmp)) } if dib != 0 { procDeleteObject.Call(uintptr(dib)) } procDeleteDC.Call(uintptr(memDC)) } } // redraw 重新绘制取景框(运行在窗口自己的线程上)。 func (f *Frame) redraw() { f.mu.Lock() r := f.region memDC := f.memDC hwnd := f.hwnd f.mu.Unlock() w, h := r.Width(), r.Height() if w <= 0 || h <= 0 || memDC == 0 || hwnd == 0 { return } f.ensureBitmap(w, h) // 画框:四边实心红条,中间留空(全透明)。 const thickness = 2 red := uint32(0xFFFF0000) // DIB 是 BGRA 顺序,A=255 R=255 G=0 B=0 for i := range f.buf { f.buf[i] = 0 } fillRect(f.buf, w, h, 0, 0, w, thickness, red) fillRect(f.buf, w, h, 0, h-thickness, w, h, red) fillRect(f.buf, w, h, 0, 0, thickness, h, red) fillRect(f.buf, w, h, w-thickness, 0, w, h, red) copy(unsafe.Slice((*uint32)(f.bits), len(f.buf)), f.buf) hdcScreen, _, _ := procGetDC.Call(0) if hdcScreen == 0 { return } defer procReleaseDC.Call(0, hdcScreen) blend := blendFunction{BlendOp: acSrcOver, SourceConstantAlpha: 255, AlphaFormat: acSrcAlpha} dst := winPoint{X: int32(r.Left), Y: int32(r.Top)} size := winSize{Cx: int32(w), Cy: int32(h)} src := winPoint{} ok, _, _ := procUpdateLayered.Call( uintptr(hwnd), hdcScreen, uintptr(unsafe.Pointer(&dst)), uintptr(unsafe.Pointer(&size)), uintptr(memDC), uintptr(unsafe.Pointer(&src)), 0, uintptr(unsafe.Pointer(&blend)), ulwAlpha, ) f.mu.Lock() f.lastULW = ok f.lastErr = lastErrorCode() f.mu.Unlock() // 保持在最上层,但不抢焦点。 procSetWindowPos.Call(uintptr(hwnd), hwndTopMost, 0, 0, 0, 0, swpNoMove|swpNoSize|swpNoActivate) } // ensureBitmap 保证离屏位图尺寸与区域一致。 func (f *Frame) ensureBitmap(w, h int) { f.mu.Lock() defer f.mu.Unlock() if f.dib != 0 && f.w == w && f.h == h { return } if f.dib != 0 { procSelectObject.Call(uintptr(f.memDC), uintptr(f.oldBmp)) procDeleteObject.Call(uintptr(f.dib)) f.dib = 0 f.bits = nil } bmi := bitmapInfo{} bmi.Header.Size = uint32(unsafe.Sizeof(bitmapInfoHeader{})) bmi.Header.Width = int32(w) bmi.Header.Height = -int32(h) // 负数表示自上而下 bmi.Header.Planes = 1 bmi.Header.BitCount = 32 bmi.Header.Compression = biRgb var bits unsafe.Pointer dib, _, _ := procCreateDIBSection.Call( uintptr(f.memDC), uintptr(unsafe.Pointer(&bmi)), dibRgbColors, uintptr(unsafe.Pointer(&bits)), 0, 0, ) if dib == 0 { return } old, _, _ := procSelectObject.Call(uintptr(f.memDC), dib) f.dib = syscall.Handle(dib) f.oldBmp = syscall.Handle(old) f.bits = bits f.w, f.h = w, h f.buf = make([]uint32, w*h) } // fillRect 在像素缓冲里填一块矩形。 func fillRect(buf []uint32, w, h, x0, y0, x1, y1 int, color uint32) { if x0 < 0 { x0 = 0 } if y0 < 0 { y0 = 0 } if x1 > w { x1 = w } if y1 > h { y1 = h } for y := y0; y < y1; y++ { row := y * w for x := x0; x < x1; x++ { buf[row+x] = color } } } // frameWndProc 是取景框的窗口过程。 func frameWndProc(hwnd uintptr, msg uint32, wparam, lparam uintptr) uintptr { switch msg { case wmNCHitTest: // 鼠标穿透:让下层窗口收到点击。 return htTransparent case wmEraseBkgnd: // 不擦背景:整个窗口由分层位图定义。 return 1 case wmFrameRedraw: framesMu.Lock() f := frames[syscall.Handle(hwnd)] framesMu.Unlock() if f != nil { f.redraw() } return 0 case wmDestroy: return 0 } r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wparam, lparam) return r } // lastErrorCode 取最近一次 Win32 调用的错误码。 func lastErrorCode() uintptr { if err, ok := windows.GetLastError().(syscall.Errno); ok { return uintptr(err) } return 0 }