macOS 键盘按键 event 的三种截获方式

一般情况下,你不需要了解这些内容。

在极少数情况下,你的app可能需要去获取用户按下的按键信息,比如盗号木马 开发一款输入法。只有这样你才能给用户提供候选。

怎么在 macOS 下创建一个输入法,我在Swift 使用 InputMethodKit 写输入法这篇文章中有详细的说明,这里略过不提,我们重点放在如何处理用户按键,尤其是修饰按键的处理上。

落格输入法一直以来有一个不大不小的 bug,就是在某些输入框里,按 shift 按键是无法正确切换中英文的。说点专业的,就是说输入法在某些输入框下无法获取这个 shift 的按键 event

注意,输入是正常的,其他一切都是正常的。就是对于输入法来说,shift按键从来没按下过。

默认情况下,我们是继承   IMKInputController ,通过其中的 open override func handle(_ event: NSEvent!, client sender: Any!) -> Bool 来处理按键信息的,系统会自动调用这个函数给我们发来数据,通过返回的 Bool 来判断这个 event 是否要继续传递,一般的处理是没问题的,但对于修饰符,则会出现上文中的 bug。

当然,这个原因我想大家都能想的出来,就是我们获取按键信息的层级太高了,经过了层层传递才到我们的手中,这时候这个 event 已经被改了太多次,天知道它在哪里被拦截了,所以我们要想办法从更底层的地方去获取这个 event。

Event 传递的三个层级

在 macOS ,Event 传递经过了三个层级:

  • Cocoa/AppKit ——这是最高层级,也是我们默认在用的级别 NSEvent ;
  • Quartz 会把来自 IOKit 的 event 发送到各个 app,也就是 CGEvent ;
  • IOKit — 最底层,直接来自硬件。
插图来自 https://www.codesd.com/item/how-to-hang-up-remap-an-arbitrary-keyboard-event-on-osx.html

NSEvent

这个我们最常见,它里边封装了按键的代码、修饰符标识等等——最大的特点是 shift 不分左右(其他的也不分哈)。

一般来说,我们用它是足够的,比如判断一些组合键,判断用户输入了什么内容。但是对于上文的 bug,尤其是在一些游戏里,输入无法切换中英文,所以,我们需要寻求更底层的解决办法。

CGEvent

NSEvent  来自 CGEvent  的封装,后者更底层一些,在这一层你就可以得到区分左右的 shift 编码了,当然,这样一来  NSEvent.ModifierFlags 中的枚举类型就直接都不能用了,因为这里的编码不区分左右。这下你就需要自己去根据按键编码判断组合键了。

不过,对于我们的输入法来说,考虑到结构问题,我们并不在这一层获取所有的按键信息,只要获取组合键即可,这样一来,默认的数据不变,只要改变组合键的判断就行了,如此得到的另外一个收益就是可以区别对待每一个修饰按键了,都能区分左右——如果你确实需要的话。

首先,是在系统里添加一个监听,这个要写在你的  AppDelegate 里,确保一旦程序启动就执行:

这里我们设定了只监听  flagsChanged 的 event

然后是供回调的闭包函数:

值得注意的是由于这里调用了 C 函数,所以这个闭包不能捕获上下文(这里我就写成函数了,因为内容比较多),同时,也是同样的原因,这个函数必须在 Swift 下是全局函数,即写在所有的大括号外边即可。

这里还要注意高亮的一行,我们用通知中心把消息传递给输入法,由于输入法类的实例生存周期不确定(一般来说实际上是一个输入框就有一个实例,每次输入框获得焦点都会重新生成一个实例但旧实例不会立即释放),我们无法直接调用输入法的实例来实现参数的传递。

IMKInputController 类中,有一对监控输入法进行输入还是结束输入的回调,我们利用这个回调来实现监听的接收和释放,避免多个输入法实例同时监听导致多线程资源竞用:

这样一来,每次进入输入模式注册监听,离开输入模式则注销监听。至于哪个实例来监听,这是由系统保证的,除了我们的修饰符外,其他的按钮也是依旧由系统保证什么时侯发送给哪个实例,一切都完美了。

 

最后,补上通知监听回调的处理:

这里有一个 GitHub 仓库,里边是用 Objective-C 实现的键盘按键监听,供你参考:/keylogger

IOKit

这个框架可能对一些驱动开发和串口开发的开发者来说会很熟悉,这是苹果最低层的框架,你可以不用触及内核而直接与接入 macOS 的外设硬件进行交互,对于我们来说,这一层太过深奥,用 Swift 调用这一层的内容也不是很舒服。

总之,如果你想了解更多,这里有一个 GitHub 仓库,/Swift-Keylogger这个项目使用 Swift 4 构建,直接使用最低层的 IOKit 与 HID 设备沟通,供你参考。

 

参考文献

How to hang up / remap an arbitrary keyboard event on OSX?

发表评论

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