1139 lines
28 KiB
Go
1139 lines
28 KiB
Go
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
|
||
}
|