first commit

This commit is contained in:
hero920103 committed 2026-10-27 17:58:28 +08:00
commit 3553790475
27 files changed
+3839

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
// Package capture 负责屏幕抓取、显示器信息与坐标换算。
package capture
import (
"fmt"
"image"
"math"
"github.com/kbinani/screenshot"
"viewsagent/internal/win32"
)
// Monitor 是显示器信息(物理像素)。
type Monitor = win32.Monitor
// Region 是以物理像素表示的屏幕区域,坐标基于 Windows 虚拟桌面。
type Region struct {
X int `json:"x"`
Y int `json:"y"`
W int `json:"w"`
H int `json:"h"`
}
// Valid 判断区域是否可用于抓屏。
func (r Region) Valid() bool { return r.W > 0 && r.H > 0 }
// Rect 转换为 image.Rectangle。
func (r Region) Rect() image.Rectangle {
return image.Rect(r.X, r.Y, r.X+r.W, r.Y+r.H)
}
// Inset 向内收缩 d 像素,避免把边缘干扰一起截进去。
func (r Region) Inset(d int) Region {
if d*2 >= r.W || d*2 >= r.H {
return r
}
return Region{X: r.X + d, Y: r.Y + d, W: r.W - 2*d, H: r.H - 2*d}
}
// String 返回 "1024x576@(0,0)" 形式的描述。
func (r Region) String() string {
return fmt.Sprintf("%dx%d@(%d,%d)", r.W, r.H, r.X, r.Y)
}
// Mapping 把 Fyne 逻辑坐标换算成物理像素。
// 由于窗口尺寸由系统缩放决定,比例按实际窗口尺寸反推,避免 125%/150% 缩放下的偏移。
type Mapping struct {
OffsetX float64 // 窗口左上角的物理 X
OffsetY float64 // 窗口左上角的物理 Y
ScaleX float64 // 物理宽 / 逻辑宽
ScaleY float64 // 物理高 / 逻辑高
}
// ToPhysical 把逻辑矩形换算为物理区域。
func (m Mapping) ToPhysical(x, y, w, h float64) Region {
// 用 Round 而不是 +0.5 截断:副屏坐标可能为负,截断会向零取整导致偏移。
return Region{
X: int(math.Round(m.OffsetX + x*m.ScaleX)),
Y: int(math.Round(m.OffsetY + y*m.ScaleY)),
W: int(math.Round(w * m.ScaleX)),
H: int(math.Round(h * m.ScaleY)),
}
}
// ToLogical 是 ToPhysical 的逆运算,用于在逻辑坐标系里定位选区。
func (m Mapping) ToLogical(r Region) (x, y, w, h float64) {
return (float64(r.X) - m.OffsetX) / m.ScaleX,
(float64(r.Y) - m.OffsetY) / m.ScaleY,
float64(r.W) / m.ScaleX,
float64(r.H) / m.ScaleY
}
// Screen 是基于 GDI 的抓屏实现。
type Screen struct{}
// Grab 抓取指定区域。
func (Screen) Grab(r Region) (image.Image, error) {
if !r.Valid() {
return nil, fmt.Errorf("抓屏区域无效: %s", r)
}
img, err := screenshot.CaptureRect(r.Rect())
if err != nil {
return nil, fmt.Errorf("抓取区域 %s 失败: %w", r, err)
}
return img, nil
}
// Desktop 抓取整个虚拟桌面(所有显示器)。
func (Screen) Desktop() (image.Image, error) {
vd := win32.VirtualScreen()
if vd.Width() <= 0 || vd.Height() <= 0 {
return nil, fmt.Errorf("获取虚拟桌面尺寸失败")
}
r := Region{X: vd.Left, Y: vd.Top, W: vd.Width(), H: vd.Height()}
return Screen{}.Grab(r)
}
// Monitors 枚举显示器。
func (Screen) Monitors() []Monitor { return win32.Monitors() }
// VirtualScreen 返回虚拟桌面矩形。
func (Screen) VirtualScreen() win32.Rect { return win32.VirtualScreen() }
+37
View File
@@ -0,0 +1,37 @@
package capture
import "testing"
func TestMappingRoundTrip(t *testing.T) {
// 模拟 125% 缩放的副屏:窗口左上角在 (-1920,0)
m := Mapping{OffsetX: -1920, OffsetY: 0, ScaleX: 1.25, ScaleY: 1.25}
got := m.ToPhysical(100, 100, 400, 200)
want := Region{X: -1795, Y: 125, W: 500, H: 250}
if got != want {
t.Fatalf("逻辑→物理换算错误,期望 %+v,实际 %+v", want, got)
}
x, y, w, h := m.ToLogical(got)
if x != 100 || y != 100 || w != 400 || h != 200 {
t.Fatalf("物理→逻辑逆向换算错误:%v %v %v %v", x, y, w, h)
}
}
func TestRegionValidAndInset(t *testing.T) {
if (Region{}).Valid() {
t.Fatal("零区域不应有效")
}
r := Region{X: 10, Y: 20, W: 100, H: 50}
if !r.Valid() {
t.Fatal("正常区域应有效")
}
in := r.Inset(5)
if in != (Region{X: 15, Y: 25, W: 90, H: 40}) {
t.Fatalf("Inset 结果错误: %+v", in)
}
// 收缩超过一半时保持原样,避免出现负尺寸
if r.Inset(60) != r {
t.Fatal("过度收缩应返回原区域")
}
}