【问题标题】:How to listen to global hotkeys with Swift in a macOS app?如何在 macOS 应用程序中使用 Swift 收听全局热键?
【发布时间】:2015-02-02 15:58:22
【问题描述】:

我试图在我的 Mac OS X 应用程序中使用 Swift 编写一个处理程序,用于全局(系统范围)热键组合,但我找不到合适的文档。我已经读到我不得不为它使用一些遗留的 Carbon API,没有更好的方法吗?你能给我看一些概念证明 Swift 代码吗?提前致谢!

【问题讨论】:

  • 你到底尝试了什么?热键是什么意思?你试过 NSEvent 全局监视器还是 CGEventTap

标签: swift macos cocoa keyboard-shortcuts


【解决方案1】:

从 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 4 中不起作用,如果没有更详细的解释,它是不可能实现的。
  • 在 Swift 4 中运行良好,我已将我的项目转换为 Swift 4 并且运行良好。作为开发人员,您应该知道“不起作用”是对问题最糟糕的描述——“不起作用”是什么意思?它不编译?你得到什么编译错误?真的没什么好解释的 - 每个使用的功能都由 Apple 记录。
  • 只有良好的氛围,查理!如果您也可以分享您的 Swift 4 解决方案,那就太好了。您发布的解决方案不完整,这使初学者难以适应。有一件事会有所帮助,那就是导入 Carbon 是必要的。我面临的下一个问题是String 没有.fourCharCodeValue 属性。如果您能提供keyCodemodifierKeys 的示例,那就太好了。
  • @ixany - 抱歉,如果这听起来充满敌意 - 根本不是故意的。 fourCharCodeValue 将字符串转换为 Int - 它在过去被大量使用 - 4 个 ASCII 字符转换为 32 位整数 - 它比随机整数更容易在转储中识别。你可以在这里找到我的实现:github.com/charlieMonroe/XUCore/blob/master/XUCore/additions/… - 我目前正在从SRRecorderControl 检索keyCodegist.github.com/charlieMonroe/0923985c704695b252ed9bb879e7f99d
  • 我已经完成了我的功课,并让这个解决方案与 Swift 5 一起使用。请参阅 here
【解决方案2】:

我不相信你今天可以在 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:) 添加本地处理程序。

【讨论】:

  • 虽然它可用,但重要的是要记住,用户授予您的应用程序监控每个按键的能力,这具有潜在的危险,主要是如果您的应用程序没有沙盒化并且可以注入。旧的 Carbon API 仅针对用户定义的组合执行此操作,使它们成为“更安全”的解决方案。
  • InstallEventHandler() 在 Swift 中运行良好(至少从 Swift 3 开始)。我正在使用DDHotKeyCenter 的 Swift 版本
【解决方案3】:

设置的快速 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)

【讨论】:

  • 注意:基于 Carbon 的 Hotkey API 不需要访问权限(不能在 Mac App Store 上),尽管这确实更容易实现。
  • 有像 CopyLess 2 这样的应用程序可以监听 keyDown 并且不需要辅助功能,你知道吗?也许商店的应用不需要它?
【解决方案4】:

以下代码适用于 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)
  }
}

【讨论】:

    【解决方案5】:

    查看热键库。您可以简单地使用 Carthage 将其实现到您自己的应用程序中。 HotKey Library

    【讨论】:

      【解决方案6】:

      如果您的应用有 Menu:

      • 添加一个新的MenuItem(可以将其称为“热键虚拟对象”)
      • 在属性检查器中,在Key Equivalent 字段中方便地输入您的热键
      • Allowed when HiddenEnabledHidden 设置为true
      • 将其与 IBAction 链接以执行您的热键应该执行的任何操作

      完成!

      【讨论】:

      • 有趣的方法,但它不适用于仅存在于菜单栏中的应用程序:要检测键盘快捷键,必须以某种方式激活应用程序,例如通过在菜单中单击它吧。
      【解决方案7】:

      我维护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。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-11-22
        • 2010-09-23
        • 2023-03-31
        • 2016-07-12
        • 1970-01-01
        • 2013-06-12
        • 2012-08-02
        • 1970-01-01
        相关资源
        最近更新 更多