package ui import ( "fmt" "image" "image/color" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "viewsagent/internal/capture" "viewsagent/internal/win32" ) // overlayTitle 是框选窗口标题,win32 需要靠它找到 HWND。 const overlayTitle = "viewsagent-overlay" // overlay 是一个铺满当前显示器的冻结帧框选层。 // // 之所以不依赖窗口透明:Windows 下的 OpenGL 窗口透明并不可靠, // 直接用"全屏截图做背景 + 遮罩"视觉等效,还能保证选区与截图完全对齐。 type overlay struct { win fyne.Window vd win32.Rect mapping capture.Mapping size fyne.Size calibrated bool bg *canvas.Image dims [4]*canvas.Rectangle edges [4]*canvas.Rectangle hint *canvas.Text catcher *catcher start, cur fyne.Position dragging bool closed bool onDone func(capture.Region) onCancel func() } // newOverlay 创建框选层。shot 应该是调用前抓好的整屏画面。 func newOverlay(app fyne.App, shot image.Image, vd win32.Rect, onDone func(capture.Region), onCancel func()) *overlay { o := &overlay{vd: vd, onDone: onDone, onCancel: onCancel} o.win = app.NewWindow(overlayTitle) o.win.SetPadded(false) o.win.SetFullScreen(true) o.bg = canvas.NewImageFromImage(shot) o.bg.FillMode = canvas.ImageFillStretch objs := []fyne.CanvasObject{o.bg} for i := range o.dims { o.dims[i] = canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 130}) objs = append(objs, o.dims[i]) } for i := range o.edges { o.edges[i] = canvas.NewRectangle(color.NRGBA{R: 0x5A, G: 0xC8, B: 0xFF, A: 0xFF}) objs = append(objs, o.edges[i]) } o.hint = canvas.NewText("拖拽框选区域 · Esc 取消", color.White) o.hint.TextStyle = fyne.TextStyle{Bold: true} o.hint.TextSize = 15 o.catcher = newCatcher(o) objs = append(objs, o.catcher, o.hint) o.win.SetContent(container.NewWithoutLayout(objs...)) o.win.Canvas().SetOnTypedKey(func(k *fyne.KeyEvent) { if k.Name == fyne.KeyEscape { o.cancel() } }) return o } // show 显示框选层并做一次坐标校准。 func (o *overlay) show() { o.win.Show() // 窗口尺寸在 Show 之后才确定,稍等一拍再校准。 go func() { time.Sleep(90 * time.Millisecond) fyne.Do(o.calibrate) }() } // calibrate 反推"逻辑坐标 → 物理像素"的比例与原点。 // 宁可依赖实际窗口尺寸,也不假设系统缩放,这样 125%/150% 屏都不会偏。 func (o *overlay) calibrate() { size := o.win.Canvas().Size() if size.Width < 10 || size.Height < 10 { return } o.size = size rect, err := windowRectByTitle(overlayTitle) if err != nil { mon, _ := win32.MonitorAtCursor() rect = mon.Rect } if rect.Width() <= 0 || rect.Height() <= 0 { return } o.mapping = capture.Mapping{ OffsetX: float64(rect.Left), OffsetY: float64(rect.Top), ScaleX: float64(rect.Width()) / float64(size.Width), ScaleY: float64(rect.Height()) / float64(size.Height), } if o.mapping.ScaleX <= 0 || o.mapping.ScaleY <= 0 { o.mapping.ScaleX, o.mapping.ScaleY = 1, 1 } // 把整张虚拟桌面按当前窗口的实际映射铺到画布上。 bx, by, bw, bh := o.mapping.ToLogical(capture.Region{ X: o.vd.Left, Y: o.vd.Top, W: o.vd.Width(), H: o.vd.Height(), }) o.bg.Move(fyne.NewPos(float32(bx), float32(by))) o.bg.Resize(fyne.NewSize(float32(bw), float32(bh))) o.catcher.Move(fyne.NewPos(0, 0)) o.catcher.Resize(size) o.calibrated = true o.updateSelection() } func windowRectByTitle(title string) (win32.Rect, error) { hwnd, err := win32.FindWindowByTitle(title) if err != nil { return win32.Rect{}, err } return win32.WindowRect(hwnd) } // selection 返回当前选区(逻辑坐标,已归一化)。 func (o *overlay) selection() (x0, y0, x1, y1 float32) { x0, y0 = o.start.X, o.start.Y x1, y1 = o.cur.X, o.cur.Y if x1 < x0 { x0, x1 = x1, x0 } if y1 < y0 { y0, y1 = y1, y0 } // 限制在窗口内 x0 = fyne.Max(x0, 0) y0 = fyne.Max(y0, 0) x1 = fyne.Min(x1, o.size.Width) y1 = fyne.Min(y1, o.size.Height) return } // updateSelection 重排遮罩、边框与尺寸提示。 func (o *overlay) updateSelection() { if o.size.Width <= 0 { return } w, h := o.size.Width, o.size.Height x0, y0, x1, y1 := o.selection() selW, selH := x1-x0, y1-y0 if selW < 2 || selH < 2 { // 尚未框选:整屏压暗 o.dims[0].Move(fyne.NewPos(0, 0)) o.dims[0].Resize(fyne.NewSize(w, h)) for i := 1; i < 4; i++ { o.dims[i].Resize(fyne.NewSize(0, 0)) } for _, e := range o.edges { e.Resize(fyne.NewSize(0, 0)) } o.hint.Text = "拖拽框选区域 · Esc 取消" o.hint.Move(fyne.NewPos(theme.Padding()*2, theme.Padding()*2)) o.hint.Refresh() return } // 上下左右四块压暗区域,挖出选区 o.dims[0].Move(fyne.NewPos(0, 0)) o.dims[0].Resize(fyne.NewSize(w, y0)) o.dims[1].Move(fyne.NewPos(0, y1)) o.dims[1].Resize(fyne.NewSize(w, h-y1)) o.dims[2].Move(fyne.NewPos(0, y0)) o.dims[2].Resize(fyne.NewSize(x0, selH)) o.dims[3].Move(fyne.NewPos(x1, y0)) o.dims[3].Resize(fyne.NewSize(w-x1, selH)) // 选区描边 const t = 2 o.edges[0].Move(fyne.NewPos(x0, y0)) o.edges[0].Resize(fyne.NewSize(selW, t)) o.edges[1].Move(fyne.NewPos(x0, y1-t)) o.edges[1].Resize(fyne.NewSize(selW, t)) o.edges[2].Move(fyne.NewPos(x0, y0)) o.edges[2].Resize(fyne.NewSize(t, selH)) o.edges[3].Move(fyne.NewPos(x1-t, y0)) o.edges[3].Resize(fyne.NewSize(t, selH)) phys := o.mapping.ToPhysical(float64(x0), float64(y0), float64(selW), float64(selH)) o.hint.Text = fmt.Sprintf("%d × %d", phys.W, phys.H) hy := y0 - 26 if hy < 4 { hy = y0 + 6 } o.hint.Move(fyne.NewPos(x0+6, hy)) o.hint.Refresh() } // finish 确认选区。 func (o *overlay) finish() { if !o.calibrated { o.calibrate() } x0, y0, x1, y1 := o.selection() if x1-x0 < 6 || y1-y0 < 6 { o.cancel() return } r := o.mapping.ToPhysical(float64(x0), float64(y0), float64(x1-x0), float64(y1-y0)) if r.W < 8 || r.H < 8 { o.cancel() return } done := o.onDone o.close() if done != nil { done(r) } } // cancel 取消框选。 func (o *overlay) cancel() { cancel := o.onCancel o.close() if cancel != nil { cancel() } } func (o *overlay) close() { if o.closed { return } o.closed = true o.win.Close() } // catcher 是铺满窗口的事件接收层,负责鼠标拖拽。 type catcher struct { widget.BaseWidget o *overlay } func newCatcher(o *overlay) *catcher { c := &catcher{o: o} c.ExtendBaseWidget(c) return c } func (c *catcher) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(canvas.NewRectangle(color.Transparent)) } func (c *catcher) Dragged(e *fyne.DragEvent) { if !c.o.dragging { return } c.o.cur = e.Position c.o.updateSelection() } func (c *catcher) DragEnd() {} func (c *catcher) MouseDown(e *desktop.MouseEvent) { c.o.dragging = true c.o.start = e.Position c.o.cur = e.Position c.o.updateSelection() } func (c *catcher) MouseUp(e *desktop.MouseEvent) { if !c.o.dragging { return } c.o.dragging = false c.o.cur = e.Position c.o.finish() }