macOS AppKit 的事件响应简介

图片来自网络

AppKit中的事件都处于一个响应的链条中,这个链条是由一个叫做NSResponder 的类定义的,这个响应链条其实是一个列表,它里面装满了能够响应用户事件的对象.当用户点击鼠标,或者按下键盘的某个键,或者触摸触控板,都会生成一个Event事件,然后在响应链条中寻找可以处理这个事件的对象对事件进行处理.
一个对象如果可以处理事件,那么这个对象必须继承自NSResponder这个类.在AppKit中,NSApplication,NSWindow,NSView都继承自NSResponder

一个NSResponder实例对象有三个组件:事件消息(鼠标,键盘,触控板等产生的),动作消息(action message: 比如NSButton 执行target 的action 方法,就属于一种action消息),和响应链条

一个应用(NSApplication对象)维护着一组窗口(NSWindow)列表,这些窗口都属于这个App,每个窗口对象又维护着一组继承自NSView的对象,这些NSView对象通常用来绘制交互界面以及处理响应事件.

  • 每个应用都拥有一个单利的NSApplication对象来管理主线程的事件循环(main runloop),以及跟踪窗口和菜单的消息,分发事件给相应的对象,建立自动释放池和接收App级别的通知消息.

  • NSApplication对象通过run()方法来开启事件循环(event loop).这个方法在main()函数中

  • 在Xcode项目工程中,NSApplicationMain()类似下面这样的效果:

    void NSApplicationMain(int argc ,char * argv[]){
         [NSApplication shareApplication];
         [NSBundl loadNibNamed:"main" owner: NSApp];
         [NSApp run];
    }
    
  • NSApplication 对象通过调用自身的类方法初始化显示的数据环境,然后挂接到macOS系统的窗口服务(接收事件)和显示服务(显示内容)中.

  • NSApplication 的一个重要任务就是从macOS系统的窗口服务中接收事件(Event),然后将它们派发到相应的NSResponsder对象.

  • NSApplication 会将接收到的Event 转换为NSEvent 对象.

  • 所有的鼠标和键盘事件都会被NSApplication 派发到与之关联的某个具体的NSWindow 对象中,但有一种情况例外:如果按下的是Command(⌘)键,那么所有的NSWindow对象都有机会响应这个事件.

  • NSApplication同时会响应(或派发)接收到的Apple Event(这个比较重要),比如应用启动或者被再次打开(reopened),这个最常用的一个使用场景是通过URL打开我们的App(处理方式与iOS不同哦,需要特别注意呀),前提是需要使用NSAppleEventManager类对事件进行注册!!,通常都是写在applicationWillFinishedLaunching(_:)这个方法中.

  • NSApplication同时负责与macOS的系统服务交互(需要制作系统偏好设置的同学可要注意这里哦),这样可以使你通过服务菜单提供一些系统设置.

  • NSAPPlication可以设置Delegate(这个就不细说了,基本与iOS相似)来响应一些具体消息.

一个窗口对象(NSWindow)处理窗口级别的事件(window-level events)以及将其他事件传递给窗口中的视图对象,同时一个NSWindow还允许通过它的delegate实现自定义窗口的行为方式.

一个事件(Event)是怎样开始传递到应用(Cocoa Application)的?

我们这里说的事件,是指用户通过连接到macOS系统中的鼠标,键盘或者触控板,手写笔等硬件设备的具体操作(比如按下鼠标的按键).

Apple event

我们以最常用的鼠标键盘操作来说明事件传递到应用的过程.当用户按下鼠标或者键盘时:

  • 1.硬件设备首先检测到用户的这个操作,然后通过驱动程序将这个操作动作转换为操作数据.
  • 2.驱动程序将操作数据准备好之后,会调用macOS内核系统的I/O Kit,生成一个硬件级别的事件.
  • 3.驱动程序将这个事件发送到macOS系统的窗口服务事件队列中.
  • 4.驱动程序通知macOS的窗口服务,告知其已经添加了一个事件队列中待处理.
  • 5.macOS的窗口服务收到驱动程序的消息后,会寻找对应的进程(也就是应用程序).
  • 6.当窗口服务找到App 进程后,会将事件派发到这个应用进程的runloop
  • 7.当应用进程的runloop接收到事件后,就开始了事件响应机制,从此刻后,将事件将遵循NSResponder类的处理.

通常情况下,应用程序只有在前台运行的时候才会接收鼠标和键盘的事件;当应用处于后台时,即便是在运行中,一般也不会响应鼠标和键盘的事件,除非是要将这个应用唤醒到前台的事件才会得到响应

事件循环runloop

每个应用都有一种明确的机制用来确保从操作系统的窗口服务中获取事件(Event).在Cocoa Application中,这种机制叫做runloop(一个NSRunLoop对象,它允许进程接收窗口服务的各种来源).默认情况下,OSX中每个线程都有自己的runloop.NSApplication 主线程的runloop称为main runloop,主事件循环的一个显著特点是它由NSApplication对象创建的事件输入源(也就是其他对象,通常是操作系统的窗口服务,可以向它添加事件源).
为了能从窗口服务接收事件和对接收到的事件进行处理,runloop通常包含这两个部分:端口(Mach port)和事件队列(event queue)

image

  • 端口(Mach port):用来从操作系统的窗口服务中获取事件(Event)
  • 队列(Event queue):保存这些事件,直到它们被响应处理.

从另一种意义上讲,应用程序是被事件(event)驱动的:

  • 它从runloop的事件队列中获取一个事件(NSEvent)
  • 派发事件(NSEvent)到合适的对象(Object)
  • 事件被处理完成后,再取下一个事件(NSEvent),直到应用退出.

事件分发(Event Dispatch)

在主事件循环中(main event runloop),应用程序对象(NSApp)会不断的从事件队列中(event queue)获取最前面的事件,然后将它转换为NSEvent 对象后,派发到最终目标.

  • NSApp是通过nextEventMatchingMask:untilDate:inMode:dequeue:这个方法从事件队列中获取到事件,当事件队列为空的时候(也就是队列中无事件),这个方法会阻塞,直到有新的事件到来才会继续.
  • NSApp将事件转换为NSEvent后,第一件事就是调用sendEvent:方法进行派发.
  • 大部分的情况下,NSApp都会将事件派发给用户操作的那个窗口(NSWindow),这是通过调用窗口(NSWindow)的sendEvent:方法完成的.
  • NSWindow窗口对象将事件以NSResponder Message消息的形式(比如mouseDown:或者keyDown:)派发到与用户操作关联的NSView对象.
  • NSWindow派发事件时会根据事件类型略有不同:对于鼠标和触控板事件,NSWindow对象会将事件派发到用户鼠标点击的NSView.对于键盘(keyboard)事件,NSWindow通常会将事件派发给key Window第一响应者

由此可见,在事件派发的过程中,会根据事件种类(AppKit中定义的NSAppKitDefined类型)的不同而进行不同的派发选择.有些事件只能由NSWindow或者NSApplication自身来处理,比如应用的隐藏/显示/激活状态/失去激活状态等.

鼠标(或触控板)事件派发路径

前面已经提到过,一个NSWindow对象使用sendEvent:方法将鼠标事件派发给用户操作的视图(NSView)对象.那么NSWindow是怎样识别是哪个NSView在被用户操作呢?是通过调用NSViewhitTest:方法,根据这个方法的返回值(通常是显示在最顶层的View)来确定.
NSWindow对象是将事件以一个与鼠标相关的NSResponder明确消息方式发送视图(NSView),比如mouseDown:,mouseDragged:,或者rightMouseUp:,如果是鼠标按下事件,NSWindow还会询问NSView是否希望成为第一响应者,以便接收键盘action消息.

一个NSView对象可以接收三种类型的鼠标事件:鼠标点击,鼠标拖拽鼠标移动.
鼠标点击事件可以根据点击方向(按下或抬起)和鼠标按钮(左键,右键,或其他)被进一步的细化分类,这些定义在了NSEventTypeNSResponder 中.

鼠标拖动事件鼠标抬起事件通常都会被发送给之前鼠标按下的那个视图(NSView)对象.

鼠标移动事件通常会派发到第一响应者.

当用户在一个视图控件上点击鼠标按钮后,如果包含这个视图的NSWindow不是key Window,那么这个NSWindow将会变成key Window,并且丢弃本次的鼠标事件;也就是说如果你用鼠标点击了一个不是key Window窗口中的一个(NSButton)按钮时,这个点击动作仅仅是将这个窗口(NSWindow)对象变成key Window而已,你还需要使用鼠标再次点击这个按钮,此时这个按钮才会接收到鼠标点击的事件. 如果你要避免这种情况,可以通过重写NSView的acceptsFirstMouse: 方法,并返回YES

NSView 通常会自动接收鼠标点击鼠标拖拽事件,而不会主动接收鼠标移动事件.因为鼠标移动事件发生的太过频繁,很容易阻塞事件队列,所以默认情况下NSView不响应鼠标移动事件.如果一个NSView需要处理鼠标移动事件,那么需要向它的窗口对象(NSWindow)明确的声明一下,也就是调用NSWindow的 setAcceptsMouseMovedEvents:方法

键盘事件派发路径

响应键盘输入是事件派发中最复杂的部分之一.Cocoa 应用程序会遍历每一个键盘事件来确定它属于那种类型然后以及如何处理.先来看一下苹果官方给出的一个键盘事件可能的传递传递路径:


image

下面我们来解释一下:

  1. Key equivalents(快捷键): 通常是一个或者一组键(通常是与⌘,⌥,⌃等组合),它们被绑定到应用(NSApplication)的某个菜单栏条目(menu item)或者某个NSObject 控件对象 ,当按下这个(或这组)键时,就相当于选中了菜单栏或者点击了控件对象.
  2. NSApplication接收到Key equivalents(快捷键)事件时,会先传递给key window的视图(NSView)体系中,通过给每个对象发送performKeyEquivalent:消息来确认是否有控件响应此事件(根据这个方法的返回值来确认:YES代表响应,NO则是不响应默认为NO),如果视图(NSView)体系中没有任何对象响应,那么NSApplication会将这个快捷键事件发送给menu bar(菜单栏).在Cocoa 的一些类中,比如 NSButton, NSMenu, NSMatrix, 和 NSSavePanel 都提供了默认的响应处理.
  3. Keyboard interface control (控制键): 键盘的控制键事件通常是用来在用户界面上控制当前焦点对象的.在key window中,NSWIndow对象负责对接收到的Keyboard interface control事件进行具体的解释.例如按下Tab键会将当前焦点从一个控件转移到下一个上面;按下Shift-Tab会按照反方向移动焦点;按下Space bar(空格)会模拟鼠标点击等等.这些交互的控件顺序你可以通过(Xcode中的)Interface Builder来构建也可以通过代码来控制(使用NSViewsetNextKeyView:nextKeyView 方法)

关于控制键的更详细内容,有兴趣的同学可以通过这个链接Handling Key Events查看苹果官方的文档

  1. Keyboard action(命令键):这种事件通常是指预先将种操作通过按键绑定机制Text System Defaults and Key Bindings绑定到某个具体的物理按键上,当这个按钮被按下时,执行特定的方法,比如 pageDown:, moveToBeginningOfLine:capitalizeWord:. NSWindow通常会将命令键的event发送到窗口内的第一响应者对象.
  2. 如果是文本字符事件,那么会将按键字符插入到文本中.

应用程序处理键盘事件时,如果这个事件不是快捷键(Key equivalents)或者控制键Keyboard interface control,那么应用程序会将事件通过sendEvent:方法发送给kew window,然后窗口(key window)对象会调用第一响应者的keyDown:方法,将事件传递到整个响应链条中.

关于键盘事件的派发与处理细节,大家可以查看苹果官方文档Handling Key Events

其他事件派发

在应用程序中,我们可以使用NSTrackingArea类添加一个监控区域,这些事件NSWindow对象会直接派发到拥有这个区域的指定对象(通常发送 mouseEntered:和 mouseExited:消息).

应用程序(NSApplication)生成的周期性事件(NSPeriodic)通常不会使用sendEvent:派发,它们是通过某个NSObject对象注册后(通过调用nextEventMatchingMask:untilDate:inMode:dequeue: 方法)才会得到处理.具体的详细内容,可以参考Other Types of Events

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容