first commit
This commit is contained in:
commit
3553790475
27 files changed
+3839
No files matched your search
@@ -0,0 +1,188 @@
|
||||
package win32
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
|
||||
procRegisterHotKey = user32.NewProc("RegisterHotKey")
|
||||
procUnregisterHotKey = user32.NewProc("UnregisterHotKey")
|
||||
procGetMessageW = user32.NewProc("GetMessageW")
|
||||
procPostThreadMessageW = user32.NewProc("PostThreadMessageW")
|
||||
// GetCurrentThreadId 位于 kernel32,不在 user32。
|
||||
procGetCurrentThreadID = kernel32.NewProc("GetCurrentThreadId")
|
||||
)
|
||||
|
||||
const (
|
||||
wmHotkey = 0x0312
|
||||
wmQuit = 0x0012
|
||||
|
||||
modAlt = 0x0001
|
||||
modControl = 0x0002
|
||||
modShift = 0x0004
|
||||
modWin = 0x0008
|
||||
modNoRepeat = 0x4000
|
||||
)
|
||||
|
||||
// msg 对应 Win32 的 MSG 结构(x/sys/windows 未导出该类型)。
|
||||
type msg struct {
|
||||
HWnd uintptr
|
||||
Message uint32
|
||||
_ uint32
|
||||
WParam uintptr
|
||||
LParam uintptr
|
||||
Time uint32
|
||||
Pt struct {
|
||||
X, Y int32
|
||||
}
|
||||
}
|
||||
|
||||
// Hotkey 是一组解析后的热键定义。
|
||||
type Hotkey struct {
|
||||
ID int
|
||||
Mods uint32
|
||||
VK uint32
|
||||
Spec string
|
||||
}
|
||||
|
||||
var namedKeys = map[string]uint32{
|
||||
"space": 0x20, "esc": 0x1B, "escape": 0x1B, "enter": 0x0D, "tab": 0x09,
|
||||
"backspace": 0x08, "delete": 0x2E, "insert": 0x2D, "home": 0x24, "end": 0x23,
|
||||
"pageup": 0x21, "pagedown": 0x22, "left": 0x25, "up": 0x26, "right": 0x27, "down": 0x28,
|
||||
"f1": 0x70, "f2": 0x71, "f3": 0x72, "f4": 0x73, "f5": 0x74, "f6": 0x75,
|
||||
"f7": 0x76, "f8": 0x77, "f9": 0x78, "f10": 0x79, "f11": 0x7A, "f12": 0x7B,
|
||||
}
|
||||
|
||||
// ParseHotkey 解析 "Ctrl+Alt+Q" 形式的热键描述。
|
||||
func ParseHotkey(spec string) (Hotkey, error) {
|
||||
parts := strings.Split(strings.TrimSpace(spec), "+")
|
||||
if len(parts) == 0 {
|
||||
return Hotkey{}, fmt.Errorf("热键为空")
|
||||
}
|
||||
hk := Hotkey{Mods: modNoRepeat, Spec: spec}
|
||||
for _, raw := range parts {
|
||||
p := strings.ToLower(strings.TrimSpace(raw))
|
||||
switch p {
|
||||
case "ctrl", "control":
|
||||
hk.Mods |= modControl
|
||||
case "alt":
|
||||
hk.Mods |= modAlt
|
||||
case "shift":
|
||||
hk.Mods |= modShift
|
||||
case "win", "meta":
|
||||
hk.Mods |= modWin
|
||||
case "":
|
||||
return Hotkey{}, fmt.Errorf("热键 %q 含空段", spec)
|
||||
default:
|
||||
if hk.VK != 0 {
|
||||
return Hotkey{}, fmt.Errorf("热键 %q 只能有一个主键", spec)
|
||||
}
|
||||
if vk, ok := namedKeys[p]; ok {
|
||||
hk.VK = vk
|
||||
continue
|
||||
}
|
||||
if len(p) == 1 && p[0] >= 'a' && p[0] <= 'z' {
|
||||
hk.VK = uint32(p[0] - 'a' + 'A')
|
||||
continue
|
||||
}
|
||||
if len(p) == 1 && p[0] >= '0' && p[0] <= '9' {
|
||||
hk.VK = uint32(p[0])
|
||||
continue
|
||||
}
|
||||
return Hotkey{}, fmt.Errorf("无法识别的主键 %q", raw)
|
||||
}
|
||||
}
|
||||
if hk.VK == 0 {
|
||||
return Hotkey{}, fmt.Errorf("热键 %q 缺少主键", spec)
|
||||
}
|
||||
return hk, nil
|
||||
}
|
||||
|
||||
// ListenHotkeys 在独立 OS 线程上注册全局热键并泵送消息。
|
||||
// onFire 运行在该线程上,调用方应自行转到 UI 线程。
|
||||
// 返回的 stop 函数可安全调用一次。
|
||||
//
|
||||
// 单个热键被其它程序占用时只跳过它并在 warnings 里说明,其余热键照常生效;
|
||||
// 只有全部注册失败才返回 err。
|
||||
func ListenHotkeys(specs []string, onFire func(spec string)) (stop func(), warnings []string, err error) {
|
||||
if len(specs) == 0 {
|
||||
return func() {}, nil, nil
|
||||
}
|
||||
|
||||
type registration struct {
|
||||
hotkeys []Hotkey
|
||||
tid uint32
|
||||
warnings []string
|
||||
}
|
||||
|
||||
ready := make(chan error, 1)
|
||||
done := make(chan struct{})
|
||||
reg := make(chan registration, 1)
|
||||
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
tid, _, _ := procGetCurrentThreadID.Call()
|
||||
hotkeys := make([]Hotkey, 0, len(specs))
|
||||
var warns []string
|
||||
for i, spec := range specs {
|
||||
if strings.TrimSpace(spec) == "" {
|
||||
continue
|
||||
}
|
||||
hk, perr := ParseHotkey(spec)
|
||||
if perr != nil {
|
||||
warns = append(warns, perr.Error())
|
||||
continue
|
||||
}
|
||||
hk.ID = 100 + i
|
||||
if ok, _, e := procRegisterHotKey.Call(0, uintptr(hk.ID), uintptr(hk.Mods), uintptr(hk.VK)); ok == 0 {
|
||||
warns = append(warns, fmt.Sprintf("%s 已被其它程序占用(%v)", spec, e))
|
||||
continue
|
||||
}
|
||||
hotkeys = append(hotkeys, hk)
|
||||
}
|
||||
if len(hotkeys) == 0 {
|
||||
ready <- fmt.Errorf("所有全局热键都无法注册:%s", strings.Join(warns, ";"))
|
||||
return
|
||||
}
|
||||
reg <- registration{hotkeys: hotkeys, tid: uint32(tid), warnings: warns}
|
||||
ready <- nil
|
||||
|
||||
var m msg
|
||||
for {
|
||||
r, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
|
||||
if int32(r) <= 0 {
|
||||
break
|
||||
}
|
||||
if m.Message != wmHotkey {
|
||||
continue
|
||||
}
|
||||
id := int(m.WParam)
|
||||
for _, hk := range hotkeys {
|
||||
if hk.ID == id {
|
||||
onFire(hk.Spec)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, hk := range hotkeys {
|
||||
procUnregisterHotKey.Call(0, uintptr(hk.ID))
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
if err := <-ready; err != nil {
|
||||
return func() {}, nil, err
|
||||
}
|
||||
r := <-reg
|
||||
return func() {
|
||||
procPostThreadMessageW.Call(uintptr(r.tid), wmQuit, 0, 0)
|
||||
<-done
|
||||
}, r.warnings, nil
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user