Files
deskview/internal/win32/frame.go
T
2026-10-28 08:47:09 +08:00

542 lines
13 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 + 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
}