鼠标和键盘事件

事件分发过程

OSX 与用户交互的主要外设是鼠标,键盘。鼠标键盘的活动会产生底层系统事件。这个事件首先传递到IOKit框架处理后存储到队列,通知Window Server服务层处理。Window Server存储到FIFO先进先出队列中,逐一转发到当前的活动窗口或能响应这个事件的应用程序去处理。

每个应用都有自己的Main Run Loop线程,Run Loop会遍历event消息队列,逐一分发这些事件到应用中合适的对象去处理。具体来说就是调用NSApp 的 sendEvent:方法发送消息到NSWindow,NSWindow再分发到NSView视图对象,由其鼠标或键盘事件响应方法去处理。

EventDispatch

事件响应者(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];
}

键盘事件

NSApp对不同的键盘事件处理规则:

1)如果是快捷键,NSApp转发到快捷键关联的NSWindow中的控件或菜单,执行对应的功能。

2)如果是控制键,转发到Key Window,控制切换选择不同的控件。

3)如果是其他键, 使用sendEvent转发到Key Window。窗口对象定位到第一响应者对象,从响应链按优先级寻找响应KeyDown键盘事件的视图控件,如果找到转发到视图处理,否则执行insertText方法。如果是文本视图控件会显示输入文本。

快捷键

如果按下的是快捷键,系统会在当前活动的window内发送performKeyEquivalent:消息,window依次遍历它的子视图,沿着它们的响应链寻找performKeyEquivalent:消息的响应者。如果没有找到,window会转发performKeyEquivalent:消息到应用的菜单中继续寻找。performKeyEquivalent方法中会做逻辑判断,满足条件则执行相应的方法,返回YES,不满足返回NO。

在一个视图中定义快捷键处理方法,判断是否按下了组合键 command+l

- (BOOL)performKeyEquivalent:(NSEvent *)theEvent {
    NSString  *characters; = [theEvent charactersIgnoringModifiers];
    if ([characters isEqual:@"l"]) {
        [self performClick:self];
        return YES;
    }
    return NO;
}

控制键

Tab,Shift,Space,Arrow keys,Option or Shift,Command,Control-Tab,Control-Shift-Tab 这些按钮定义为控制键
这些键用来在当前活动Window内切换选择不同的控件,或者模拟执行鼠标按下的操作。

key绑定事件

系统约定了一些key按下自动执行的方法,key绑定事件定义在字典文件中。定义在/System/Library/Frameworks/AppKit.framework/Resources/StandardKeyBinding.dict文件中。

用户自定义的key绑定事件定义在 ~/Library/KeyBindings/ 下面,可以修改默认的key绑定的事件。

/* ~/Library/KeyBindings/DefaultKeyBinding.dict */
{
      /* Additional Emacs bindings */
      "~f" = "moveWordForward:";
      "~b" = "moveWordBackward:";
      "~<" = "moveToBeginningOfDocument:";
      "~>" = "moveToEndOfDocument:";
      "~v" = "pageUp:";
      "~d" = "deleteWordForward:";
      "~^h" = "deleteWordBackward:";
      "~\010" = "deleteWordBackward:";  /* Option-backspace */
      "~\177" = "deleteWordBackward:";  /* Option-delete */
}
    
“^” 表示 Control
“~” 表示 Option
“$” 表示 Shift
“#” 表示 numeric keypad

DefaultKeyBinding.dict中定义的每一行表示按下对应的组合键,执行当前控件中实现的方法。

文字输入

普通文本的字符输入触发keyDown方法,keyDown内部执行字符解析过程interpretKeyEvents。对于按下的delete,enter,up,down,left,right键盘,由doCommandBySelector分发到对应的deleteBackward,insertNewline,moveUp,moveDown,moveLeft,moveRight预定义的方法;对于字符执行insertText方法执行文本插入。

对于NSTextField,NSTextView 文字输入的控件,自动将输入的文字显示到控件视图上。其他控件要响应键盘事件,首先实现KeyDown:方法,在内部执行interpretKeyEvents方法,由系统解析键盘事件,如果是对应的系统key绑定事件,执行绑定方法;否则执行insertText。

要接受文字输入,自定义的NSView必须覆盖实现acceptsFirstResponder:方法,返回YES。

系统NSTextField,NSTextView中实现了文字输入的这种控制过程。但是对于特殊按键的拦截处理(比如说enter回车键)仍然需要在doCommandBySelector中做代码处理。

- (BOOL)acceptsFirstResponder {
    return YES;
}

- (void)keyDown:(NSEvent *)theEvent {
    [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]];
}

- (void)insertText:(id)insertString {
    NSLog(@"insertString %@",insertString);
}

- (void)doCommandBySelector:(SEL)aSelector {
    NSLog(@"doCommandBySelector %@",NSStringFromSelector(aSelector));
    [super doCommandBySelector:aSelector];
}

- (void)moveUp:(id)sender {
    
}
- (void)moveDown:(id)sender {
    
}
- (void)moveLeft:(id)sender {
    
}
- (void)moveRight:(id)sender {
    
}
- (void)deleteBackward:(id)sender {
    
}
- (void)insertNewline:(id)sender {
    
}

NSTextView响应回车控制键的话,需要在控制器Controller中实现下面的代理方法

- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector{
    //NSLog(@"%@",NSStringFromSelector(aSelector));
    //回车键
    if (aSelector == @selector(insertNewline:)) {
        //Do something against ENTER key
        NSLog(@"Return was pressed!");
       //这里是按下回车后的具体处理
        return YES;

    } else if (aSelector == @selector(deleteForward:)) {
        //Do something against DELETE key
        
    } else if (aSelector == @selector(deleteBackward:)) {
        //Do something against BACKSPACE key
        
    } else if (aSelector == @selector(insertTab:)) {
        //Do something against TAB key
    }
    return NO;
}

事件监控

系统提供了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对象中。

MouseDown

可以看出普通的事件消息是在控件内部处理,KeyDown,MouseDown等事件响应方法是定义在控件内部。而Action消息的事件响应方法一般是在target对象的内部定义实现的。

NSControl ,NSMenu ,NSToolbar等控件都是以Action消息形式响应事件。