【问题标题】:macOS App: handling key combinations bound to global keyboard shortcutsmacOS App:处理绑定到全局键盘快捷键的组合键
【发布时间】:2025-12-11 03:35:01
【问题描述】:

在某些应用程序中,应用程序直接处理键盘快捷键是有意义的,否则这些快捷键会绑定到系统范围的组合。例如,⌘-Space(通常是 Spotlight)或 ⌘-Tab(通常是应用切换器)。这适用于各种 Mac 应用程序,例如 VMWare Fusion、Apple 自己的屏幕共享和远程桌面客户端(分别将事件转发到 VM 或服务器,而不是在本地处理它们),以及应用程序中的一些类似的第三方应用程序商店。

我们想在我们正在开发的应用程序中实现这样的模式,但很难弄清楚如何去做。我应该指出,有问题的应用程序是一个常规的前台应用程序,是沙盒的,任何解决方案都必须符合 App Store 规则。商店中的其他应用程序可以做到这一点意味着这一定是可能的。

明确地说,我们想要:

  • 检测并处理所有按键,包括绑定到全局快捷键的按键。
  • 防止全局快捷方式触发其全局绑定效果。

Apple 的Event Architecture document 建议前台应用程序应该已经接收到这些事件。 (它只讨论处理诸如电源和弹出按钮之类的早期级别,这很好。)它继续建议,key events document also implies NSApplicationsendEvent: 方法是基于检测潜在快捷方式的方法修饰符标志,将它们发送到窗口,如果失败,则发送到菜单栏。它没有明确说明全局绑定快捷方式会发生什么。

我尝试继承 NSApplication 并覆盖 sendEvent:。无论我是否将所有事件传递给超类实现,或者如果我说过滤修饰键事件,当我按下⌘-空格时,我都会收到按下和释放命令 (⌘) 键的事件,而不是空格键。 Spotlight UI 总是弹出。

我没有找到太多关于继承 NSApplication 及其早期事件处理的信息,来自 Apple 或其他公司。我似乎无法找出检测和处理全局快捷方式的级别。

有人可以指点我正确的方向吗?

不起作用的可能解决方案:

我在其他 Stack Overflow 帖子中看到的建议,但不适用于我见过的其他应用程序(并且会违反 App Store 规则):

  • 可访问性 API(需要特殊权限)
  • 事件点击/挂钩(需要以 root 身份运行)

无论如何,这两种方法都太过分了,因为它们可以让你在所有时间拦截所有事件,而不仅仅是在你的应用程序是前台应用程序时。

NSeventaddGlobalMonitorForEventsMatchingMask:handler: 同时不会阻止全局快捷方式处理程序为这些事件触发,所以我什至没有费心去尝试。

【问题讨论】:

  • Carbon 框架的RegisterEventHotKey 是否违反了 AppStore 规则?它至少在 10.12 Sierra 中有效。
  • App Store 中有哪些第三方应用可以做到这一点?
  • @TheNextman 例如,Remotix VNC & RDP 可以通过系统范围的快捷方式进入被控制的系统,就像苹果自己的屏幕共享一样。

标签: macos cocoa keyboard-shortcuts keyboard-events nsevent


【解决方案1】:

好的,所以 Cocoa 事件方法和 Quartz 事件点击已退出,因为它们要么需要 root 或可访问性访问,要么在停靠之前不捕获事件。

Carbon 的 PushSymbolicHotKeyMode 已退出,因为根据文档,它需要可访问性访问。

Carbon 的 RegisterEventHotKey 可能已退出,因为 Apple 似乎不允许这样做(请参阅我对问题的评论中的链接)。然而,即便如此,我测试了,你不能用它来捕捉Command+Tab。

我对这个如何工作做了一个快速的概念验证,但是 YMMV:

  • 从这个answer 实现KeyboardWatcher 示例类。您需要关联 IOKit。
  • 添加硬件 - USB (com.apple.security.device.usb) 沙盒授权。这是必要的,因为 KeyboardWatcher 使用 HID 来捕捉按键
  • Handle_DeviceEventCallback 将为您提供按下的键。您显然可以根据自己的需要进行修改
  • 使用SetSystemUIMode 阻止任务切换器和Spotlight。您需要关联 Carbon。

SetSystemUIMode(kUIModeContentSuppressed, kUIOptionDisableProcessSwitch);

请注意,这仅在您的应用处于前台时才有效(可能是您想要的)。我使用跟踪矩形在我的视图上设置了它,因此它仅在鼠标悬停在我的视图上时生效(如在 Remotix 中):

- (void)viewDidLoad {
[super viewDidLoad];

NSTrackingArea* trackingArea = [[NSTrackingArea alloc] initWithRect:[self.view bounds] options: (NSTrackingMouseEnteredAndExited | 

NSTrackingActiveAlways) owner:self userInfo:nil];
    [self.view addTrackingArea:trackingArea];
}

- (void) mouseEntered:(NSEvent*)theEvent {
    SetSystemUIMode(kUIModeContentSuppressed, kUIOptionDisableProcessSwitch);
}

- (void) mouseExited:(NSEvent*)theEvent {
    SetSystemUIMode(kUIModeNormal, 0);
}

Remotix 似乎链接了 Carbon 和 IOKit,但我看不到它们是否具有 USB 权利(我尝试了演示,而不是 App Store 版本)。他们可能正在做这样的事情。


实现此目的的常规方法是安装 Quartz 事件水龙头。但是,要接收针对其他应用程序的事件,您需要(如您所说)是 root,或者为您的应用程序启用可访问性访问。

目前的沙盒规则似乎无法使用事件点击。这在developer forum 中得到证实。该链接仅用于登录,但要引用线程:

是否有机会通过阻止启动 iTunes 来处理来自媒体键的事件。在沙盒之前,可以通过创建 CGEventTap,但现在沙盒拒绝使用 hid-controll。

不,目前这在 App Sandbox 中是不可能的。

我不确定其他方法可以做到这一点;我很想知道 App Store 中的哪些应用可以?

VMWare Fusion 显然没有沙盒化,Apple 自己的应用程序不受规则约束。请记住,沙盒仅在 2012 年推出后添加的新应用程序上强制执行。在该日期之前添加的应用程序不强制执行沙盒。看到这个answer

【讨论】:

  • App Store 上的 Remotix 应用最后一次更新是在 2016 年,假设我正确读取了 codesign -d --entitlements - /Applications/Remotix.app 的输出,它确实是沙盒的。 (<key>com.apple.security.app-sandbox</key> <true/>)
  • @pmdj 对,但它是什么时候添加到 App Store 的?它似乎已经存在了相当长的一段时间 (web.archive.org/web/20120520143615/https://www.nulana.com/…),它与应用程序何时更新无关,而与它何时添加有关。旧的应用程序是祖父的,不需要沙盒。或者他们有可能获得 Apple 的沙盒豁免 - 请参阅我的答案中的链接。
  • 但如果它具有沙盒权利,那么它肯定是沙盒吗?另请注意,它不需要管理员访问权限,据我所知,它不链接到石英事件点击功能。
  • @pmdj 为答案添加了一个潜在的解决方案
【解决方案2】:

我很久以前就解决了这个问题,但我只是注意到我从未在此处发布过。答案最终涉及CGSSetGlobalHotKeyOperatingMode()。这不是一个公共 API,但有许多 Mac App Store 应用程序通过混淆函数名称并动态查找它来使用它。苹果似乎并不介意。 API 使用起来非常简单,并且有大量的开源示例源代码浮动。

【讨论】:

  • 你能描述一下解决方案或分享一个使用它的源代码(swift)
  • @bhagyashingale 对不起,我不会很快,所以我不能给你一个明确的答案,但如果你能以某种方式导入声明函数的 C 头文件(GitHub上有一些)应该够了。
【解决方案3】:

对于寻求全屏应用解决方案的其他人,或者如果您愿意接管全屏,您可以使用:CGDisplayCapture

这将导致您的应用捕获所有键盘输入,甚至阻止使用键盘调用 Spotlight 和应用切换。

import Quartz

// disable keyboard events for all apps, except yours
CGDisplayCapture(CGMainDisplayID())

// reenable keyboard events for other apps
CGDisplayRelease(CGMainDisplayID())

注意:在显示释放之前,应用不会收到窗口/应用程序活动/退出事件。因此,也许您可​​以在应用程序处于活动状态时使用鼠标跟踪来释放显示。此外,甚至屏幕保护程序/锁定屏幕也会受到影响。确保根据需要停用捕获。

参考:

【讨论】: