如何在 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 HiddenEnabledHidden 设置为 true
  • link 它与 IBAction 一起执行您的热键应该执行的任何操作

完成!

以下代码适用于 Swift 5.0.1。此解决方案结合了 Charlie Monroe 接受的答案和 Rob Napier 推荐使用 DDHotKey 的解决方案。

DDHotKey 似乎开箱即用,但它有一个我必须更改的限制:eventKind 被硬编码为 kEventHotKeyReleased,而我同时需要 kEventHotKeyPressedkEventHotKeyReleased 事件类型。

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.