Files
2026-10-27 17:58:28 +08:00

189 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}