231 lines
5.8 KiB
Go
231 lines
5.8 KiB
Go
// Package win32 封装 Fyne 未提供的 Windows 原生能力:
|
|
// 显示器枚举、窗口置顶/移动、全局热键、DPI 查询。
|
|
package win32
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
var (
|
|
user32 = windows.NewLazySystemDLL("user32.dll")
|
|
shcore = windows.NewLazySystemDLL("shcore.dll")
|
|
|
|
procFindWindowW = user32.NewProc("FindWindowW")
|
|
procGetWindowRect = user32.NewProc("GetWindowRect")
|
|
procSetWindowPos = user32.NewProc("SetWindowPos")
|
|
procGetCursorPos = user32.NewProc("GetCursorPos")
|
|
procEnumDisplayMonitors = user32.NewProc("EnumDisplayMonitors")
|
|
procGetMonitorInfoW = user32.NewProc("GetMonitorInfoW")
|
|
procGetDpiForMonitor = shcore.NewProc("GetDpiForMonitor")
|
|
)
|
|
|
|
const (
|
|
swpNoSize = 0x0001
|
|
swpNoMove = 0x0002
|
|
swpNoZOrder = 0x0004
|
|
swpNoActivate = 0x0010
|
|
swpShowWindow = 0x0040
|
|
|
|
hwndTopMost = ^uintptr(0) // (HWND)-1
|
|
mdtEffectiveDPI = 0
|
|
monitorInfoPrimary = 1
|
|
)
|
|
|
|
// Rect 是以物理像素表示的屏幕矩形。
|
|
type Rect struct {
|
|
Left, Top, Right, Bottom int
|
|
}
|
|
|
|
// Width 返回矩形宽度。
|
|
func (r Rect) Width() int { return r.Right - r.Left }
|
|
|
|
// Height 返回矩形高度。
|
|
func (r Rect) Height() int { return r.Bottom - r.Top }
|
|
|
|
// Contains 判断点是否落在矩形内。
|
|
func (r Rect) Contains(x, y int) bool {
|
|
return x >= r.Left && x < r.Right && y >= r.Top && y < r.Bottom
|
|
}
|
|
|
|
type nativeRect struct {
|
|
Left, Top, Right, Bottom int32
|
|
}
|
|
|
|
// Monitor 描述一台显示器。
|
|
type Monitor struct {
|
|
Index int
|
|
Rect Rect // 完整屏幕区域(物理像素)
|
|
Work Rect // 去掉任务栏的可用区域
|
|
Primary bool
|
|
Scale float64 // 系统缩放,例如 1.25
|
|
}
|
|
|
|
type nativeMonitorInfo struct {
|
|
CbSize uint32
|
|
RcMonitor nativeRect
|
|
RcWork nativeRect
|
|
DwFlags uint32
|
|
}
|
|
|
|
type point struct {
|
|
X, Y int32
|
|
}
|
|
|
|
var (
|
|
enumMu sync.Mutex
|
|
enumSink *[]Monitor
|
|
enumCallback uintptr
|
|
)
|
|
|
|
// Monitors 枚举当前所有显示器,坐标已换算为物理像素。
|
|
func Monitors() []Monitor {
|
|
enumMu.Lock()
|
|
defer enumMu.Unlock()
|
|
|
|
var out []Monitor
|
|
enumSink = &out
|
|
if enumCallback == 0 {
|
|
enumCallback = windows.NewCallback(enumProc)
|
|
}
|
|
procEnumDisplayMonitors.Call(0, 0, enumCallback, 0)
|
|
enumSink = nil
|
|
|
|
for i := range out {
|
|
out[i].Index = i
|
|
}
|
|
return out
|
|
}
|
|
|
|
func enumProc(hMonitor, _, _, _ uintptr) uintptr {
|
|
if enumSink == nil {
|
|
return 1
|
|
}
|
|
info := nativeMonitorInfo{CbSize: uint32(unsafe.Sizeof(nativeMonitorInfo{}))}
|
|
if r, _, _ := procGetMonitorInfoW.Call(hMonitor, uintptr(unsafe.Pointer(&info))); r == 0 {
|
|
return 1
|
|
}
|
|
m := Monitor{
|
|
Rect: Rect{
|
|
Left: int(info.RcMonitor.Left),
|
|
Top: int(info.RcMonitor.Top),
|
|
Right: int(info.RcMonitor.Right),
|
|
Bottom: int(info.RcMonitor.Bottom),
|
|
},
|
|
Work: Rect{
|
|
Left: int(info.RcWork.Left),
|
|
Top: int(info.RcWork.Top),
|
|
Right: int(info.RcWork.Right),
|
|
Bottom: int(info.RcWork.Bottom),
|
|
},
|
|
Primary: info.DwFlags&monitorInfoPrimary != 0,
|
|
Scale: 1,
|
|
}
|
|
var dpiX, dpiY uint32
|
|
if hr, _, _ := procGetDpiForMonitor.Call(hMonitor, mdtEffectiveDPI,
|
|
uintptr(unsafe.Pointer(&dpiX)), uintptr(unsafe.Pointer(&dpiY))); hr == 0 && dpiX > 0 {
|
|
m.Scale = float64(dpiX) / 96
|
|
}
|
|
*enumSink = append(*enumSink, m)
|
|
return 1
|
|
}
|
|
|
|
// VirtualScreen 返回所有显示器的并集,作为虚拟桌面坐标参考。
|
|
func VirtualScreen() Rect {
|
|
ms := Monitors()
|
|
if len(ms) == 0 {
|
|
return Rect{}
|
|
}
|
|
union := ms[0].Rect
|
|
for _, m := range ms[1:] {
|
|
if m.Rect.Left < union.Left {
|
|
union.Left = m.Rect.Left
|
|
}
|
|
if m.Rect.Top < union.Top {
|
|
union.Top = m.Rect.Top
|
|
}
|
|
if m.Rect.Right > union.Right {
|
|
union.Right = m.Rect.Right
|
|
}
|
|
if m.Rect.Bottom > union.Bottom {
|
|
union.Bottom = m.Rect.Bottom
|
|
}
|
|
}
|
|
return union
|
|
}
|
|
|
|
// CursorPos 返回鼠标当前位置(物理像素)。
|
|
func CursorPos() (int, int) {
|
|
var p point
|
|
if r, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&p))); r == 0 {
|
|
return 0, 0
|
|
}
|
|
return int(p.X), int(p.Y)
|
|
}
|
|
|
|
// MonitorAt 返回包含指定点的显示器,找不到时退化为第一台。
|
|
func MonitorAt(x, y int) (Monitor, bool) {
|
|
ms := Monitors()
|
|
for _, m := range ms {
|
|
if m.Rect.Contains(x, y) {
|
|
return m, true
|
|
}
|
|
}
|
|
if len(ms) > 0 {
|
|
return ms[0], false
|
|
}
|
|
return Monitor{}, false
|
|
}
|
|
|
|
// MonitorAtCursor 返回鼠标所在的显示器。
|
|
func MonitorAtCursor() (Monitor, bool) {
|
|
x, y := CursorPos()
|
|
return MonitorAt(x, y)
|
|
}
|
|
|
|
// FindWindowByTitle 按标题查找顶层窗口句柄。
|
|
func FindWindowByTitle(title string) (syscall.Handle, error) {
|
|
p, err := windows.UTF16PtrFromString(title)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
r, _, e := procFindWindowW.Call(0, uintptr(unsafe.Pointer(p)))
|
|
if r == 0 {
|
|
return 0, fmt.Errorf("未找到标题为 %q 的窗口: %w", title, e)
|
|
}
|
|
return syscall.Handle(r), nil
|
|
}
|
|
|
|
// WindowRect 返回窗口在屏幕上的物理像素矩形。
|
|
func WindowRect(hwnd syscall.Handle) (Rect, error) {
|
|
var r nativeRect
|
|
if ok, _, e := procGetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r))); ok == 0 {
|
|
return Rect{}, fmt.Errorf("GetWindowRect 失败: %w", e)
|
|
}
|
|
return Rect{Left: int(r.Left), Top: int(r.Top), Right: int(r.Right), Bottom: int(r.Bottom)}, nil
|
|
}
|
|
|
|
// SetTopMost 让窗口保持置顶但不抢焦点。
|
|
func SetTopMost(hwnd syscall.Handle) error {
|
|
if ok, _, e := procSetWindowPos.Call(uintptr(hwnd), hwndTopMost, 0, 0, 0, 0,
|
|
swpNoMove|swpNoSize|swpNoActivate); ok == 0 {
|
|
return fmt.Errorf("SetWindowPos(TOPMOST) 失败: %w", e)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MoveTo 把窗口移动到指定物理矩形(不改变 Z 序)。
|
|
func MoveTo(hwnd syscall.Handle, r Rect) error {
|
|
if ok, _, e := procSetWindowPos.Call(uintptr(hwnd), 0,
|
|
uintptr(r.Left), uintptr(r.Top),
|
|
uintptr(r.Width()), uintptr(r.Height()),
|
|
swpNoZOrder|swpNoActivate|swpShowWindow); ok == 0 {
|
|
return fmt.Errorf("SetWindowPos(MoveTo) 失败: %w", e)
|
|
}
|
|
return nil
|
|
}
|