first commit
This commit is contained in:
commit
3553790475
27 files changed
+3839
No files matched your search
@@ -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() }
|
||||
@@ -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("过度收缩应返回原区域")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user