macOS注册全局快捷键RegisterEventHotKey
macOS注册全局快捷键RegisterEventHotKey

macOS注册全局快捷键RegisterEventHotKey

RegisterEventHotKey 是 macOS 的 Carbon API 中的一个函数,用于注册全局快捷键(热键),即使应用当前不在前台,也可以响应指定的键盘组合。

基本用法

OSStatus RegisterEventHotKey(
   UInt32 inHotKeyCode,
   UInt32 inHotKeyModifiers,
   EventHotKeyID inHotKeyID,
   EventTargetRef inTarget,
   OptionBits inOptions,
   EventHotKeyRef *outRef
);

可以通过它实现「按下 ⌘ + ⇧ + S 在任何应用中截屏」这样的功能。

参数详解

1、inHotKeyCode:快捷键的 keyCode(物理按键的代码,如 kVK_ANSI_S);

2、inHotKeyModifiers:修饰键,如 cmd, shift, option, control 等(例如 cmd + shift 就是 `cmdKey;

3、inHotKeyID:用于标识这个热键(signature 和 id 字段);

4、inTarget:通常为 GetEventDispatcherTarget(),表示系统事件分发器;

5、inOptions:一般传 0;

6、outRef:返回注册的热键引用,用于后续注销(UnregisterEventHotKey)。

快捷键修饰键常量

cmdKey       = 0x1000
optionKey    = 0x0800
controlKey   = 0x4000
shiftKey     = 0x0200

使用场景-注册快捷键

import Carbon.HIToolbox

var hotKeyRef: EventHotKeyRef? = nil
var gMyHotKeyID = EventHotKeyID(signature: OSType(bitPattern: UInt32(truncatingIfNeeded: "TEST".fourCharCodeValue)),
                                id: 1)

// 注册快捷键 cmd + shift + S
RegisterEventHotKey(UInt32(kVK_ANSI_S),
                    UInt32(cmdKey | shiftKey),
                    gMyHotKeyID,
                    GetEventDispatcherTarget(),
                    0,
                    &hotKeyRef)

extension String {
    var fourCharCodeValue: UInt32 {
        var result: UInt32 = 0
        for character in self.utf8.prefix(4) {
            result = (result << 8) + UInt32(character)
        }
        return result
    }
}

还需要监听热键事件,一般写在入口文件或 NSApplicationDelegate 里:

InstallEventHandler(GetApplicationEventTarget(), { (handlerCallRef, eventRef, userData) in
    var hotKeyID = EventHotKeyID()
    GetEventParameter(eventRef, EventParamName(kEventParamDirectObject),
                      EventParamType(typeEventHotKeyID), nil,
                      MemoryLayout<EventHotKeyID>.size,
                      nil, &hotKeyID)
    
    if hotKeyID.id == 1 {
        print("触发快捷键:cmd+shift+S")
    }
    
    return noErr
}, 1, [EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed))], nil, nil)

1、快捷键代码解析

1、导入Carbon框架

import Carbon.HIToolbox

Carbon 是 macOS 上的底层 C API 框架;

HIToolbox 提供了与键盘、菜单等 UI 相关的功能;

RegisterEventHotKey 和 InstallEventHandler 都定义在这个框架中。

2、声明热键变量和 ID

var hotKeyRef: EventHotKeyRef? = nil

用来保存注册热键的引用;

后面如果需要取消注册,可以使用 UnregisterEventHotKey(hotKeyRef!)。

var gMyHotKeyID = EventHotKeyID(
    signature: OSType(bitPattern: UInt32(truncatingIfNeeded: "TEST".fourCharCodeValue)),
    id: 1
)

EventHotKeyID 是用来标识热键事件的结构体;

1)signature: 任意 4 个字符(例如 “TEST”),用于分类;

2)id: 用于区分不同的热键(如果注册了多个热键,可以通过 id 来区分是哪个被按下)。

3)fourCharCodeValue 是一个扩展方法,将 “TEST” 转为 UInt32。也可以手动写 FOUR_CHAR_CODE(“TEST”)。

如果不想要使用fourCharCodeValue方法,也可以直接转换为UInt32类型,例如“TEST”可以转换为:

let hotKeyID = EventHotKeyID(signature: OSType(0x54455354), id: 1) // 'TEST'

3、注册热键

RegisterEventHotKey(
    UInt32(kVK_ANSI_S),                    // 键码(S键)
    UInt32(cmdKey | shiftKey),            // 修饰键(⌘ + ⇧)
    gMyHotKeyID,                          // 热键ID
    GetEventDispatcherTarget(),           // 事件分发目标(系统默认即可)
    0,                                    // 默认选项
    &hotKeyRef                            // 返回热键引用
)

这段代码注册了一个全局快捷键 ⌘ + ⇧ + S;

如果系统中没有冲突(其他应用没占用),注册将成功;

此快捷键即使在其他 App 中也能触发事件处理器。

2、监听热键事件代码解析

安装事件监听器:

InstallEventHandler(
    GetApplicationEventTarget(),          // 事件监听目标
    { (handlerCallRef, eventRef, userData) in
        var hotKeyID = EventHotKeyID()
        GetEventParameter(eventRef,
                          EventParamName(kEventParamDirectObject),
                          EventParamType(typeEventHotKeyID),
                          nil,
                          MemoryLayout<EventHotKeyID>.size,
                          nil,
                          &hotKeyID)

        if hotKeyID.id == 1 {
            print("触发快捷键:cmd+shift+S")
        }

        return noErr
    },
    1,                                    // 监听的事件数量
    [EventTypeSpec(eventClass: OSType(kEventClassKeyboard),
                   eventKind: UInt32(kEventHotKeyPressed))],  // 快捷键按下事件
    nil,
    nil
)

InstallEventHandler(…):注册一个事件回调函数:

InstallEventHandler(
    _ inTarget: EventTargetRef,          // 谁来接收事件
    _ inHandler: EventHandlerUPP,        // 回调函数
    _ inNumTypes: UInt32,                // 要监听的事件数量
    _ inList: UnsafePointer<EventTypeSpec>, // 要监听的事件列表
    _ inUserData: UnsafeMutableRawPointer?, // 用户数据(可选)
    _ outRef: UnsafeMutablePointer<EventHandlerRef>? // 返回值(可选)
)

注册了一个全局键盘快捷键(例如 ⌘ + ⇧ + S),注册热键之后,还需要监听“热键被触发”的事件,这就是 InstallEventHandler 的作用。

1、监听层级

InstallEventHandler(
    GetApplicationEventTarget(), // 表示“应用级别”的事件目标

这里监听的是整个 App 层面的事件(不是窗口,不是控件)。

2、回调事件

{ (handlerCallRef, eventRef, userData) in
    var hotKeyID = EventHotKeyID()
    GetEventParameter(eventRef,
                      EventParamName(kEventParamDirectObject),
                      EventParamType(typeEventHotKeyID),
                      nil,
                      MemoryLayout<EventHotKeyID>.size,
                      nil,
                      &hotKeyID)

从 eventRef 中提取出触发事件的 HotKey ID。它对应之前注册的 EventHotKeyID(signature: …, id: 1)。

if hotKeyID.id == 1 {
    print("触发快捷键:cmd+shift+S")
}

return noErr

根据热键 ID 判断哪个热键被触发,然后执行对应操作(比如截图、弹窗、调用其他函数等)。

3、监听事件数量

1, // 要监听一个事件

监听的事件种类数量为 1 个。

4、指定监听的事件

[EventTypeSpec(
    eventClass: OSType(kEventClassKeyboard),
    eventKind: UInt32(kEventHotKeyPressed)
)],  // 指定要监听“热键被按下”的事件
nil,
nil

指定监听的事件是:键盘事件中的 “热键按下” 类型。

3、实现效果

注意事项

1、RegisterEventHotKey 是 Carbon API,虽然已过时但仍然有效;

2、注册全局快捷键不等于监听按键,需要手动添加事件处理器;

3、若要监听快捷键并做 UI 操作,建议使用非沙盒 App;

4、注册冲突(已有其他应用占用快捷键)时,不会生效,建议提供设置界面让用户自定义快捷键。

5、InstallEventHandler 必须在主线程中执行。

总结

RegisterEventHotKey用于注册全局快捷键,在应用启动时,会监听应用层面的快捷键,如果与其他应用的快捷键重复,可能会失效或覆盖其他应用的快捷键。

以Command + Shift + S为例,在Word应用中为另存为文件,如果设置Command + Shift + S的RegisterEventHotKey快捷键后,按Command + Shift + S就会调用RegisterEventHotKey对于的方法。

与其他监听不同的是,RegisterEventHotKey走的是 Carbon 的 系统事件分发机制,属于官方支持的“安全”热键注册机制,不涉及任何监听或拦截行为:

它是向系统注册快捷键,系统在合适时把事件“分发”给用户;

它不会监听用户的按键流,不会知道用户是否按了其他键;

所以 macOS 不认为这侵犯隐私,不要求授权辅助功能权限

快捷键优先级问题

当注册的全局快捷键与其他应用的全局快捷键重复时,macOS会遵循以下优先级规则(非官方总结):

1、前台 App(active App)优先;

2、先注册的 App 优先(同一层级时);

3、某些系统级 App(如 iShot、Alfred、Karabiner)使用了 更底层机制(如 CGEventTap)或私有 API,可绕过常规限制;

4、辅助功能(Accessibility)权限更高的 App 可注册更强控制;

5、沙盒应用在某些情况下受到限制。

而RegisterEventHotKey属于 Carbon 的全局热键注册机制,不需要辅助权限,所以它的“全局”不是系统层最高优先级,而是系统协调分发的那一层;

如果其他 App 注册了相同组合,RegisterEventHotKey就抢不过它们。

完整代码

1、HotKey代码:

import Carbon.HIToolbox
extension String {
    var fourCharCodeValue: UInt32 {
        var result: UInt32 = 0
        for character in self.utf8.prefix(4) {
            result = (result << 8) + UInt32(character)
        }
        return result
    }
}

class HotKey {
    
    var hotKeyRef: EventHotKeyRef? = nil
    var gMyHotKeyID = EventHotKeyID(signature: OSType("TEST".fourCharCodeValue), id: 1)
    
        
    func registerHotKey() {
        // 注册快捷键 cmd + shift + S
        RegisterEventHotKey(UInt32(kVK_ANSI_S),
                            UInt32(cmdKey | shiftKey),
                            gMyHotKeyID,
                            GetEventDispatcherTarget(),
                            0,
                            &hotKeyRef)
    }
}

2、AppDelegate文件代码:

import Carbon.HIToolbox

class AppDelegate: NSObject, NSApplicationDelegate {
    
    var hotKey: HotKey?
    
    func applicationDidFinishLaunching(_ notification: Notification) {
        hotKey = HotKey()
        hotKey?.registerHotKey()
        print("注册快捷键")
        InstallEventHandler(
            GetApplicationEventTarget(),          // 事件监听目标
            { (handlerCallRef, eventRef, userData) in
                var hotKeyID = EventHotKeyID()
                GetEventParameter(eventRef,
                                  EventParamName(kEventParamDirectObject),
                                  EventParamType(typeEventHotKeyID),
                                  nil,
                                  MemoryLayout<EventHotKeyID>.size,
                                  nil,
                                  &hotKeyID)

                if hotKeyID.id == 1 {
                    print("触发快捷键:cmd+shift+S")
                    print("调用全屏截图")
                    StatusBarController.shared.fullScreenshoot()
                }

                return noErr
            },
            1,                                    // 监听的事件数量
            [EventTypeSpec(eventClass: OSType(kEventClassKeyboard),
                           eventKind: UInt32(kEventHotKeyPressed))],  // 快捷键按下事件
            nil,
            nil
        )
        print("快捷键注册完成")
    }
}
   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注