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 }