Files
deskview/internal/win32/frame.go
T
2026-10-28 17:39:33 +08:00

1139 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}