本文轉自:http://www.macdev.io/ebook/event.html
- 事件分發過程
OSX 與用戶交互的主要外設是鼠標,鍵盤。鼠標鍵盤的活動會產生底層系統事件。這個事件首先傳遞到IOKit框架處理后存儲到隊列,通知Window Server服務層處理。Window Server存儲到FIFO先進先出隊列中,逐一轉發到當前的活動窗口或能響應這個事件的應用程序去處理。
每個應用都有自己的Main Run Loop線程,Run Loop會遍歷event消息隊列,逐一分發這些事件到應用中合適的對象去處理。具體來說就是調用NSApp 的 sendEvent:方法發送消息到NSWindow,NSWindow再分發到NSView視圖對象,由其鼠標或鍵盤事件響應方法去處理。
事件響應者(Responders):能處理鼠標鍵盤等事件的對象,包括NSApplication, NSWindow, NSDrawer, NSWindowController, NSView 以及繼承於NSView的所有控件對象。
第一響應者(First Responders):鼠標按下或者鍵盤輸入激活的當前對象稱之為第一響應者。
響應者鏈:能處理事件響應的一組按優先級排序的對象,從層級上看離觀察者最近的視圖優先響應事件,通過view的hitTest方法檢測,滿足hitTest方法的的子視圖優先響應事件。
- 事件中的幾個關鍵類
NSResponder
NSResponder定義了鼠標鍵盤觸控板等多種事件方法,下面列出一些鼠標鍵盤主要的方法
1.鼠標按下事件響應方法
-(void)mouseDown:(NSEvent *)theEvent;
2.鼠標右鍵按下事件響應方法
-(void)rightMouseDown:(NSEvent *)theEvent;
3.鼠標松開事件響應方法
-(void)mouseUp:(NSEvent *)theEvent;
4.鼠標拖放事件響應方法
-(void)mouseDragged:(NSEvent *)theEvent;
5.鼠標進入跟蹤區域事件響應方法
-(void)mouseEntered:(NSEvent *)theEvent;
6.鼠標退出跟蹤區域事件響應方法
-(void)mouseExited:(NSEvent *)theEvent;
7.鼠標拖放事件響應方法
-(void)mouseMoved:(NSEvent *)theEvent;
8.鍵盤按鍵按下事件響應方法
-(void)keyDown:(NSEvent *)theEvent;
9.鍵盤按鍵松開事件響應方法
-(void)keyUp:(NSEvent *)theEvent;
NSResponder除了定義基本的響應事件外,還定義了很多key綁定事件方法。具體請參考NSResponder.h的頭文件定義。
NSEvent
1.事件類型,指示鼠標,鍵盤,觸控板不同的事件源
@property (readonly) NSEventType type;
2.鍵盤不同功能區的標志,可以用來區分數字鍵,F1-F2功能鍵,Command,Optioan,Control,Shift不同的功能鍵
@property (readonly) NSEventModifierFlags modifierFlags;
3.鼠標,鍵盤等事件發生的時間
@property (readonly) NSTimeInterval timestamp;
4.事件發生的窗口
@property (nullable, readonly, assign) NSWindow *window;
5.鼠標點擊次數
@property (readonly) NSInteger clickCount;
@property (readonly) NSInteger buttonNumber;
6.鼠標在窗口的位置
@property (readonly) NSPoint locationInWindow;
7.輸入的字符串
@property (nullable, readonly, copy) NSString *characters;
8.輸入的字符串不包括控制鍵(Ctrl,Option,Command,Shift)
@property (nullable, readonly, copy) NSString *charactersIgnoringModifiers;
9.按鍵編碼
@property (readonly) unsigned short keyCode;
鼠標事件
NSApp對於激活/去激活/隱藏/顯示應用的鼠標消息,會自己處理。其他鼠標消息轉發到NSWindow。
NSWindow窗口接收到鼠標event事件,NSWindow調用sendEvent: 發送到鼠標事件發生位置最頂層的View視圖上。
從NSWindow的sendEvent方法中可以攔截到所有的事件消息,可以在這里做特殊流程處理。
- (void)sendEvent:(NSEvent *)theEvent { NSLog(@"theEvent %@ ",theEvent); [super sendEvent:theEvent]; }
從操作行為和處理機制上把鼠標事件分為鼠標點擊/鼠標拖放/鼠標區域跟蹤事件,下面逐一介紹說明。
鼠標事件發生的位置
先獲取event發生的window中的坐標,在轉換成view視圖坐標系的坐標。
NSPoint eventLocation = [event locationInWindow]; NSPoint center = [self convertPoint:eventLocation fromView:nil];
鼠標點擊事件
鼠標按下,鼠標松開一個連續的動作或者鼠標右鍵按下被認為是一個鼠標點擊事件。
mouseDown對應鼠標按下事件響應方法,mouseUp對應鼠標松開事件響應方法。
鼠標左鍵按下
- (void)mouseDown:(NSEvent *)theEvent { //獲取鼠標點擊位置坐標 NSPoint clickLocation = [self convertPoint:[event locationInWindow] fromView:nil]; //邏輯處理代碼... }
鼠標右鍵按下
-(void)rightMouseDown:(NSEvent *)theEvent
鼠標左鍵松開
-(void)mouseUp:(NSEvent *)theEvent;
鼠標右鍵松開
-(void)rightMouseUp:(NSEvent *)theEvent;
判斷是否按下了Command鍵,如果滿足條件則處理,否則轉由super父類去處理。
- (void)mouseDown:(NSEvent *)theEvent { if ([theEvent modifierFlags] & NSCommandKeyMask) { [self setFrameRotation:[self frameRotation]+90.0]; [self setNeedsDisplay:YES]; } else{ [super mouseDown:theEvent]; } }
判斷是否鼠標雙擊
- (void)mouseDown:(NSEvent *)theEvent { if ([theEvent clickCount] > 1) { //雙擊相關處理 } else{ [super mouseDown:theEvent]; } }
鼠標拖放
鼠標按下,接着移動到某一個位置,最后松開鼠標按鍵。這樣一個過程,稱之為鼠標拖放3階段。
對應的事件過程:mouseDown->mouseDragged->mouseUp
判斷鼠標位置是否在點擊准備拖放的控件的中心點范圍內,如果是置拖放標記為YES。
(實際項目中可以自行設置滿足拖放的條件)
- (void)mouseDown:(NSEvent *)theEvent { NSPoint eventLocation = [theEvent locationInWindow]; NSPoint point = [self convertPoint:eventLocation fromView:nil]; //判斷當前鼠標位置是否在中心點范圍內 if (NSPointInRect(point, centerBox)) { draged = YES; } }
如果拖放標記為YES,修改正在拖放的控件的位置
- (void)mouseDragged:(NSEvent *)theEvent { if (draged) { NSPoint eventLocation = [theEvent locationInWindow]; CGRect positionBox = CGRectMake(eventLocation.x, eventLocation.y, self.frame.size.width, self.frame.size.height); self.frame = positionBox; } }
拖放接收,修改拖放標記為NO
- (void)mouseUp:(NSEvent *)theEvent { draged = NO; }
鼠標跟蹤
為了高效的處理鼠標事件,避免無效的區域被監測。可以定義一個矩形區域,在這個區域內鼠標的任何活動(進入/移動/退出)都會收到鼠標事件。
1.使用NSTrackingArea定義跟蹤區域
CGRect eyeBox = CGRectMake(0, 0, 40, 40); NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:eyeBox options: (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow ) owner:self userInfo:nil]; [self addTrackingArea:trackingArea];
各種options選項,根據需要可以按位或來表示需要跟蹤哪些事件
NSTrackingMouseEnteredAndExited:鼠標進入/退出
NSTrackingMouseMoved:鼠標移動
NSTrackingActiveWhenFirstResponder:第一響應者時跟蹤所有事件
NSTrackingActiveInKeyWindow:應用是key Window時 跟蹤所有事件
NSTrackingActiveInActiveApp:應用是激活狀態時跟蹤所有事件
NSTrackingActiveAlways:跟蹤所有事件(鼠標進入/退出/移動)
NSTrackingCursorUpdate:更新鼠標光標形狀
2.監測鼠標事件
鼠標進入跟蹤區域
- (void)mouseEntered:(NSEvent *)theEvent { NSLog(@"mouseEntered"); }
鼠標在跟蹤區域移動
- (void)mouseMoved:(NSEvent *)theEvent { NSLog(@"mouseMoved"); }
鼠標離開跟蹤區域
- (void)mouseExited:(NSEvent *)theEvent { NSLog(@"mouseExited"); }
鼠標光標更新為十字架形狀
- (void)cursorUpdate:(NSEvent *)theEvent { [[NSCursor crosshairCursor] set]; }
事件監控
系統提供了2種事件監控處理方法,一種是不包括應用本身事件的全局監控,一個是只監控應用中發生的事件的局部監控。
1.全局監控
第一個參數為事件類型,可以增加多種事件類型,第二個參數是事件回調函數。
id eventMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSKeyDownMask handler: ^ (NSEvent *theEvent) { return theEvent; }
2.局部監控
id eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler: ^ (NSEvent *theEvent) { return theEvent; }
3.監控刪除
窗口關閉或頁面關閉時刪除監控
[NSEvent removeMonitor:eventMonitor];
比如鼠標離開一個window,需要關閉這個window時至少有2種方法可以解決:
1)注冊NSNotificationCenter消息中心的 NSApplicationDidResignActiveNotification 消息通知來處理。
2)使用NSEvent的局部監控事件的方法
注冊NSEvent事件監控會接收大量的系統事件,從性能上考慮事件監控不是解決問題的最優方案,盡量不要使用事件監控。
Action消息
Action消息是一種特殊的系統事件,不同於普通的鼠標鍵盤事件NSApp使用sendEvent做消息轉發,Action消息是NSApp 的sendAction方法轉發的。
-(BOOL)sendAction:(SEL)theAction to:(id)theTarget from:(id)sender;
theAction參數是事件響應方法,theTarget參數是事件響應關聯的controller或其他對象, sender是事件發生的控件本身。
Action事件是MouseDown事件的2次轉發。鼠標點擊首先觸發控件的MouseDown方法,MouseDown中會執執行sendAction:to:方法將分發到實現了action事件的target對象中。
可以看出普通的事件消息是在控件內部處理,KeyDown,MouseDown等事件響應方法是定義在控件內部。而Action消息的事件響應方法一般是在target對象的內部定義實現的。
NSControl ,NSMenu ,NSToolbar等控件都是以Action消息形式響應事件。