一般情况下,你不需要了解这些内容。
在极少数情况下,你的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 — 最底层,直接来自硬件。
NSEvent
这个我们最常见,它里边封装了按键的代码、修饰符标识等等——最大的特点是 shift 不分左右(其他的也不分哈)。
一般来说,我们用它是足够的,比如判断一些组合键,判断用户输入了什么内容。但是对于上文的 bug,尤其是在一些游戏里,输入无法切换中英文,所以,我们需要寻求更底层的解决办法。
CGEvent
NSEvent 来自 CGEvent 的封装,后者更底层一些,在这一层你就可以得到区分左右的 shift 编码了,当然,这样一来 NSEvent.ModifierFlags 中的枚举类型就直接都不能用了,因为这里的编码不区分左右。这下你就需要自己去根据按键编码判断组合键了。
不过,对于我们的输入法来说,考虑到结构问题,我们并不在这一层获取所有的按键信息,只要获取组合键即可,这样一来,默认的数据不变,只要改变组合键的判断就行了,如此得到的另外一个收益就是可以区别对待每一个修饰按键了,都能区分左右——如果你确实需要的话。
首先,是在系统里添加一个监听,这个要写在你的 AppDelegate 里,确保一旦程序启动就执行:
1 2 3 4 5 6 7 8 |
let eventTap:CFMachPort = CGEvent.tapCreate(tap: .cghidEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: 1 << CGEventType.flagsChanged.rawValue, callback: myCGEventCallback, userInfo: nil)! let runLoopSource:CFRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0); CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, CFRunLoopMode.commonModes); CGEvent.tapEnable(tap: eventTap, enable: true); CFRunLoopRun(); |
这里我们设定了只监听 flagsChanged 的 event
然后是供回调的闭包函数:
1 2 3 4 5 6 7 8 9 10 11 12 |
func myCGEventCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? { if (type != .keyDown) { if (type != .keyUp) { if (type != .flagsChanged) { return Unmanaged.passRetained(event) } } } NotificationCenter.default.post(name: NSNotification.Name("modifier"), object: nil) return Unmanaged.passRetained(event) } |
值得注意的是由于这里调用了 C 函数,所以这个闭包不能捕获上下文(这里我就写成函数了,因为内容比较多),同时,也是同样的原因,这个函数必须在 Swift 下是全局函数,即写在所有的大括号外边即可。
这里还要注意高亮的一行,我们用通知中心把消息传递给输入法,由于输入法类的实例生存周期不确定(一般来说实际上是一个输入框就有一个实例,每次输入框获得焦点都会重新生成一个实例但旧实例不会立即释放),我们无法直接调用输入法的实例来实现参数的传递。
在 IMKInputController 类中,有一对监控输入法进行输入还是结束输入的回调,我们利用这个回调来实现监听的接收和释放,避免多个输入法实例同时监听导致多线程资源竞用:
1 2 3 4 5 6 7 8 9 10 |
open override func activateServer(_ sender: Any!) { currentClient = sender as? IMKTextInput candidatesWindow.imkDelegate = self NotificationCenter.default.addObserver(self, selector: #selector(getModifier(_:)), name: NSNotification.Name("modifier"), object: nil) } open override func deactivateServer(_ sender: Any!) { print("!!!deactive!!!") NotificationCenter.default.removeObserver(self) } |
这样一来,每次进入输入模式注册监听,离开输入模式则注销监听。至于哪个实例来监听,这是由系统保证的,除了我们的修饰符外,其他的按钮也是依旧由系统保证什么时侯发送给哪个实例,一切都完美了。
最后,补上通知监听回调的处理:
1 2 3 4 |
@objc func getModifier(_ noti:Notification) { let a = noti.object as! NSEvent let _ = self.handle(a, client: currentClient) } |
这里有一个 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?
本文由 落格博客 原创撰写:落格博客 » macOS 键盘按键 event 的三种截获方式
转载请保留出处和原文链接:https://www.logcg.com/archives/2902.html
为什么这个只能获取到功能键(command/option…)的 而字母和数字不能获取到
不清楚,我后来放弃了,并没有用这个接口,因为这样会导致按键shift和字母并发并发生错序……