Mac 鼠標(biāo)/鍵盤事件的監(jiān)聽和模擬


參考:
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)用 NSAppsendEvent: 方法發(fā)送消息到NSWindowNSWindow 再分發(fā)到 NSView 視圖對象,由其鼠標(biāo)或鍵盤事件響應(yīng)方法去處理。

EventDispatch
Apple event


事件響應(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)的類

NSResponderhttps://developer.apple.com/documentation/appkit/nsresponder

NSEventhttps://developer.apple.com/documentation/appkit/nsevent
NSEventTypehttps://developer.apple.com/documentation/appkit/nseventtype
NSEventModifierFlagshttps://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)行判斷~


各種‘kVK_’的對應(yīng)值如下:

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)事件,將枚舉值修改一下即可。



2.模擬鼠標(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)。



3.模擬鍵盤事件

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 燈也是不會有變化的。










goyohol's essay

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容