189 lines
4.6 KiB
Go
189 lines
4.6 KiB
Go
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
|
||
}
|