优化代码

This commit is contained in:
hero920103 committed 2026-10-28 08:47:09 +08:00
1 parent 3553790475
commit eda7e8bc96
23 files changed
+3113 -342

No files matched your search

+541
View File
@@ -0,0 +1,541 @@
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
}
+118
View File
@@ -0,0 +1,118 @@
//go:build windows
package win32_test
import (
"image"
"image/png"
"os"
"path/filepath"
"testing"
"time"
"viewsagent/internal/capture"
"viewsagent/internal/win32"
)
// TestFrameVisibleButNotCaptured 验证取景框的两个关键性质:
// 1. 它确实显示在屏幕上(截屏能看到红框);
// 2. 打开"排除截屏"后,它不会被抓屏录进去(模型看到的画面里没有红框)。
//
// 这是个需要桌面会话的集成测试,默认跳过:
//
// VIEWSAGENT_UI_TEST=1 go test ./internal/win32/ -run Frame
func TestFrameVisibleButNotCaptured(t *testing.T) {
if os.Getenv("VIEWSAGENT_UI_TEST") == "" {
t.Skip("需要桌面会话,设置 VIEWSAGENT_UI_TEST=1 后运行")
}
f, err := win32.NewFrame()
if err != nil {
t.Skipf("无法创建取景框窗口(可能没有桌面会话): %v", err)
}
defer f.Close()
region := capture.Region{X: 120, Y: 120, W: 420, H: 300}
f.SetRegion(win32.Rect{
Left: region.X, Top: region.Y,
Right: region.X + region.W, Bottom: region.Y + region.H,
})
time.Sleep(400 * time.Millisecond)
// 关闭排除:截屏里应该能看到红框,说明它真的贴在屏幕上。
f.SetCaptureExclusion(false)
time.Sleep(300 * time.Millisecond)
visible := grabRegion(t, region)
saveShot(t, "frame-visible.png", visible)
if n := countRedInBorder(visible, 3); n == 0 {
t.Fatal("取景框没有出现在屏幕上:关掉排除后截屏里也找不到红线")
} else {
t.Logf("边框窄带里有 %d 个红像素,取景框已显示在屏幕上", n)
}
// 打开排除:截屏里不应再有红线。
f.SetCaptureExclusion(true)
time.Sleep(300 * time.Millisecond)
hidden := grabRegion(t, region)
saveShot(t, "frame-excluded.png", hidden)
if n := countRedInBorder(hidden, 3); n != 0 {
t.Fatalf("取景框被录进截屏了,边框窄带里有 %d 个红像素", n)
}
t.Log("取景框已被排除在截屏之外")
}
// grabRegion 抓取一块屏幕区域。
func grabRegion(t *testing.T, r capture.Region) image.Image {
t.Helper()
img, err := (capture.Screen{}).Grab(r)
if err != nil {
t.Fatalf("抓屏失败: %v", err)
}
return img
}
// countRedInBorder 只统计区域四边 band 像素宽的窄带里的红色像素。
// 取景框只画在这一圈上,这样不会把页面里本来就有的红色(图标之类)算进来。
func countRedInBorder(img image.Image, band int) int {
b := img.Bounds()
isRed := func(x, y int) bool {
r, g, bl, _ := img.At(x, y).RGBA()
return r > 0xB000 && g < 0x5000 && bl < 0x5000
}
n := 0
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
nearEdge := x < b.Min.X+band || x >= b.Max.X-band ||
y < b.Min.Y+band || y >= b.Max.Y-band
if nearEdge && isRed(x, y) {
n++
}
}
}
return n
}
// saveShot 把截图写到 VIEWSAGENT_FRAME_SHOT_DIR 指定的目录,方便肉眼核对。
func saveShot(t *testing.T, name string, img image.Image) {
t.Helper()
dir := os.Getenv("VIEWSAGENT_FRAME_SHOT_DIR")
if dir == "" {
return
}
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Logf("创建截图目录失败: %v", err)
return
}
path := filepath.Join(dir, name)
f, err := os.Create(path)
if err != nil {
t.Logf("创建截图文件失败: %v", err)
return
}
defer f.Close()
if err := png.Encode(f, img); err != nil {
t.Logf("写入截图失败: %v", err)
return
}
t.Logf("截图已保存: %s", path)
}
+14
View File
@@ -16,6 +16,7 @@ var (
shcore = windows.NewLazySystemDLL("shcore.dll")
procFindWindowW = user32.NewProc("FindWindowW")
procFindWindowExW = user32.NewProc("FindWindowExW")
procGetWindowRect = user32.NewProc("GetWindowRect")
procSetWindowPos = user32.NewProc("SetWindowPos")
procGetCursorPos = user32.NewProc("GetCursorPos")
@@ -200,6 +201,19 @@ func FindWindowByTitle(title string) (syscall.Handle, error) {
return syscall.Handle(r), nil
}
// FindWindowByClass 按窗口类名查找顶层窗口句柄。
func FindWindowByClass(class string) (syscall.Handle, error) {
p, err := windows.UTF16PtrFromString(class)
if err != nil {
return 0, err
}
r, _, e := procFindWindowExW.Call(0, 0, uintptr(unsafe.Pointer(p)), 0)
if r == 0 {
return 0, fmt.Errorf("未找到类名为 %q 的窗口: %w", class, e)
}
return syscall.Handle(r), nil
}
// WindowRect 返回窗口在屏幕上的物理像素矩形。
func WindowRect(hwnd syscall.Handle) (Rect, error) {
var r nativeRect