如何在 macOS 应用程序中使用 Swift 收听全局热键?
How to listen to global hotkeys with Swift in a macOS app?
我试图在我的 Mac OS X 应用程序中使用 Swift 编写一个全局(系统范围)热键组合的处理程序,但我找不到合适的它的文档。我已经读到我必须为此在一些遗留的 Carbon API 中乱搞,没有更好的方法吗?你能给我一些概念验证 Swift 代码吗?提前致谢!
我不相信你今天可以 100% Swift 做到这一点。您需要调用 InstallEventHandler()
或 CGEventTapCreate()
,并且这两个都需要 CFunctionPointer
,无法在 Swift 中创建。您最好的计划是使用已建立的 ObjC 解决方案,例如 DDHotKey 并桥接到 Swift.
您可以尝试使用 NSEvent.addGlobalMonitorForEventsMatchingMask(handler:)
,但这只会制作事件的副本。你不能消费它们。这意味着热键也将传递给当前活动的应用程序,这可能会导致问题。这是一个例子,但我推荐 ObjC 方法;它几乎肯定会更好地工作。
let keycode = UInt16(kVK_ANSI_X)
let keymask: NSEventModifierFlags = .CommandKeyMask | .AlternateKeyMask | .ControlKeyMask
func handler(event: NSEvent!) {
if event.keyCode == self.keycode &&
event.modifierFlags & self.keymask == self.keymask {
println("PRESSED")
}
}
// ... to set it up ...
let options = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionaryRef
let trusted = AXIsProcessTrustedWithOptions(options)
if (trusted) {
NSEvent.addGlobalMonitorForEventsMatchingMask(.KeyDownMask, handler: self.handler)
}
这还要求为该应用程序批准无障碍服务。它也不会捕获发送到您自己的应用程序的事件,因此您必须使用响应链捕获它们,我们使用 addLocalMointorForEventsMatchingMask(handler:)
添加本地处理程序。
从 Swift 2.0 开始,您现在可以将函数指针传递给 C API。
var gMyHotKeyID = EventHotKeyID()
gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
gMyHotKeyID.id = UInt32(keyCode)
var eventType = EventTypeSpec()
eventType.eventClass = OSType(kEventClassKeyboard)
eventType.eventKind = OSType(kEventHotKeyPressed)
// Install handler.
InstallEventHandler(GetApplicationEventTarget(), {(nextHanlder, theEvent, userData) -> OSStatus in
var hkCom = EventHotKeyID()
GetEventParameter(theEvent, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, sizeof(EventHotKeyID), nil, &hkCom)
// Check that hkCom in indeed your hotkey ID and handle it.
}, 1, &eventType, nil, nil)
// Register hotkey.
let status = RegisterEventHotKey(UInt32(keyCode), UInt32(modifierKeys), gMyHotKeyID, GetApplicationEventTarget(), 0, &hotKeyRef)
快速 Swift 3 设置更新:
let opts = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionary
guard AXIsProcessTrustedWithOptions(opts) == true else { return }
NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: self.handler)
看看热键库。您可以简单地使用 Carthage 将其实现到您自己的应用程序中。
HotKey Library
如果您的应用程序有 Menu
:
- 添加一个新的
MenuItem
(可以将其命名为 "Dummy for Hotkey")
- 在属性检查器中,在
Key Equivalent
字段中方便地输入您的热键
- 将
Allowed when Hidden
、Enabled
和 Hidden
设置为 true
- link 它与 IBAction 一起执行您的热键应该执行的任何操作
完成!
以下代码适用于 Swift 5.0.1。此解决方案结合了 Charlie Monroe 接受的答案和 Rob Napier 推荐使用 DDHotKey 的解决方案。
DDHotKey 似乎开箱即用,但它有一个我必须更改的限制:eventKind 被硬编码为 kEventHotKeyReleased
,而我同时需要 kEventHotKeyPressed
和 kEventHotKeyReleased
事件类型。
eventSpec.eventKind = kEventHotKeyReleased;
如果您想同时处理 Pressed 和 Released 事件,只需添加第二个 InstallEventHandler
调用来注册其他事件类型。
这是为 kEventHotKeyReleased
类型注册 "Command + R" 键的代码的完整示例。
import Carbon
extension String {
/// This converts string to UInt as a fourCharCode
public var fourCharCodeValue: Int {
var result: Int = 0
if let data = self.data(using: String.Encoding.macOSRoman) {
data.withUnsafeBytes({ (rawBytes) in
let bytes = rawBytes.bindMemory(to: UInt8.self)
for i in 0 ..< data.count {
result = result << 8 + Int(bytes[i])
}
})
}
return result
}
}
class HotkeySolution {
static
func getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags) -> UInt32 {
let flags = cocoaFlags.rawValue
var newFlags: Int = 0
if ((flags & NSEvent.ModifierFlags.control.rawValue) > 0) {
newFlags |= controlKey
}
if ((flags & NSEvent.ModifierFlags.command.rawValue) > 0) {
newFlags |= cmdKey
}
if ((flags & NSEvent.ModifierFlags.shift.rawValue) > 0) {
newFlags |= shiftKey;
}
if ((flags & NSEvent.ModifierFlags.option.rawValue) > 0) {
newFlags |= optionKey
}
if ((flags & NSEvent.ModifierFlags.capsLock.rawValue) > 0) {
newFlags |= alphaLock
}
return UInt32(newFlags);
}
static func register() {
var hotKeyRef: EventHotKeyRef?
let modifierFlags: UInt32 =
getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags.command)
let keyCode = kVK_ANSI_R
var gMyHotKeyID = EventHotKeyID()
gMyHotKeyID.id = UInt32(keyCode)
// Not sure what "swat" vs "htk1" do.
gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
// gMyHotKeyID.signature = OSType("htk1".fourCharCodeValue)
var eventType = EventTypeSpec()
eventType.eventClass = OSType(kEventClassKeyboard)
eventType.eventKind = OSType(kEventHotKeyReleased)
// Install handler.
InstallEventHandler(GetApplicationEventTarget(), {
(nextHanlder, theEvent, userData) -> OSStatus in
// var hkCom = EventHotKeyID()
// GetEventParameter(theEvent,
// EventParamName(kEventParamDirectObject),
// EventParamType(typeEventHotKeyID),
// nil,
// MemoryLayout<EventHotKeyID>.size,
// nil,
// &hkCom)
NSLog("Command + R Released!")
return noErr
/// Check that hkCom in indeed your hotkey ID and handle it.
}, 1, &eventType, nil, nil)
// Register hotkey.
let status = RegisterEventHotKey(UInt32(keyCode),
modifierFlags,
gMyHotKeyID,
GetApplicationEventTarget(),
0,
&hotKeyRef)
assert(status == noErr)
}
}
我维护 this Swift package 可以轻松地向您的应用程序添加全局键盘快捷键,也可以让用户设置自己的键盘快捷键。
import SwiftUI
import KeyboardShortcuts
// Declare the shortcut for strongly-typed access.
extension KeyboardShortcuts.Name {
static let toggleUnicornMode = Self("toggleUnicornMode")
}
@main
struct YourApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
// …
}
Settings {
SettingsScreen()
}
}
}
@MainActor
final class AppState: ObservableObject {
init() {
// Register the listener.
KeyboardShortcuts.onKeyUp(for: .toggleUnicornMode) { [self] in
isUnicornMode.toggle()
}
}
}
// Present a view where the user can set the shortcut they want.
struct SettingsScreen: View {
var body: some View {
Form {
HStack(alignment: .firstTextBaseline) {
Text("Toggle Unicorn Mode:")
KeyboardShortcuts.Recorder(for: .toggleUnicornMode)
}
}
}
}
本例使用的是SwiftUI,但也支持Cocoa.
我试图在我的 Mac OS X 应用程序中使用 Swift 编写一个全局(系统范围)热键组合的处理程序,但我找不到合适的它的文档。我已经读到我必须为此在一些遗留的 Carbon API 中乱搞,没有更好的方法吗?你能给我一些概念验证 Swift 代码吗?提前致谢!
我不相信你今天可以 100% Swift 做到这一点。您需要调用 InstallEventHandler()
或 CGEventTapCreate()
,并且这两个都需要 CFunctionPointer
,无法在 Swift 中创建。您最好的计划是使用已建立的 ObjC 解决方案,例如 DDHotKey 并桥接到 Swift.
您可以尝试使用 NSEvent.addGlobalMonitorForEventsMatchingMask(handler:)
,但这只会制作事件的副本。你不能消费它们。这意味着热键也将传递给当前活动的应用程序,这可能会导致问题。这是一个例子,但我推荐 ObjC 方法;它几乎肯定会更好地工作。
let keycode = UInt16(kVK_ANSI_X)
let keymask: NSEventModifierFlags = .CommandKeyMask | .AlternateKeyMask | .ControlKeyMask
func handler(event: NSEvent!) {
if event.keyCode == self.keycode &&
event.modifierFlags & self.keymask == self.keymask {
println("PRESSED")
}
}
// ... to set it up ...
let options = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionaryRef
let trusted = AXIsProcessTrustedWithOptions(options)
if (trusted) {
NSEvent.addGlobalMonitorForEventsMatchingMask(.KeyDownMask, handler: self.handler)
}
这还要求为该应用程序批准无障碍服务。它也不会捕获发送到您自己的应用程序的事件,因此您必须使用响应链捕获它们,我们使用 addLocalMointorForEventsMatchingMask(handler:)
添加本地处理程序。
从 Swift 2.0 开始,您现在可以将函数指针传递给 C API。
var gMyHotKeyID = EventHotKeyID()
gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
gMyHotKeyID.id = UInt32(keyCode)
var eventType = EventTypeSpec()
eventType.eventClass = OSType(kEventClassKeyboard)
eventType.eventKind = OSType(kEventHotKeyPressed)
// Install handler.
InstallEventHandler(GetApplicationEventTarget(), {(nextHanlder, theEvent, userData) -> OSStatus in
var hkCom = EventHotKeyID()
GetEventParameter(theEvent, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, sizeof(EventHotKeyID), nil, &hkCom)
// Check that hkCom in indeed your hotkey ID and handle it.
}, 1, &eventType, nil, nil)
// Register hotkey.
let status = RegisterEventHotKey(UInt32(keyCode), UInt32(modifierKeys), gMyHotKeyID, GetApplicationEventTarget(), 0, &hotKeyRef)
快速 Swift 3 设置更新:
let opts = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionary
guard AXIsProcessTrustedWithOptions(opts) == true else { return }
NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: self.handler)
看看热键库。您可以简单地使用 Carthage 将其实现到您自己的应用程序中。 HotKey Library
如果您的应用程序有 Menu
:
- 添加一个新的
MenuItem
(可以将其命名为 "Dummy for Hotkey") - 在属性检查器中,在
Key Equivalent
字段中方便地输入您的热键 - 将
Allowed when Hidden
、Enabled
和Hidden
设置为 true - link 它与 IBAction 一起执行您的热键应该执行的任何操作
完成!
以下代码适用于 Swift 5.0.1。此解决方案结合了 Charlie Monroe 接受的答案和 Rob Napier 推荐使用 DDHotKey 的解决方案。
DDHotKey 似乎开箱即用,但它有一个我必须更改的限制:eventKind 被硬编码为 kEventHotKeyReleased
,而我同时需要 kEventHotKeyPressed
和 kEventHotKeyReleased
事件类型。
eventSpec.eventKind = kEventHotKeyReleased;
如果您想同时处理 Pressed 和 Released 事件,只需添加第二个 InstallEventHandler
调用来注册其他事件类型。
这是为 kEventHotKeyReleased
类型注册 "Command + R" 键的代码的完整示例。
import Carbon
extension String {
/// This converts string to UInt as a fourCharCode
public var fourCharCodeValue: Int {
var result: Int = 0
if let data = self.data(using: String.Encoding.macOSRoman) {
data.withUnsafeBytes({ (rawBytes) in
let bytes = rawBytes.bindMemory(to: UInt8.self)
for i in 0 ..< data.count {
result = result << 8 + Int(bytes[i])
}
})
}
return result
}
}
class HotkeySolution {
static
func getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags) -> UInt32 {
let flags = cocoaFlags.rawValue
var newFlags: Int = 0
if ((flags & NSEvent.ModifierFlags.control.rawValue) > 0) {
newFlags |= controlKey
}
if ((flags & NSEvent.ModifierFlags.command.rawValue) > 0) {
newFlags |= cmdKey
}
if ((flags & NSEvent.ModifierFlags.shift.rawValue) > 0) {
newFlags |= shiftKey;
}
if ((flags & NSEvent.ModifierFlags.option.rawValue) > 0) {
newFlags |= optionKey
}
if ((flags & NSEvent.ModifierFlags.capsLock.rawValue) > 0) {
newFlags |= alphaLock
}
return UInt32(newFlags);
}
static func register() {
var hotKeyRef: EventHotKeyRef?
let modifierFlags: UInt32 =
getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags.command)
let keyCode = kVK_ANSI_R
var gMyHotKeyID = EventHotKeyID()
gMyHotKeyID.id = UInt32(keyCode)
// Not sure what "swat" vs "htk1" do.
gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
// gMyHotKeyID.signature = OSType("htk1".fourCharCodeValue)
var eventType = EventTypeSpec()
eventType.eventClass = OSType(kEventClassKeyboard)
eventType.eventKind = OSType(kEventHotKeyReleased)
// Install handler.
InstallEventHandler(GetApplicationEventTarget(), {
(nextHanlder, theEvent, userData) -> OSStatus in
// var hkCom = EventHotKeyID()
// GetEventParameter(theEvent,
// EventParamName(kEventParamDirectObject),
// EventParamType(typeEventHotKeyID),
// nil,
// MemoryLayout<EventHotKeyID>.size,
// nil,
// &hkCom)
NSLog("Command + R Released!")
return noErr
/// Check that hkCom in indeed your hotkey ID and handle it.
}, 1, &eventType, nil, nil)
// Register hotkey.
let status = RegisterEventHotKey(UInt32(keyCode),
modifierFlags,
gMyHotKeyID,
GetApplicationEventTarget(),
0,
&hotKeyRef)
assert(status == noErr)
}
}
我维护 this Swift package 可以轻松地向您的应用程序添加全局键盘快捷键,也可以让用户设置自己的键盘快捷键。
import SwiftUI
import KeyboardShortcuts
// Declare the shortcut for strongly-typed access.
extension KeyboardShortcuts.Name {
static let toggleUnicornMode = Self("toggleUnicornMode")
}
@main
struct YourApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
// …
}
Settings {
SettingsScreen()
}
}
}
@MainActor
final class AppState: ObservableObject {
init() {
// Register the listener.
KeyboardShortcuts.onKeyUp(for: .toggleUnicornMode) { [self] in
isUnicornMode.toggle()
}
}
}
// Present a view where the user can set the shortcut they want.
struct SettingsScreen: View {
var body: some View {
Form {
HStack(alignment: .firstTextBaseline) {
Text("Toggle Unicorn Mode:")
KeyboardShortcuts.Recorder(for: .toggleUnicornMode)
}
}
}
}
本例使用的是SwiftUI,但也支持Cocoa.