本文转自: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消息形式响应事件。