一般情況下,你不需要了解這些內容。
在極少數情況下,你的app可能需要去獲取用戶按下的按鍵信息,比如盜號木馬 開發一款輸入法。只有這樣你才能給用戶提供候選。
怎麼在 macOS 下創建一個輸入法,我在Swift 使用 InputMethodKit 寫輸入法這篇文章中有詳細的說明,這里略過不提,我們重點放在如何處理用戶按鍵,尤其是修飾按鍵的處理上。
落格輸入法一直以來有一個不大不小的 bug,就是在某些輸入框裡,按 shift 按鍵是無法正確切換中英文的。說點專業的,就是說輸入法在某些輸入框下無法獲取這個 shift 的按鍵 event。
注意,輸入是正常的,其他一切都是正常的。就是對於輸入法來說,shift按鍵從來沒按下過。
默認情況下,我們是繼承 IMKInputController ,通過其中的 打開 覆蓋 FUNC 處理(_ 事件: NSEvent!, 客戶 寄件人: Any!) -> 布爾 來處理按鍵信息的,系統會自動調用這個函數給我們發來數據,通過返回的 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 的事件
然後是供回調的閉包函數:
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 實現的鍵盤按鍵監聽,供你參考:/鍵盤記錄
由於IOKit
這個框架可能對一些驅動開發和串口開發的開發者來說會很熟悉,這是蘋果最低層的框架,你可以不用觸及內核而直接與接入 macOS 的外設硬件進行交互,對於我們來說,這一層太過深奧,用 Swift 調用這一層的內容也不是很舒服。
總之,如果你想了解更多,這裡有一個 GitHub 倉庫,/斯威夫特 - 鍵盤記錄這個項目使用 Swift 4 構建,直接使用最低層的 IOKit 與 HID 設備溝通,供你參考。
參考文獻
本文由 落格博客 原創撰寫:落格博客 » macOS 鍵盤按鍵 event 的三種截獲方式
轉載請保留出處和原文鏈接:https://www.logcg.com/archives/2902.html
為什麼這個只能獲取到功能鍵(命令/選項…)的 而字母和數字不能獲取到
不清楚,我後來放棄了,並沒有用這個接口,因為這樣會導致按鍵shift和字母並發並發生錯序……