參考:
《macOS AppKit 的事件響應(yīng)簡介》
《Mac OSX 鼠標(biāo)鍵盤事件的監(jiān)聽和模擬》
事件分發(fā)機制:
在 macOS 系統(tǒng)中鼠標(biāo)、鍵盤和觸摸板的活動事件都會產(chǎn)生底層的系統(tǒng)事件,首先傳遞到 IOKit 框架處理后存儲到隊列中,通知 Window Server 服務(wù)層處理。Window Server 存儲到 FIFO 優(yōu)先隊列中,然后逐一轉(zhuǎn)發(fā)到當(dāng)前活動窗口或者能響應(yīng)這個事件的應(yīng)用程序去處理。
在 macOS 或者 iOS 程序中,都會有一個 Main Run Loop 的線程,RunLoop 循環(huán)中會遍歷 event 消息隊列,逐一分發(fā)這些事件到應(yīng)用中合適的對象去處理。具體來說就是調(diào)用 NSApp 的 sendEvent: 方法發(fā)送消息到NSWindow,NSWindow 再分發(fā)到 NSView 視圖對象,由其鼠標(biāo)或鍵盤事件響應(yīng)方法去處理。


事件響應(yīng)鏈:
響應(yīng)者鏈?zhǔn)?Application Kit 事件處理架構(gòu)的中心機制,由一系列鏈接在一起的響應(yīng)者對象組成,事件或者動作消息可以沿著這些對象進(jìn)行傳遞。消息沿著響應(yīng)者鏈向上、向更高級別的對象傳遞,直到最終被處理(如果最終還是沒有被處理,就會被拋棄)。
事件響應(yīng)者 Responders 類為核心應(yīng)用程序架構(gòu)的三個主要模式或機制定義了一個接口:
- 它聲明了一些處理事件消息(也就是源自用戶事件的消息,比如鼠標(biāo)點擊或按鍵按下這樣的事件)的方法。
- 它聲明了數(shù)十個處理動作消息的方法,它們和標(biāo)準(zhǔn)的鍵綁定(比如那些在文本內(nèi)部移動插入點的綁定)密切相關(guān)。動作消息會被派發(fā)到目標(biāo)對象;如果目標(biāo)沒有被指定,應(yīng)用程序會負(fù)責(zé)檢索合適的響應(yīng)者。
- 它定義了一套在應(yīng)用程序中指派和管理響應(yīng)者的方法。這些響應(yīng)者組成了我們所知道的響應(yīng)者鏈,即一系列響應(yīng)者,事件或動作消息在它們之間傳遞,直到找到能夠?qū)λ鼈冞M(jìn)行處理的對象。
從層級上看離觀察者最近的視圖優(yōu)先響應(yīng)事件,通過 view 的 hitTest 方法檢測,滿足 hitTest 方法的的子視圖優(yōu)先響應(yīng)事件。
NSApplication, NSWindow, NSDrawer, NSWindowController, NSView 以及繼承于 NSView 的所有控件對象都直接或間接繼承了 Responders 類,所以這些類都能處理鼠標(biāo)和鍵盤事件。
相關(guān)的類
NSResponder:https://developer.apple.com/documentation/appkit/nsresponder
NSEvent:https://developer.apple.com/documentation/appkit/nsevent
NSEventType:https://developer.apple.com/documentation/appkit/nseventtype
NSEventModifierFlags:https://developer.apple.com/documentation/appkit/nseventmodifierflags/
事件的監(jiān)聽方法
《Mac OSX 鼠標(biāo)鍵盤事件的監(jiān)聽和模擬》中提到:鼠標(biāo)/鍵盤事件的監(jiān)聽有多種方法,第一種方法是重寫事件響應(yīng)者 Responders 對應(yīng)的方法來獲取對應(yīng)的事件;第二是通過重寫 NSWindow 的 sendEvent: 方法; 第三是通過的 NSEvent 提供靜態(tài)方法來監(jiān)聽對應(yīng)的事件~
沒有逐一去試驗,如下鍵盤事件/鼠標(biāo)事件只是各用一種方式實現(xiàn)了相應(yīng)監(jiān)聽!
- [A].鍵盤事件的監(jiān)聽——通過的
NSEvent提供靜態(tài)方法來監(jiān)聽對應(yīng)的事件!
NSEvent提供的靜態(tài)方法可以用監(jiān)聽整個系統(tǒng)的事件或者當(dāng)前應(yīng)用程序內(nèi)的事件。
+ (nullable id)addGlobalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(void (^)(NSEvent*))block`
+ (nullable id)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(NSEvent* __nullable (^)(NSEvent*))block
+ (void)removeMonitor:(id)eventMonitor
Swift實現(xiàn)代碼:(開啟對鍵盤的監(jiān)聽,并書寫其響應(yīng)方法)
//開啟對鍵盤的監(jiān)聽 NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.flagsChanged) { self.flagsChanged(with: $0) return $0 } NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.keyDown) { self.keyDown(with: $0) return $0 } NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.keyUp) { self.keyUp(with: $0) return $0 }鍵盤事件的響應(yīng)方法:
//MARK:KeyBoard鍵盤的響應(yīng) override func keyUp(with event: NSEvent) { //鍵盤抬起:(含)普通按鍵Key——可一直輸入的Key按鍵 } override func keyDown(with event: NSEvent) {//鍵盤按下:(含)普通按鍵Key——可一直輸入的Key按鍵 } override func flagsChanged(with event: NSEvent) {//按鍵變化:(僅有)特殊的功能控制鍵Key——shift、control、option、option及相互組合 }
- [B].鼠標(biāo)事件的監(jiān)聽——通過使用重寫 Responders 的方法來監(jiān)聽鼠標(biāo)事件:
鼠標(biāo)的事件類型:
1.左/右鍵的按下與抬起事件
2.左鍵的雙擊(或者多擊事件)——clickCount屬性
3.鼠標(biāo)移動事件
4.左鍵或者右鍵的拖拽事件
5.鼠標(biāo)的滾動事件
使用如下重寫 Responders 的方法來監(jiān)聽鼠標(biāo)事件:
- (void)mouseDown:(NSEvent *)event;
- (void)rightMouseDown:(NSEvent *)event;
- (void)mouseUp:(NSEvent *)event;
- (void)rightMouseUp:(NSEvent *)event;
- (void)mouseMoved:(NSEvent *)event;
- (void)mouseDragged:(NSEvent *)event;
- (void)rightMouseDragged:(NSEvent *)event;
- (void)scrollWheel:(NSEvent *)event;
Swift實現(xiàn)代碼:(直接重寫 其響應(yīng)方法)
//MARK:Mouse鼠標(biāo)的響應(yīng) override func mouseDown(with event: NSEvent) { } override func rightMouseDown(with event: NSEvent) { } override func mouseUp(with event: NSEvent) { } override func rightMouseUp(with event: NSEvent) { } override func mouseMoved(with event: NSEvent) { } override func mouseDragged(with event: NSEvent) { } override func rightMouseDragged(with event: NSEvent) { } override func scrollWheel(with event: NSEvent) { }
使用例子??:(在'ViewController.swift'文件中)
import Cocoa
class ViewController: NSViewController,NSWindowDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//開啟對鍵盤的監(jiān)聽
NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.flagsChanged) {
self.flagsChanged(with: $0)
return $0
}
NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.keyDown) {
self.keyDown(with: $0)
return $0
}
NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.keyUp) {
self.keyUp(with: $0)
return $0
}
}
//MARK:KeyBoard鍵盤事件的響應(yīng)
override func keyUp(with event: NSEvent) { //鍵盤抬起:(含)普通按鍵Key——可一直輸入的Key按鍵
let keyCode = event .keyCode //類型:CUnsignedShort即UInt16
print("keyUp-> keyCode:\(keyCode) event.characters:\(event.characters as Any)")
}
override func keyDown(with event: NSEvent) {//鍵盤按下:(含)普通按鍵Key——可一直輸入的Key按鍵
let keyCode = event .keyCode //類型:CUnsignedShort即UInt16
print("keyCode:\(keyCode) event.characters:\(event.characters as Any)")
//根據(jù) 對應(yīng)的`event .keyCode`數(shù)值和`event.characters`字符串,來進(jìn)行相應(yīng)操作
if keyCode == 53 {//點擊了'Esc'按鍵
print("press 'Esc' key")
}
//處理“特殊的功能控制按鍵Key+普通按鍵Key”按鍵組合
switch event.modifierFlags.intersection(NSEvent.ModifierFlags.deviceIndependentFlagsMask) {
case [.command] where event.characters == "l", [.command, .shift] where event.characters == "l":
print("command-l or command-shift-l")
default:
break
}
}
override func flagsChanged(with event: NSEvent) {//按鍵變化:(僅有)特殊的功能控制按鍵Key——shift、control、option、option及相互組合 NSEventModifierFlags
print("flagsChanged->", event.modifierFlags.intersection(NSEvent.ModifierFlags.deviceIndependentFlagsMask))
switch event.modifierFlags.intersection(NSEvent.ModifierFlags.deviceIndependentFlagsMask) {
case [.shift]:
print("shift key is pressed")
case [.control]:
print("control key is pressed")
case [.command]:
print("command key is pressed")
case [.option]:
print("option key is pressed")
case [.control, .shift]:
print("control-shift keys are pressed")
case [.control, .command]:
print("control-command keys are pressed")
case [.control, .option]:
print("control-option keys are pressed")
case [.command, .shift]:
print("command-shift keys are pressed")
case [.option, .shift]:
print("option-shift keys are pressed")
case [.option, .command]:
print("option-command keys are pressed")
case [.shift, .control, .command]:
print("shift-control-command keys are pressed")
case [.shift, .control, .option]:
print("shift-control-option keys are pressed")
case [.shift, .command, .option]:
print("shift-command-option keys are pressed")
case [.control, .option, .command]:
print("control-option-command keys are pressed")
case [.shift, .control, .option, .command]:
print("shift-control-option-command keys are pressed")
default://抬手時也會響應(yīng)——NSEventModifierFlags(rawValue: 0)
break //print("no modifier keys are pressed")//?
}
}
//MARK:Mouse鼠標(biāo)事件的響應(yīng)
override func mouseDown(with event: NSEvent) {
//event.type——判斷鼠標(biāo)的操作、event.locationInWindow——獲取鼠標(biāo)的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue為1
}
override func mouseUp(with event: NSEvent) {
//event.type——判斷鼠標(biāo)的操作、event.locationInWindow——獲取鼠標(biāo)的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue為2
}
override func rightMouseDown(with event: NSEvent) {
//event.type——判斷鼠標(biāo)的操作、event.locationInWindow——獲取鼠標(biāo)的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue為3
}
override func rightMouseUp(with event: NSEvent) {
//event.type——判斷鼠標(biāo)的操作、event.locationInWindow——獲取鼠標(biāo)的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue為4
}
override func mouseMoved(with event: NSEvent) {
//event.type——判斷鼠標(biāo)的操作、event.locationInWindow——獲取鼠標(biāo)的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue為5
}
override func mouseDragged(with event: NSEvent) {
//event.type——判斷鼠標(biāo)的操作、event.locationInWindow——獲取鼠標(biāo)的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue為6
}
override func rightMouseDragged(with event: NSEvent) {
//event.type——判斷鼠標(biāo)的操作、event.locationInWindow——獲取鼠標(biāo)的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue為7
}
override func scrollWheel(with event: NSEvent) {
//event.type——判斷鼠標(biāo)的操作、event.locationInWindow——獲取鼠標(biāo)的位置
let eventType = event.type
let locPoint = event.locationInWindow
print("eventType:\(eventType.rawValue),locPoint:\(locPoint)")//eventType.rawValue為22
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
鍵盤事件的響應(yīng):其中keyUp方法和keyDown方法——點擊時只要含有普通按鍵就會響應(yīng)、flagsChanged方法——只響應(yīng) 特殊的功能控制按鍵!
鼠標(biāo)事件的響應(yīng):在鼠標(biāo)事件的方法中,通過event.type——判斷鼠標(biāo)進(jìn)行的相應(yīng)操作、event.locationInWindow——獲取鼠標(biāo)的位置~
Tips:在代碼中引入“
Carbon.HIToolbox”(OC中:“Carbon/HIToolbox/Events.h”):import Carbon.HIToolbox//OC中:Carbon/HIToolbox/Events.h就可以‘kVK_’的對應(yīng)值更直觀來進(jìn)行判斷:
可將
if keyCode == 53 {//點擊了'Esc'按鍵 print("press 'Esc' key") }替換為
if keyCode == kVK_Escape {//點擊了'Esc'按鍵 print("press 'Esc' key") }來進(jìn)行判斷~
Tips:要響應(yīng)鼠標(biāo)的
mouseEntered、mouseExited、mouseMoved回調(diào)方法,需要為對應(yīng)的NSView實例添加上NSTrackingArea(監(jiān)視區(qū)域)~請參考《NSTrackingArea(監(jiān)視區(qū)域)監(jiān)聽鼠標(biāo)的移入/內(nèi)部移動/移出事件》
模擬事件 (C語言方式)
1.模擬鼠標(biāo)事件:
void PostMouseEvent(CGMouseButton button, CGEventType type, const CGPoint &point, int64_t clickCount)
{
CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStatePrivate);
CGEventRef theEvent = CGEventCreateMouseEvent(source, type, point, button);
CGEventSetIntegerValueField(theEvent, kCGMouseEventClickState, clickCount);
CGEventSetType(theEvent, type);
CGEventPost(kCGHIDEventTap, theEvent);
CFRelease(theEvent);
CFRelease(source);
}
左鍵單擊模擬:
PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 1); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 1);左鍵雙擊模擬:
PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 1); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 1); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 2); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 2);拖拽事件:
如果是拖拽事件,例如左鍵拖拽事件,則需要先發(fā)送左鍵的kCGEventLeftMouseDown事件,然后連續(xù)發(fā)送kCGEventLeftMouseDragged事件,再發(fā)送kCGEventLeftMouseUp事件,代碼如下:PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDown, CGPointZero, 1); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDragged, CGPointZero, 1); ... PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseDragged, CGPointZero, 1); PostMouseEvent(kCGMouseButtonLeft, kCGEventLeftMouseUp, CGPointZero, 1);模擬其他鼠標(biāo)事件,將枚舉值修改一下即可。
void PostScrollWheelEvent(int32_t scrollingDeltaX, int32_t scrollingDeltaY)
{
CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStatePrivate);
CGEventRef theEvent = CGEventCreateScrollWheelEvent(source, kCGScrollEventUnitPixel, 2, scrollingDeltaY, scrollingDeltaX);
CGEventPost(kCGHIDEventTap, theEvent);
CFRelease(theEvent);
CFRelease(source);
}
鼠標(biāo)滾輪事件只要傳入水平和垂直方向的偏移即可實現(xiàn)。
void PostKeyboardEvent(CGKeyCode virtualKey, bool keyDown, CGEventFlags flags)
{
CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStatePrivate);
CGEventRef push = CGEventCreateKeyboardEvent(source, virtualKey, keyDown);
CGEventSetFlags(push, flags);
CGEventPost(kCGHIDEventTap, push);
CFRelease(push);
CFRelease(source);
}
鍵盤事件的模擬需要注意的就是 CGEventFlags flags 參數(shù),該參數(shù)用來模擬組合鍵的實現(xiàn),類型定義如下:
kCGEventFlagMaskAlphaShift:大小寫鎖定鍵是否處于開啟狀態(tài)
kCGEventFlagMaskShift:Shift 鍵是否按下
kCGEventFlagMaskControl:Control 鍵是否按下
kCGEventFlagMaskAlternate:Alt 鍵是否按下,對應(yīng) Mac 鍵盤的 option 鍵
kCGEventFlagMaskCommand:Command 鍵是否按下,對應(yīng) Windows 的 WIN 鍵
kCGEventFlagMaskHelp:Help 鍵
kCGEventFlagMaskSecondaryFn:Fn 鍵
kCGEventFlagMaskNumericPad:數(shù)字鍵盤
kCGEventFlagMaskNonCoalesced:沒有任何鍵按下
如果有多個控制鍵同時按下,則使用位運算的或 | 加上對應(yīng)的鍵值即可。例如模擬 Command + Control + S:
PostKeyboardEvent(kVK_ANSI_S, true, kCGEventFlagMaskCommand | kCGEventFlagMaskControl)
PostKeyboardEvent(kVK_ANSI_S, false, kCGEventFlagMaskNonCoalesced)
注意:大小寫鎖定鍵,無法通過
kVK_CapsLock按鍵的按下和抬起事件來模擬大小鍵的鎖定,同時按鍵上的 LED 燈也是不會有變化的。
