package win32 // 取景框:屏幕上的一个红框,常驻显示当前框选区域。 // // 四个关键点: // 1. 用 WS_EX_LAYERED 分层窗口自己画,框内像素 alpha=0,既不遮挡下方内容, // 也不需要 Fyne 的透明窗口支持(OpenGL 窗口改颜色键并不可靠); // 2. 整个窗口带 WS_EX_TRANSPARENT,鼠标事件一律穿透给下层程序, // 被监视的软件照常能点、能拖,不会被取景框挡住; // 3. 要跟红框交互(拖边框改大小、右上角「确定 / 取消」按钮), // 靠一个全局低级鼠标钩子(WH_MOUSE_LL)在事件到达下层程序之前拿到它们, // 只在自己的小区域里拦截,其余地方一律放行; // 4. SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE) 让它不出现在截屏和录屏 // 画面里,包括本程序自己抓屏用的 GDI BitBlt(Windows 10 2004+)。 import ( "fmt" "os" "runtime" "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) // 低级鼠标钩子。 whMouseLL = 14 hcAction = 0 wmMouseMove = 0x0200 wmLButtonDown = 0x0201 wmLButtonUp = 0x0202 // 标准尺寸光标。 idcArrow = 32512 idcSizeNS = 32645 idcSizeWE = 32644 idcSizeNESW = 32643 idcSizeNWSE = 32642 ) // 取景框上的交互区。 const ( zoneNone = iota zoneLeft zoneTop zoneRight zoneBottom zoneTopLeft zoneTopRight zoneBottomLeft zoneBottomRight zoneButtons ) // 按钮编号。 const ( btnNone = iota btnOK btnCancel ) // 取景框的尺寸常量(物理像素)。 const ( frameGrip = 6 // 边框的拖拽响应宽度 frameMinSide = 16 // 选区允许的最小边长 frameBtnSize = 16 // 按钮边长 frameBtnGap = 4 // 两个按钮之间的间距 frameBtnRight = 8 // 按钮条距右边 frameBtnTop = 8 // 按钮条距上边 ) var ( gdi32 = windows.NewLazySystemDLL("gdi32.dll") km32 = windows.NewLazySystemDLL("kernel32.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") procSetTimer = user32.NewProc("SetTimer") procKillTimer = user32.NewProc("KillTimer") procSetCapture = user32.NewProc("SetCapture") procReleaseCapture = user32.NewProc("ReleaseCapture") procSetCursor = user32.NewProc("SetCursor") procLoadCursorW = user32.NewProc("LoadCursorW") procSetWindowsHookExW = user32.NewProc("SetWindowsHookExW") procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx") procCallNextHookEx = user32.NewProc("CallNextHookEx") procGetModuleHandleW = km32.NewProc("GetModuleHandleW") ) 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 } // msllHookStruct 对应 Win32 的 MSLLHOOKSTRUCT。 type msllHookStruct struct { Pt point MouseData uint32 Flags uint32 Time uint32 DwExtraInfo uintptr } // Frame 是屏幕上的取景框。内部自带一条消息线程, // 窗口消息必须有人泵,否则窗口不会响应。 type Frame struct { mu sync.Mutex hwnd syscall.Handle region Rect // 已生效的区域 draft Rect // 正在编辑的区域(拖拽时变化) shown bool hidden bool // 用户点了取消:红框不再自动显示 hoverZone int // 当前悬停的交互区 hoverBtn bool // 按钮条是否处于悬停状态 pressed int // 按下的按钮 dragging bool dragZone int dragStartX, dragStartY int dragBase Rect onApply func(Rect) onCancel func() hook syscall.Handle 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{} ) // active 是当前生效的取景框,鼠标钩子里要用。 var ( activeMu sync.Mutex active *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() { // 必须把 goroutine 钉在同一个系统线程上:窗口的消息队列是按线程分的, // 一旦被调度到别的线程,GetMessage 就泵不到这个窗口的队列, // 红框从此不再重绘,看起来就是"卡死"。 runtime.LockOSThread() defer runtime.UnlockOSThread() 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 } } // WS_EX_TRANSPARENT:整个窗口对鼠标透明,事件直接穿透给下层程序。 // 交互交给全局鼠标钩子处理,这样绝不会挡住被监视的软件。 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) } // 装全局低级鼠标钩子:取景框是穿透窗口,收不到鼠标消息, // 只能在事件到达下层程序之前截住。 activeMu.Lock() active = f activeMu.Unlock() hmod, _, _ := procGetModuleHandleW.Call(0) hh, _, _ := procSetWindowsHookExW.Call(whMouseLL, frameHookCallback, hmod, 0) f.mu.Lock() f.hook = syscall.Handle(hh) f.mu.Unlock() 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 // 拖拽过程中不要把正在编辑的区域顶掉。 if !f.dragging { f.draft = r } show := r.Width() > 0 && r.Height() > 0 && !f.hidden f.shown = show hwnd := f.hwnd f.mu.Unlock() if hwnd == 0 { return } if !show { procShowWindow.Call(uintptr(hwnd), 0) // SW_HIDE return } procShowWindow.Call(uintptr(hwnd), swShowNoActivate) procPostMessageW.Call(uintptr(hwnd), wmFrameRedraw, 0, 0) } // Show 重新显示取景框(会清掉"用户已取消"的状态)。 func (f *Frame) Show() { f.mu.Lock() f.hidden = false r := f.region f.mu.Unlock() f.SetRegion(r) } // Hide 隐藏取景框(临时隐藏,不会阻止 SetRegion 再次显示它)。 func (f *Frame) Hide() { f.mu.Lock() f.shown = false hwnd := f.hwnd f.mu.Unlock() if hwnd != 0 { procKillTimer.Call(uintptr(hwnd), 1) procShowWindow.Call(uintptr(hwnd), 0) } } // HideByUser 按用户意图隐藏取景框:之后 SetRegion 也不会把它显示出来。 func (f *Frame) HideByUser() { f.mu.Lock() f.hidden = true f.shown = false f.hoverBtn = false f.pressed = btnNone hwnd := f.hwnd f.mu.Unlock() if hwnd != 0 { procKillTimer.Call(uintptr(hwnd), 1) procShowWindow.Call(uintptr(hwnd), 0) } } // ToggleVisible 切换取景框的显示状态,返回切换后是否可见。 func (f *Frame) ToggleVisible() bool { f.mu.Lock() visible := f.shown f.mu.Unlock() if visible { f.HideByUser() return false } f.Show() return true } // Visible 返回取景框当前是否显示。 func (f *Frame) Visible() bool { f.mu.Lock() defer f.mu.Unlock() return f.shown } // SetCallbacks 设置交互回调:确定时回传当前框体大小,取消时通知上层。 func (f *Frame) SetCallbacks(onApply func(Rect), onCancel func()) { f.mu.Lock() f.onApply = onApply f.onCancel = onCancel f.mu.Unlock() } // 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() { // 先摘掉钩子,别让已经关掉的取景框再拦截鼠标。 activeMu.Lock() if active == f { active = nil } activeMu.Unlock() f.mu.Lock() hwnd := f.hwnd threadID := f.threadID hook := f.hook f.hwnd = 0 f.hook = 0 memDC := f.memDC dib := f.dib oldBmp := f.oldBmp f.memDC, f.dib, f.oldBmp = 0, 0, 0 f.mu.Unlock() if hook != 0 { procUnhookWindowsHookEx.Call(uintptr(hook)) } 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)) } } // ---------- 交互 ---------- // buttonRects 返回「确定」和「取消」两个按钮在框内坐标系下的矩形。 func (f *Frame) buttonRects(w int) (okR, cancelR [4]int) { right := w - frameBtnRight cancelR = [4]int{right - frameBtnSize, frameBtnTop, right, frameBtnTop + frameBtnSize} okR = [4]int{cancelR[0] - frameBtnGap - frameBtnSize, frameBtnTop, cancelR[0] - frameBtnGap, frameBtnTop + frameBtnSize} return } // buttonBar 返回按钮条(悬停区)在框内坐标系下的范围。 func (f *Frame) buttonBar(w int) (x0, y0, x1, y1 int) { okR, cancelR := f.buttonRects(w) x0 = okR[0] - 2 y0 = frameBtnTop - 2 x1 = cancelR[2] + 2 y1 = cancelR[3] + 2 return } // hitTest 判断屏幕坐标落在哪个交互区。 func (f *Frame) hitTest(r Rect, sx, sy int) int { w, h := r.Width(), r.Height() if w <= 0 || h <= 0 { return zoneNone } lx, ly := sx-r.Left, sy-r.Top if lx < 0 || ly < 0 || lx >= w || ly >= h { return zoneNone } // 按钮条优先:它就贴在右上角内侧,和边角拖拽区不重叠。 bx0, by0, bx1, by1 := f.buttonBar(w) if lx >= bx0 && lx < bx1 && ly >= by0 && ly < by1 { return zoneButtons } const grip = frameGrip nearL := lx < grip nearR := lx >= w-grip nearT := ly < grip nearB := ly >= h-grip switch { case nearL && nearT: return zoneTopLeft case nearR && nearT: return zoneTopRight case nearR && nearB: return zoneBottomRight case nearL && nearB: return zoneBottomLeft case nearL: return zoneLeft case nearR: return zoneRight case nearT: return zoneTop case nearB: return zoneBottom } return zoneNone } // buttonAt 判断点在按钮条里的哪个按钮上。 func (f *Frame) buttonAt(r Rect, sx, sy int) int { w := r.Width() okR, cancelR := f.buttonRects(w) lx := sx - r.Left ly := sy - r.Top inOK := lx >= okR[0] && lx < okR[2] && ly >= okR[1] && ly < okR[3] inCancel := lx >= cancelR[0] && lx < cancelR[2] && ly >= cancelR[1] && ly < cancelR[3] switch { case inOK: return btnOK case inCancel: return btnCancel } return btnNone } // frameHookCallback 是全局低级鼠标钩子的入口。 var frameHookCallback = windows.NewCallback(mouseHookTrampoline) // mouseHookTrampoline 处理全局鼠标事件。返回非 0 表示吞掉这次事件。 func mouseHookTrampoline(hhk int32, nCode uint32, wParam uint32, lParam uintptr) uintptr { // 钩子里 panic 会直接崩掉整个程序,这里兜一下。 defer func() { _ = recover() }() if nCode == hcAction { activeMu.Lock() f := active activeMu.Unlock() if f != nil && f.handleHook(wParam, lParam) { return 1 // 吞掉,别让下层程序收到 } } r, _, _ := procCallNextHookEx.Call(uintptr(hhk), uintptr(wParam), lParam) return r } // hookStruct 把钩子回调传来的 lParam 还原成 MSLLHOOKSTRUCT。 // // 直接写 unsafe.Pointer(lParam) 会被 vet 判成 uintptr 误用, // 这里用"取变量地址再按指针读"的等价写法绕开。 func hookStruct(lParam uintptr) *msllHookStruct { p := *(*unsafe.Pointer)(unsafe.Pointer(&lParam)) return (*msllHookStruct)(p) } // handleHook 处理钩子收到的事件。返回 true 表示这次事件被取景框接管。 func (f *Frame) handleHook(wParam uint32, lParam uintptr) bool { hs := hookStruct(lParam) x, y := int(hs.Pt.X), int(hs.Pt.Y) switch wParam { case wmMouseMove: return f.hookMove(x, y) case wmLButtonDown: return f.hookDown(x, y) case wmLButtonUp: return f.hookUp(x, y) } return false } // hookMove 处理鼠标移动:更新悬停状态与光标,拖拽时更新选区。 func (f *Frame) hookMove(x, y int) bool { f.mu.Lock() if !f.shown || f.hidden { f.mu.Unlock() return false } dragging := f.dragging d := f.draft f.mu.Unlock() if dragging { f.applyDrag(x, y) // 拖拽期间吞掉移动事件,下层程序别跟着乱动。 return true } zone := f.hitTest(d, x, y) showBtns := zone == zoneButtons f.mu.Lock() f.hoverZone = zone changed := f.hoverBtn != showBtns f.hoverBtn = showBtns f.mu.Unlock() if changed { f.redraw() } if zone == zoneNone { return false } if zone == zoneButtons { // 按钮条:光标用默认箭头就行,不用拦下层程序的移动事件, // 免得那一小块区域的悬停效果失效。 return false } f.setCursorFor(zone) // 吞掉:下层程序收不到移动事件,我们的缩放光标才不会被它改回去。 return true } // hookDown 处理鼠标按下:按钮或边框按下都接管,其余放行。 func (f *Frame) hookDown(x, y int) bool { f.mu.Lock() if !f.shown || f.hidden { f.mu.Unlock() return false } d := f.draft hoverBtn := f.hoverBtn f.mu.Unlock() switch zone := f.hitTest(d, x, y); zone { case zoneButtons: if !hoverBtn { // 按钮还没显示出来(鼠标刚扫过去),这次点击让给下层程序。 return false } f.mu.Lock() f.pressed = f.buttonAt(d, x, y) f.mu.Unlock() return true case zoneNone: return false default: f.mu.Lock() f.dragging = true f.dragZone = zone f.dragStartX, f.dragStartY = x, y f.dragBase = d f.mu.Unlock() return true } } // hookUp 处理鼠标松开。 func (f *Frame) hookUp(x, y int) bool { f.mu.Lock() dragging := f.dragging pressed := f.pressed f.dragging = false f.pressed = btnNone f.mu.Unlock() if dragging { // 松手后不立刻生效,等用户点「确定」。 f.redraw() return true } switch pressed { case btnOK: f.applyDraft() return true case btnCancel: f.cancel() return true } return false } // applyDrag 按当前鼠标位置算出新的框。 func (f *Frame) applyDrag(sx, sy int) { f.mu.Lock() zone := f.dragZone base := f.dragBase startX, startY := f.dragStartX, f.dragStartY f.mu.Unlock() if zone == zoneNone { return } dx := sx - startX dy := sy - startY r := base vd := VirtualScreen() minSide := frameMinSide clamp := func(v, lo, hi int) int { if v < lo { return lo } if v > hi { return hi } return v } switch zone { case zoneLeft: r.Left = clamp(base.Left+dx, vd.Left, base.Right-minSide) case zoneRight: r.Right = clamp(base.Right+dx, base.Left+minSide, vd.Right) case zoneTop: r.Top = clamp(base.Top+dy, vd.Top, base.Bottom-minSide) case zoneBottom: r.Bottom = clamp(base.Bottom+dy, base.Top+minSide, vd.Bottom) case zoneTopLeft: r.Left = clamp(base.Left+dx, vd.Left, base.Right-minSide) r.Top = clamp(base.Top+dy, vd.Top, base.Bottom-minSide) case zoneTopRight: r.Right = clamp(base.Right+dx, base.Left+minSide, vd.Right) r.Top = clamp(base.Top+dy, vd.Top, base.Bottom-minSide) case zoneBottomLeft: r.Left = clamp(base.Left+dx, vd.Left, base.Right-minSide) r.Bottom = clamp(base.Bottom+dy, base.Top+minSide, vd.Bottom) case zoneBottomRight: r.Right = clamp(base.Right+dx, base.Left+minSide, vd.Right) r.Bottom = clamp(base.Bottom+dy, base.Top+minSide, vd.Bottom) } f.mu.Lock() f.draft = r f.mu.Unlock() f.redraw() } // applyDraft 把当前编辑中的区域交给上层(点「确定」)。 func (f *Frame) applyDraft() { f.mu.Lock() d := f.draft cb := f.onApply f.mu.Unlock() if cb != nil { cb(d) } } // cancel 放弃当前编辑并隐藏红框(点「取消」)。 func (f *Frame) cancel() { f.mu.Lock() f.draft = f.region f.hidden = true f.shown = false f.hoverBtn = false f.pressed = btnNone hwnd := f.hwnd cb := f.onCancel f.mu.Unlock() if hwnd != 0 { procShowWindow.Call(uintptr(hwnd), 0) } if cb != nil { cb() } } // setCursorFor 按交互区设置光标形状。 func (f *Frame) setCursorFor(zone int) { id := uintptr(idcArrow) switch zone { case zoneLeft, zoneRight: id = idcSizeWE case zoneTop, zoneBottom: id = idcSizeNS case zoneTopLeft, zoneBottomRight: id = idcSizeNWSE case zoneTopRight, zoneBottomLeft: id = idcSizeNESW } cur, _, _ := procLoadCursorW.Call(0, id) if cur == 0 { return } procSetCursor.Call(cur) } // ---------- 绘制 ---------- // redraw 重新绘制取景框(运行在窗口自己的线程上)。 func (f *Frame) redraw() { f.mu.Lock() r := f.draft hoverBtn := f.hoverBtn 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) f.drawButtons(w, h, hoverBtn) 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) } // drawButtons 画右上角的「确定 / 取消」。 // // 平时完全不画(像素全透明),鼠标移上去才显示。 // 鼠标事件由全局钩子处理,这里不需要留任何"能点到的底"。 func (f *Frame) drawButtons(w, h int, visible bool) { if !visible { return } okR, cancelR := f.buttonRects(w) bg := uint32(0xFF1E1E1E) fillRect(f.buf, w, h, int(okR[0]), int(okR[1]), int(okR[2]), int(okR[3]), bg) fillRect(f.buf, w, h, int(cancelR[0]), int(cancelR[1]), int(cancelR[2]), int(cancelR[3]), bg) // 图标:绿色对勾 + 红色叉。 drawCheck(f.buf, w, h, okR, uint32(0xFF4CD44C)) drawCross(f.buf, w, h, cancelR, uint32(0xFF5252FF)) } // 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 } } } // drawLine 在像素缓冲里画一条线(Bresenham)。 func drawLine(buf []uint32, w, h, x0, y0, x1, y1 int, c uint32) { dx := absInt(x1 - x0) dy := -absInt(y1 - y0) sx, sy := -1, -1 if x0 < x1 { sx = 1 } if y0 < y1 { sy = 1 } err := dx + dy for { if x0 >= 0 && y0 >= 0 && x0 < w && y0 < h { buf[y0*w+x0] = c } if x0 == x1 && y0 == y1 { return } e2 := 2 * err if e2 >= dy { err += dy x0 += sx } if e2 <= dx { err += dx y0 += sy } } } // drawCheck 在按钮里画一个对勾。 func drawCheck(buf []uint32, w, h int, r [4]int, c uint32) { x0, y0 := r[0], r[1] size := r[2] - r[0] drawLine(buf, w, h, x0+size*2/5, y0+size/2, x0+size*3/5, y0+size*4/5, c) drawLine(buf, w, h, x0+size*3/5, y0+size*4/5, x0+size*4/5, y0+size/5, c) } // drawCross 在按钮里画一个叉。 func drawCross(buf []uint32, w, h int, r [4]int, c uint32) { x0, y0 := r[0], r[1] size := r[2] - r[0] drawLine(buf, w, h, x0+size/4, y0+size/4, x0+size*3/4, y0+size*3/4, c) drawLine(buf, w, h, x0+size*3/4, y0+size/4, x0+size/4, y0+size*3/4, c) } // absInt 返回绝对值。 func absInt(v int) int { if v < 0 { return -v } return v } // frameWndProc 是取景框的窗口过程。 func frameWndProc(hwnd uintptr, msg uint32, wparam, lparam uintptr) uintptr { switch msg { case wmNCHitTest: // 鼠标穿透:让下层窗口收到点击。交互由全局钩子负责。 return htTransparent case wmEraseBkgnd: // 不擦背景:整个窗口由分层位图定义。 return 1 case wmFrameRedraw: if f := frameOf(hwnd); f != nil { f.redraw() } return 0 case wmDestroy: return 0 } r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wparam, lparam) return r } // frameOf 按窗口句柄找到取景框实例。 func frameOf(hwnd uintptr) *Frame { framesMu.Lock() defer framesMu.Unlock() return frames[syscall.Handle(hwnd)] } // lastErrorCode 取最近一次 Win32 调用的错误码。 func lastErrorCode() uintptr { if err, ok := windows.GetLastError().(syscall.Errno); ok { return uintptr(err) } return 0 }