Apple - Cocoa Event Handling Guide

本文翻译整理自:Cocoa Event Handling Guide(
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/Introduction/Introduction.html#//apple_ref/doc/uid/10000060i


文章目录

  • 一、导言
    • 本文件的组织
    • 另见
  • 二、事件架构
    • 1、事件如何进入Cocoa应用程序
    • 2、事件调度
      • 鼠标和平板电脑事件的路径
      • 关键事件的路径
      • 其他事件调度
    • 3、行动讯息
    • 4、响应者
      • 急救人员
      • 下一个响应者
    • 5 、响应链
      • 事件消息的响应者链
      • 操作消息的响应者链
      • 其他用途
  • 三、事件对象和类型
    • 1、NSEvent对象
      • 事件对象的属性
      • NSEvent类方法
      • 其他类方法中的事件对象
    • 2、事件类型
      • 2.1 鼠标事件
        • 与鼠标点击和移动相关的事件
        • 鼠标跟踪事件
      • 2.2 关键事件
      • 2.3 平板电脑事件
        • 平板设备概述
        • 平板电脑事件的类型
          • 邻近事件
          • 指针事件
          • 平板电脑事件的顺序
      • 2.4 其他类型的事件
  • 四、事件处理基础
    • 1、为接收事件准备自定义视图
    • 2、实施行动方法
    • 3、获取事件的位置
    • 4、测试事件类型和修改器标志
    • 5、响应者相关任务
      • 确定第一响应者状态
      • 设置第一响应者
  • 五、处理鼠标事件
    • 1、鼠标事件概述
    • 2、处理鼠标点击
    • 3、处理鼠标拖动操作
      • 三种方法
      • 鼠标跟踪循环方法
      • 在鼠标跟踪操作期间过滤掉关键事件
  • 六、处理关键事件
    • 1、关键事件概述
    • 2、覆盖keyDown:方法
      • 处理键盘操作和插入文本
      • 特别解释击键
    • 3、处理关键等价物
    • 4、键盘接口控制
  • 七、使用跟踪区域对象
    • 1、创建NSTrackingArea对象
    • 2、管理跟踪区域对象
    • 3、响应鼠标跟踪事件
    • 4、管理游标更新事件
    • 5、兼容性问题
  • 八、处理平板电脑事件
    • 1、平板电脑事件的包装
    • 2、平板电脑事件和响应链
  • 九、处理触控板事件
    • 1、手势是由触控板解释的触摸动作
      • 手势事件和手势序列的类型
      • 处理手势事件
    • 2、触控事件代表触控板上的手指
      • 多点触控序列
      • 触摸身份和其他属性
      • 休息触摸
      • 处理多点触控事件
    • 3、鼠标事件和触控板
  • 十、监控事件
  • 十一、文本系统默认值和键绑定
    • 键绑定
    • 1、选择和编辑的标准操作方法
      • 选型方向
      • 选择和插入点
      • Marks
      • The kill buffer
    • 2、文本系统默认值
      • NSMnemonicsWorkInText
      • NSRepeatCountBinding
      • NSQuotedKeystrokeBinding
      • NSTextShowsInvisibleCharacters
      • NSTextShowsControlCharacters
      • NSTextSelectionColor
      • NSMarkedTextAttribute和NSMarkedTextColor
      • NSTextKillRingSize
  • 十二、鼠标跟踪和光标更新事件
    • 1、处理鼠标跟踪事件
    • 2、管理游标更新事件


一、导言

事件驱动应用程序的主要职责是处理用户事件——即由鼠标、键盘、触控板和平板电脑等设备生成的事件。
对于大多数Cocoa应用程序,Application Kit承担了这项工作的最大份额。
它确保鼠标、键盘和其他设备生成的事件被路由到最适合处理它们的对象。
它还实现了数十个用户界面对象,如控件和文本视图,以预期的方式响应事件——例如,通过插入键入的文本或发送操作消息。
但通常应用程序,尤其是具有自定义NSViewNSWindowNSApplication对象的应用程序,发现它必须自己处理一些事件。

Cocoa事件处理指南解释了如何在Cocoa应用程序中处理所有类型的事件。
它通过描述用于调度和处理事件的Cocoa架构以及概述所有事件处理代码必须处理的NSEvent对象,为基于任务的章节提供了概念背景。
阅读本文档将为您处理Cocoa应用程序中的事件奠定坚实的基础。


本文件的组织

本文档包括以下章节:

  • 事件架构描述了事件如何进入Cocoa应用程序,如何分派它们以查看对象,以及如何处理它们,有时是在沿着响应者对象链向上移动之后。
  • 事件对象和类型检查表示事件的Cocoa对象,并调查Cocoa应用程序可以接收的事件类型。
  • 事件处理基础提供了事件处理代码中的基本任务,而不管事件类型如何。
  • 处理鼠标事件描述了如何处理由用户单击或拖动鼠标引起的事件。
  • 处理键事件描述了如何处理用户按下键盘上的键所产生的事件。
  • 使用Tracking-Area对象解释了如何使用NSTrackingArea对象来管理视图区域内的鼠标跟踪和光标更新。
  • 处理平板电脑事件描述了如何处理通过在平板电脑设备上移动和操作触控笔生成的事件。
  • 文本系统默认值和键绑定讨论了将键组合绑定到操作消息的机制,并描述了可应用于Cocoa文本系统的各种默认值。

附录使用跟踪区域对象介绍了用于鼠标跟踪和光标更新的传统API。
它解释了如何设置跟踪和光标矩形,并处理用户将鼠标光标移动到这些区域时随后生成的事件。


另见

以下文档在概念上与Cocoa事件处理指南相关:

  • 查看编程指南
  • 文本输入管理

因为视图对象经常根据事件重新绘制自己,所以还建议您仔细阅读*Cocoa绘图指南*。

以下示例代码项目包括说明性事件处理代码:

  • BoingX-鼠标拖动,按键事件
  • CI注释-鼠标点击,鼠标拖动
  • Cocoa OpenGL-鼠标点击,关键事件
  • 颜色采样器-鼠标点击,鼠标拖动
  • 裁剪图像-鼠标点击,鼠标拖动
  • 拖动鼠标-鼠标点击、鼠标拖动、键盘操作
  • FunkyOverlayWindow-鼠标跟踪
  • 图像地图示例-鼠标拖动,鼠标跟踪
  • 人员-关键事件
  • CircleView-鼠标点击,鼠标拖动
  • ClockControl-应答器相关,键盘操作
  • DotView-鼠标点击
  • 标尺-鼠标拖动
  • SimpleStickies-鼠标点击,鼠标拖动

二、事件架构

事件到Cocoa应用程序中最终处理它的对象的路径可能很复杂。
本章跟踪各种类型事件的可能路径,并描述在Application Kit中处理事件的机制和架构设计。

如需进一步了解背景,建议阅读*Mac应用程序编程指南*中的关于OS X应用程序设计。


1、事件如何进入Cocoa应用程序

事件是用户操作的低级记录,通常被路由到发生该操作的应用程序。
OS X中的典型事件起源于用户操作连接到计算机系统的输入设备,例如键盘、鼠标或平板触控笔。
当用户按下一个键或单击一个按钮或移动一个触控笔时,设备检测到该操作并启动向与其关联的设备驱动程序的数据传输。
通过I/O Kit,设备驱动程序创建一个低级事件,将其放入窗口服务器的事件队列中,并通知窗口服务器。
窗口服务器将事件分派到目标进程的适当运行循环端口。
从那里,事件被转发到适用于应用程序环境的事件处理机制。
图1-1描述了这个事件传递系统。


图1-1事件流

在这里插入图片描述


注意:应用程序通常仅在键盘和鼠标处于前台(即活动状态)时才从键盘和鼠标接收事件。
尽管在后台运行的应用程序通常不会接收键和鼠标事件,但称为事件点击的低级机制使后台应用程序可以接收事件并对其采取行动。

在将事件分派给应用程序之前,窗口服务器会以各种方式对其进行处理;它会为其添加时间戳,使用相关的窗口和处理端口对其进行注释,并且可能还会执行其他任务。
例如,考虑当用户按下按键时会发生什么。
设备驱动程序将原始扫描代码转换为虚拟按键代码,然后在事件记录中将虚拟按键代码(以及有关按键的其他信息)传递给窗口服务器。
窗口服务器有一个转换工具,可以将虚拟按键代码转换为Unicode字符。

在OS X中,事件以异步流的形式传递。
该事件流“向上”(在架构意义上)通过系统的各个级别——硬件到窗口服务器再到事件管理器——直到每个事件到达其最终目的地:应用程序。
当它通过每个子系统时,事件可能会改变结构,但它仍然标识特定的用户操作。

注意:较低级别的系统在事件流的早期捕获和处理一些事件。
这些事件永远不会路由到Cocoa应用程序。
这些事件由保留键或组合键生成,例如电源键和媒体弹出键。

每个应用程序都有一种特定于其环境的机制,用于从窗口服务器接收事件。
对于Cocoa应用程序,这种机制称为主事件循环。
运行循环,在Cocoa中是一个NSRunLoop对象,使进程能够接收来自各种源的输入。
默认情况下,OS X中的每个线程都有自己的运行循环,Cocoa应用程序主线程的运行循环称为主事件循环。
主要事件循环的区别在于一个称为事件源的输入源,它是在全局 NSApplication对象(NSApp)初始化时构造的。
事件源由一个用于从窗口服务器接收事件的端口和一个FIFO队列——事件队列——用于保存这些事件,直到应用程序可以处理它们,如图1-2所示。


图1-2主事件循环,带有事件源

在这里插入图片描述


Cocoa应用程序是事件驱动的:它从队列中获取一个事件,将其分派到适当的对象,并在事件处理完毕后获取下一个事件。
除了一些例外(例如模态事件循环),应用程序继续以这种模式运行,直到用户退出它。
以下部分事件分派描述了应用程序如何获取和分派事件。

通过事件源传递的事件并不是进入Cocoa应用程序的唯一事件类型。
应用程序还可以响应Apple事件,这是通常由Finder和Launch Services等其他进程发送的高级进程间事件。
例如,当用户双击应用程序图标以打开应用程序或双击文档以打开文档时,Apple事件将被发送到目标应用程序。
应用程序也会从队列中获取Apple事件,但不会将它们转换为NSEvent对象。
相反,Apple事件由事件处理程序直接处理。
当应用程序启动时,它会为此目的自动注册多个事件处理程序。
有关Apple事件和事件处理程序的更多信息,请参阅*Apple事件编程指南*。


2、事件调度

在主事件循环中,应用程序对象(NSApp)不断获取事件队列中的下一个(最顶层)事件,将其转换为NSEvent对象,并将其分派到最终目的地。
它通过在闭环中调用nextEventMatchingMask:untilDate:inMode:dequeue:方法来执行事件获取。
当事件队列中没有事件时,此方法会阻塞,只有当有更多事件要处理时才会恢复。

获取和转换事件后,NSAppsendEvent:方法中执行事件分派的第一阶段。
在大多数情况下,NSApp只是通过调用NSWindow对象的sendEvent:方法将事件转发到用户操作发生的窗口。
然后,窗口对象将大多数事件分派到NSView对象,该对象与NSResponder消息中的用户操作相关联,例如mouseDown:keyDown:
事件消息包含描述事件的NSEvent对象作为其唯一参数。

接收事件消息的对象因事件类型而略有不同。
对于鼠标和平板电脑事件,NSWindow对象将事件分派给用户按下鼠标或触控笔按钮的视图。
它将大多数键事件分派给键窗口的第一个响应者。
图1-3和图1-4说明了这些不同的一般传递路径。
目标视图可能决定不处理事件,而是将其向上传递给响应者链(请参阅响应者链)。


图1-3鼠标事件的路径

在这里插入图片描述


图1-4键事件的路径(要插入的字符)

在这里插入图片描述


在上一段中,您可能已经注意到限定词的使用,例如“在大多数情况下”和“通常”。
根据特定类型的事件,Cocoa中事件(尤其是关键事件)的传递可以采取许多不同的路径。
一些事件,其中许多是由应用程序工具包(类型NSAppKitDefined)定义的,与窗口或应用程序对象本身控制的操作有关。
这些事件的示例是与激活、停用、隐藏和显示应用程序相关的事件。
NSApp在其调度例程的早期过滤掉这些事件并自行处理它们。

以下部分描述了可以到达视图对象的事件的不同路径。
有关这些事件类型的详细信息,请阅读事件对象和类型。


鼠标和平板电脑事件的路径

如上所述,sendEvent:方法中的NSWindow对象将鼠标事件转发到发生涉及鼠标的用户操作的视图。
它通过调用NSView方法hitTest:来标识要接收事件的视图,该方法返回包含事件光标位置的最低后代(这通常是显示的最顶层视图)。
窗口对象通过向其发送与鼠标相关的NSResponder特定于其确切类型的消息,例如mouseDown:mouseDragged:rightMouseUp:、在(左)鼠标向下事件中,窗口对象还询问接收视图是否愿意成为后续键事件和操作消息的第一响应者。

视图对象可以接收三种一般类型的鼠标事件:鼠标点击、鼠标拖动和鼠标移动。
鼠标点击事件被进一步分类——作为特定的NSEventType常量和NSResponder方法——通过鼠标按钮(左、右或其他)和点击方向(向上或向下)。
鼠标拖动和鼠标向上事件通常发送到接收到最近鼠标向下事件的同一视图。
鼠标移动事件被发送到第一个响应者。
相对于其他鼠标事件,鼠标向下、鼠标拖动、鼠标向上和鼠标移动事件只能在特定情况下发生:

  • 每个鼠标向上事件之前必须有一个鼠标向下事件。
  • 鼠标拖动事件仅发生在鼠标向下事件和鼠标向上事件之间。
  • 鼠标移动事件不会发生在鼠标按下和鼠标上举事件之间。

当用户在光标位于视图对象上时按下鼠标按钮时,将发送鼠标向下事件。
如果包含视图的窗口不是键窗口,则该窗口将成为键窗口并丢弃鼠标向下事件。
但是,视图可以通过覆盖NSViewacceptsFirstMouse:方法返回YES来规避这种默认行为。

视图会自动接收鼠标单击和鼠标拖动的事件,但是由于鼠标移动的事件经常发生,并且会使事件队列陷入困境,视图对象必须使用NSWindow方法setAcceptsMouseMovedEvents:显式请求其窗口监视它们。
其他事件调度中描述的跟踪矩形是跟踪鼠标位置的一种成本较低的方法。

在实现NSResponder方法时,NSView的子类可以将鼠标事件解释为执行特定操作的提示,例如发送目标操作消息、选择图形元素、在不同位置重绘自身等等。
每个事件方法都包含一个NSEvent对象作为其唯一参数,视图可以从中获取有关事件的信息。
例如,视图可以使用locationInWindow在接收者窗口的坐标系中定位鼠标光标的热点。
要将其转换为视图的坐标系,请使用convertPoint:fromView:和一个nil视图参数。
从这里,您可以使用mouse:inRect:来确定点击是否发生在感兴趣的区域。

平板电脑事件传递到视图的路径类似于鼠标事件的传递路径。
表示发生平板电脑事件的窗口的NSWindow对象将事件转发到光标下的视图。
但是,平板电脑事件有两种,接近事件和指针事件。
前者通常是当触控笔移入和移出接近平板电脑时生成的本机平板电脑事件(类型为NSTabletProximity)。
平板电脑指针事件发生在接近进入和接近离开平板电脑事件之间,并指示触控笔方向、压力和按钮单击等内容。
指针事件通常是鼠标事件的子类型。
有关详细信息,请参阅处理平板电脑事件。

有关鼠标跟踪和光标更新事件采用的路径,请参阅其他事件调度。


关键事件的路径

到目前为止,处理键盘输入是事件分派中最复杂的部分。
应用程序工具包竭尽全力为您简化这个过程,事实上,处理到达您的自定义对象的关键事件相当简单。
然而,这些事件在从硬件到响应链的过程中会发生很多事情。
特别令人感兴趣的是作为NSEvent对象到达Cocoa应用程序的关键事件类型以及处理这些类型事件的顺序和方式。

Cocoa应用程序评估每个关键事件以确定它是什么样的关键事件,然后以适当的方式进行处理。
关键事件在处理之前可能需要很长的路径。
图1-5显示了这些潜在的路径。


图1-5关键事件的可能路径

在这里插入图片描述


以下列表详细描述了关键事件的可能路径,按照应用程序评估每个关键事件的顺序;

  1. 键等效项。
    键等效项是通常绑定到应用程序中某个菜单项或控件对象的键或组合键(通常是由Command键修改的键)。
    按下组合键模拟单击控件或选择菜单项的操作。
    应用程序对象通过向下键窗口中的视图层次结构来处理等效键,向每个对象发送performKeyEquivalent:消息,直到对象返回YES
    如果消息不是由视图层次结构中的对象处理的,NSApp然后将其发送到菜单栏中的菜单。
    一些Cocoa类,如NSButtonNSMenuNSMatrixNSSavePanel提供默认实现。
    有关详细信息,请参阅处理密钥等效项。
  2. 键盘界面控制
    键盘界面控制事件操作用户交互界面中对象之间的输入焦点。
    在键窗口中,NSWindow将某些键解释为命令,以将控件移动到不同的界面对象,模拟鼠标单击它,等等。
    例如,按下Tab键将输入焦点移动到下一个对象;Shift-Tab反转方向;按下空格键模拟单击按钮。
    通过这种机制控制的界面对象的顺序由键视图循环指定。
    您可以在Interface Builder中设置键视图循环,并且可以通过setNextKeyView:``NSViewnextKeyView和nextKeyView方法以编程方式操作键视图循环。
    有关详细信息,请参阅键盘接口控制。
  3. 键盘动作
    与控制发送到目标的动作消息不同(参见Action Messages),键盘动作是命令(由NSResponder类定义的方法表示),是物理击键的每视图功能解释(由NSEventcharacters方法返回的常量标识)。
    换句话说,键盘动作通过文本系统默认值和键绑定中描述的键绑定机制绑定到物理键。
    例如,pageDown:moveToBeginningOfLine:capitalizeWord:是按下绑定键时键盘动作调用的方法。
    这些动作被发送到第一个响应者,处理这些动作的方法可以在该视图或响应者链上的超级视图中实现。
    有关处理键盘操作的更多信息,请参见覆盖keyDown:方法。
  4. 作为文本插入的字符(或多个字符)。
    如果应用程序对象处理了一个键事件,结果发现它不是一个键等价物或键接口控制事件,它就会在sendEvent:消息中将它发送到键窗口。
    窗口对象调用第一个响应器中的keyDown:方法,从那里,键事件向上传播到响应器链,直到它被处理。
    此时,键事件可以是一个或多个要插入到视图显示文本中的Unicode字符、要特殊解释的键或组合键,或者键盘操作事件。

有关如何分派和处理密钥事件的详细信息,请参阅处理密钥事件。


其他事件调度

一个NSWindow对象监视跟踪矩形事件,并将这些事件直接分派到mouseEntered:mouseExited:消息中的拥有对象。
所有者在NSTrackingArea方法initWithRect:options:owner:userInfo:NSView方法addTrackingRect:owner:userData:assumeInside:.使用跟踪区域对象描述了如何设置跟踪矩形并处理相关事件。

周期性事件(类型NSPeriodic)由应用程序以指定的频率生成并放置在事件队列中。
然而,与大多数其他类型的事件不同,周期性事件不会使用NSApplicationNSWindowsendEvent:机制进行分派。
相反,为周期性事件注册的对象通常使用nextEventMatchingMask:untilDate:inMode:dequeue:方法在模态事件循环中检索它们。
有关周期性事件的更多信息,请参阅其他类型的事件。


3、行动讯息

到目前为止,讨论集中在事件消息上:由设备相关事件(如鼠标单击或按键)产生的消息。
Application Kit将适当形式的事件消息(例如mouseDown:keyDown:)发送到NSResponder对象进行处理。

但是NSResponder对象也应该处理另一种消息:动作消息。
动作是对象(通常是NSControlNSMenu对象)向应用程序对象发出的命令,这些命令作为消息发送给特定的目标或任何愿意响应它们的目标。
动作消息调用的方法有一个特定的签名:一个参数,其中包含对发起动作消息的对象的引用;按照惯例,这个参数的名称是sender
例如,

- (void)moveToEndOfLine:(id)sender; // from NSResponder.h

事件和动作方法通过不同的方法以不同的方式分派。
几乎所有事件都从窗口服务器进入应用程序,并由NSApplicationsendEvent:方法自动分派。
另一方面,动作消息由全局应用程序对象(NSApp)的sendAction:to:from:方法分派到它们正确的目的地。

注意: 事件消息和动作消息之间的一个主要区别是它们可能占用响应器链的不同路径。
有关详细信息,请参阅响应器链。

如图1-6所示,动作消息通常作为事件消息的次要效果发送。
当用户单击控件对象(例如按钮)时,会发送两个事件消息(mouseDown:mouseUp:)作为结果。
控件及其关联单元通过向应用程序对象发送sendAction:to:from:消息来处理mouseUp:消息(部分)。
第一个参数是标识要调用的动作方法的选择器。
第二个参数是消息的预期接收者,称为目标,可以是nil
最后一个参数通常是调用sendAction:to:from:的对象,从而指示哪个对象发起了动作消息。
动作消息的目标可以将消息发送回发送方以获取更多信息。
菜单和菜单项也有类似的顺序。
有关控件和单元格(以及菜单和菜单项)架构的更多信息,请参阅*Mac应用程序编程指南*中的核心应用程序设计。


图1-6从事件消息到动作消息

在这里插入图片描述


操作消息的目标由Application Kit以一种特殊的方式处理。
如果预期的目标不是nil,则直接将操作发送到该对象;这称为有针对性的操作消息。
在无针对性的操作消息(即目标参数为nil)的情况下,sendAction:to:from:在完整的响应者链(从第一个响应者开始)中搜索实现指定的操作方法的对象。
如果找到一个,它会将消息发送到该对象,并将操作消息的发起者作为唯一参数。
然后,操作消息的接收者可以向发送者查询其他信息。
您可以找到无针对性的操作消息的接收者,而无需使用targetForAction:.

事件消息形成了一个众所周知的集合,因此NSResponder为所有这些消息提供了声明和默认实现。
然而,大多数操作消息由自定义类定义,无法预测。
然而,NSResponder确实声明了许多键盘操作方法,例如pageDown:moveToBeginningOfDocument:cancelOperation:这些操作方法通常使用键绑定机制绑定到特定键,旨在执行光标移动、文本操作和类似操作。

一个更通用的动作消息分派机制是由NSResponder方法提供的tryToPerform:with:
这个方法检查接收者,看看它是否响应了提供的选择器,如果响应了,则调用消息。
如果没有,它会发送tryToPerform:with:到它的下一个响应器。
NSWindowNSApplication 覆盖这个方法以包含它们的委托,但是它们不会像sendAction:to:from:方法那样链接各个响应器链。
类似于tryToPerform:with:isdoCommandBySelector:,它接受一个方法选择器并尝试找到一个实现它的响应器。
如果没有找到,该方法会导致硬件发出哔哔声。

警告: 尽管NSResponder声明了许多操作消息,但它实际上并没有实现它们。
您永远不应该直接向未知类的响应者对象发送操作消息。
始终使用NSApplication方法sendAction:to:from:NSResponder方法tryToPerform:with:doCommandBySelector:,或者检查目标是否使用NSObject方法respondsToSelector:进行响应。


4、响应者

响应者是一个对象,它可以直接接收事件,也可以通过响应者链接收事件,因为它继承自NSResponder类。
NSApplicationNSWindowNSDrawerNSWindowControllerNSView以及Application Kit中这些类的许多后代继承自NSResponder
该类定义了用于接收事件消息和许多操作消息的编程接口。
它还定义了响应者行为的一般结构。
在响应者链中有一个第一个响应者和一个下一个响应者序列

有关响应者链的更多信息,请参阅响应者链。


急救人员

第一个响应者通常是用户用鼠标或键盘选择或激活的用户界面对象。
它通常是响应链中接收事件或操作消息的第一个对象。
NSWindow对象的第一个响应者最初是它自己;但是,您可以通过编程方式在Interface Builder中设置窗口首次放置在屏幕上时成为第一个响应者的对象。

NSWindow对象接收到鼠标按下事件时,它会自动尝试将事件下的NSView对象设为第一响应者。
它通过使用此类定义的acceptsFirstResponder方法询问视图是否要成为第一响应者来做到这一点。
此方法默认返回NO;需要成为第一响应者的响应者子类必须覆盖它才能返回YES
当用户通过键盘界面控制功能更改第一响应者时,也会调用acceptsFirstResponder方法。

您可以通过向NSWindow对象发送makeFirstResponder:以编程方式更改第一个响应者。
此消息启动一种协议,其中一个对象失去其第一个响应者状态,另一个对象获得它。
有关详细信息,请参阅设置第一个响应者。

一个NSPanel对象呈现了第一响应者行为的变体,它允许面板呈现一个用户交互界面,该界面不会从主窗口中拿走键焦点。
如果表示非活动窗口并从becomesKeyOnlyIfNeeded返回YES的面板对象接收到鼠标向下事件,它会尝试使鼠标指针下的视图对象成为第一响应者,但当该对象在acceptsFirstResponderneedsPanelToBecomeKey中返回YES时。

鼠标移动的事件(类型NSMouseMoved)总是发送到第一个响应者,而不是鼠标下的视图。


下一个响应者

每个响应者对象都有让下一个响应者进入响应者链的内置功能。
返回该对象的nextResponder方法是响应者链的基本机制。
图1-7显示了下一个响应者的顺序。


图1-7下一个响应者链

在这里插入图片描述


视图的下一个响应者总是它的超级视图——事实上,大多数响应者链包括从窗口的第一个响应者到其内容视图的视图。
当您以编程方式或在Interface Builder中创建窗口或向现有视图添加子视图时,Application Kit会自动连接响应者链中的下一个响应者。
NSViewaddSubview:方法会自动将接收者设置为新的子视图的超级视图。
如果您在视图之间插入不同的响应者,请务必在从视图层次结构中添加或删除视图后验证并可能修复响应者链。


5 、响应链

响应者链是应用事件或操作消息的一系列链接的响应者对象。
当给定的响应者对象不处理特定消息时,该对象将消息传递给链中的继任者(即它的下一个响应者)。
这允许响应者对象将处理消息的责任委托给其他通常更高级别的对象。
Application Kit如下所述自动构造响应者链,但您可以使用NSResponder方法setNextResponder:将自定义对象插入其中,您可以使用nextResponder检查它(或遍历它)。

一个应用程序可以包含任意数量的响应链,但在任何给定时间只有一个处于活动状态。
事件消息和操作消息的响应链不同,如以下部分所述。


事件消息的响应者链

几乎所有事件消息都适用于单个窗口的响应者链——相关用户事件发生的窗口。
事件消息的默认响应者链始于NSWindow对象首先将消息传递到的视图。
关键事件消息的默认响应者链始于窗口中的第一个响应者;鼠标或平板电脑事件的默认响应者链始于发生用户事件的视图。
从那里,如果不处理事件,则向上进入视图层次结构到表示窗口本身的NSWindow对象。
第一个响应者通常是窗口中的“选定”视图对象,它的下一个响应者是它的包含视图(也称为它的超级视图),依此类推直到NSWindow对象。
如果NSWindowController对象正在管理窗口,它将成为最后的下一个响应者。
您可以在NSView对象之间,甚至在链顶部附近的NSWindow对象上方插入其他响应者。
这些插入的响应者接收事件和操作消息。
如果没有找到处理事件的对象,链中的最后一个响应者调用noResponderFor:,对于按键事件,它会简单地发出哔哔声。
事件处理对象(NSWindowNSView的子类)可以覆盖此方法以根据需要执行其他步骤。


操作消息的响应者链

对于操作消息,Application Kit构建了一个更复杂的响应链,该响应链根据两个因素而变化:

  • 应用程序是否基于文档体系结构,如果不是,是否将NSWindowController对象用于其窗口
  • 应用程序当前是否显示键窗口和主窗口

动作消息比事件消息具有更精细的响应链,因为动作需要更灵活的运行时机制来确定其目标。
它们不像事件消息那样仅限于单个窗口。

最简单的情况是没有显示关联面板或辅助窗口的活动非基于文档的窗口——换句话说,主窗口也是键窗口。
在这种情况下,响应链如下所示:

  1. 主窗口的第一个响应者和连续响应者对象在视图层次结构中向上
  2. 主窗口本身
  3. 主窗口的委托(不需要从NSResponder继承)
  4. 应用程序对象NSApp
  5. 应用程序对象的委托(不需要从NSResponder继承)

该链如图1-8所示。


图1-8非基于文档的操作消息应用程序的响应器链

在这里插入图片描述


正如这个序列所示,NSWindow对象和NSApplication对象使它们的委托有机会像响应者一样处理操作消息,即使委托不在响应者链中(也就是说,到窗口或应用程序对象的nextResponder消息不返回委托)。

当应用程序同时显示主窗口和键窗口时,两个窗口的响应者链都可以参与到操作消息中。
如窗口分层和窗口类型中所述,主窗口是最前面的文档或应用程序窗口。
主窗口通常也有键状态,这意味着它们是用户输入的当前焦点。
但是主窗口可以有一个与之关联的辅助窗口或面板,例如查找面板或信息窗口,显示文档窗口中选择的详细信息。
当这个辅助窗口是用户输入的焦点时,它就是键窗口。

当应用程序显示一个主窗口和一个单独的键窗口时,键窗口的响应链在操作消息中首先被破解,主窗口的响应链紧随其后。
完整的响应链包括这些响应者和委托:

  1. 键窗口的第一个响应者和连续的响应者对象在视图层次结构中向上
  2. 键窗本身
  3. 键窗口的委托(不需要从NSResponder继承)
  4. 主窗口的第一个响应者和连续响应者对象在视图层次结构中向上
  5. 主窗口本身
  6. 主窗口的委托(不需要从NSResponder继承)
  7. 应用程序对象NSApp
  8. 应用程序对象的委托(不需要从NSResponder继承)

如您所见,键窗口和主窗口的响应器链与全局应用程序对象相同,其委托是主窗口响应器链末端的响应器。
这种设计适用于其他类型应用程序的响应器链:基于文档架构的应用程序和使用NSWindowController对象进行窗口管理的应用程序。
在后一种情况下,默认的主窗口响应器链由以下响应器和委托组成:

  1. 主窗口的第一个响应者和连续响应者对象在视图层次结构中向上
  2. 主窗口本身
  3. 窗口的NSWindowController对象(继承自NSResponder
  4. 主窗口的委托
  5. 应用程序对象NSApp
  6. 应用程序对象的委托

图1-9显示了使用NSWindowController对象的非基于文档的应用程序的响应者链。


图1-9带有NSWindowController对象(动作消息)的非文档应用程序的响应器链

在这里插入图片描述


对于基于文档的应用程序,主窗口的默认响应者链由以下响应者和委托组成:

  1. 主窗口的第一个响应者和连续响应者对象在视图层次结构中向上
  2. 主窗口本身
  3. 窗口的NSWindowController对象(继承自NSResponder
  4. 主窗口的委托。
  5. NSDocument对象(如果与主窗口的委托不同)
  6. 应用程序对象NSApp
  7. 应用程序对象的委托
  8. 应用程序的文档控制器(一个NSDocumentController对象,不继承自NSResponder

图1-10显示了基于文档的应用程序的响应链。


图1-10基于文档的操作消息应用程序的响应器链

在这里插入图片描述


其他用途

响应器链由Application Kit中的其他三种机制使用:

  • 自动菜单项和工具栏项启用 :在自动启用和禁用具有nil目标的菜单项时,NSMenu根据菜单对象代表应用程序菜单还是上下文菜单搜索不同的响应器链。
    对于应用程序菜单,NSMenu查询完整的响应器链——即第一个键,然后是主窗口——以查找实现菜单项操作方法的对象,并(如果实现了)返回YESfromvalidateMenuItem:
    对于上下文菜单,搜索仅限于显示上下文菜单的窗口的响应器链,从关联视图开始。
    工具栏项的启用和禁用以与菜单项相同的方式使用响应链。
    在这种情况下,密钥验证方法是validateToolbarItem:
    有关自动菜单项启用的详细信息,请参阅启用菜单项;有关工具栏项验证的详细信息,请参阅验证工具栏项。
  • 服务合格性 :类似地,服务工具沿着完整的响应器链传递validRequestorForSendType:returnType:消息,以检查有资格获得其他应用程序提供的服务的对象。
    有关详细信息,请参阅*服务实施指南*。
  • 错误表示 :Application Kit使用响应器链的修改版本进行错误处理和错误表示,集中在NSResponderpresentError:modalForWindow:delegate:didPresentSelector:contextInfo:presentError: 方法
    有关错误响应链的更多信息,请参阅*错误处理编程指南*。

三、事件对象和类型

Cocoa应用程序中的几乎所有事件都由NSEvent类的对象表示。
(例外包括Apple事件、通知和类似项目。)每个NSEvent对象更狭义地表示特定类型的事件,每个事件都有自己的处理要求。
以下部分描述了NSEvent对象的特征和可能的事件类型。


1、NSEvent对象

一个NSEvent对象——或者简单地说,一个事件对象——包含有关输入操作的相关信息,例如鼠标单击或按键。
它存储诸如鼠标所在位置或键入了哪个字符等详细信息。
如事件如何进入可可应用程序中所述,窗口服务器将每个用户操作与一个窗口相关联,并将事件(以较低级别的形式)报告给创建该窗口的应用程序。
应用程序将每个事件临时放置在称为事件队列的缓冲区中。
当应用程序准备好处理一个事件时,应用程序对象(NSApp)从队列中取出一个(通常是队列中最上面的一个)并将其转换为NSEvent对象,然后将其分派给应用程序中的适当对象。

应用程序的响应程序对象通过NSResponder类声明的事件方法的参数(如mouseDown:)接收当前分派的事件对象。
此外,Application Kit的其他方法允许任何对象检索当前事件或从事件队列中获取下一个事件(或特定类型的下一个事件)。
有关这些方法的更多信息,请参阅其他类方法中的事件对象。


事件对象的属性

一个NSEvent对象在很大程度上是一个与特定事件相关的信息的只读存储库。
NSEvent类的大多数方法都是用于获取事件属性值的访问器方法。
NSEvent没有对应的“setter”访问器方法,尽管您可以在使用各种类厂方法创建事件对象时指定某些属性。)一个对象,如响应者,通常使用访问器方法来获取事件的细节,从而知道如何处理它。

一些NSEvent属性(及其相应的访问器方法)对所有类型的事件都是通用的,而其他属性则特定于某些类型的事件。
例如,clickCount方法仅适用于鼠标事件,characters方法仅适用于键事件。
Tablet事件有许多仅适用于它们的访问器方法。
NSEvent的一些更重要的访问器方法如下:

  • type
    事件的类型;请参见事件类型。
  • window
    表示发生事件的窗口的NSWindow对象。
    使用windowNumber,您还可以获取窗口服务器分配给窗口的编号。
    大多数但不是所有事件都与窗口相关联;当没有关联窗口时,window返回nil
  • locationInWindow
    事件在窗口的基本坐标系中的位置。
  • modifierFlags
    指示用户在事件发生时按住了哪些修改键(命令、控制、移位等)。
  • characters
    键事件生成的Unicode字符。
    您也可以使用charactersIgnoringModifiers来获取键事件字符减去修饰符键生成的字符。
  • timestamp
    事件发生的时间(系统启动后的秒数)。
  • clickCount
    对于特定时间阈值内的鼠标事件,与特定事件相关的点击次数。
    (这可以检测两次或三次点击。)

NSEvent类方法

尽管您很少需要这样做,但您可以从头开始创建一个事件对象,然后将其插入到事件队列中进行分发,或者在事件消息中直接将其发送到目的地。
NSEvent类包括创建特定类型事件对象的类方法;例如,要创建鼠标类型的事件对象,您可以使用类方法mouseEventWithType:location:modifierFlags:timestamp:windowNumber:context:eventNumber:clickCount:pressure:
您可以通过调用NSWindow方法postEvent:atStart:NSApplication类的同名方法将事件对象添加到事件队列中。

另一个NSEvent类方法mouseLocation返回鼠标的当前位置。
它与实例方法locationInWindow在一些重要方面有所不同。
作为一个类方法,它不需要将消息发送到事件对象;它以屏幕坐标而不是基本(窗口)坐标返回位置;它返回当前鼠标位置,这可能与当前或任何挂起的鼠标事件不同。
locationInWindow

注意: OS X v10.6引入了几个类方法,允许访问键盘和鼠标的一些系统首选项。
keyRepeatDelay类方法返回一个时间值,表示必须按下一个键才能生成第一个重复键事件的周期。
keyRepeatInterval类方法返回后续重复键事件之间的时间。
对于鼠标事件,有一个doubleClickInterval类方法返回必须发生第二次点击才能被视为双击的周期。

在OS X v10.6中,事件流之外的调用者可以使用另外两个类方法来学习按下了哪些修饰符键(modifierFlags)和按下了哪些鼠标按钮(pressedMouseButtons)。


其他类方法中的事件对象

NSEvent对象分散在Application Kit中。
例如,类NSCellNSCursorNSClipViewNSMenuNSSliderNSTableView都有以事件对象作为返回值或参数的方法。
但是,一些处理事件对象的Application Kit方法特别重要。

虽然大多数事件是通过响应链自动分发的,但有时对象需要显式检索事件——例如,在鼠标跟踪时。
NSWindowNSApplication 都定义了方法nextEventMatchingMask:untilDate:inMode:dequeue:,它允许对象从事件队列中检索特定类型的事件。

NSApplicationNSWindow还定义了currentEvent方法,该方法获取从事件队列中检索到的最后一个事件对象。
这些方法非常方便,因为它们使应用程序中的任何对象都可以了解当前在主事件循环中处理的事件。

最后,NSWindowNSApplication都定义了sendEvent:方法。
这些方法的实现对于事件分派至关重要。
因为这些方法是应用程序中事件的漏斗点,所以您可以在某些情况下覆盖它们,以了解事件流中较早的事件或增强或修改本机事件分派行为。
事件分派讨论了sendEvent:方法所扮演的角色。


2、事件类型

NSEvent的type方法NSEvent标识事件排序的NSEventType值。
事件的类型在很大程度上决定了如何处理它。
不同类型的事件分为六组:

  • 鼠标事件
  • 关键事件
  • 跟踪矩形和光标更新事件
  • 平板事件
  • 周期性事件
  • 其他活动

这些组中的一些包含多个NSEventType常量,其他的只有一个。
一些事件类型可能有以下部分中描述的子类型。
NSEventType常量在NSEvent.h中声明。

允许您有选择地从事件队列中获取和丢弃事件的NSApplicationNSWindow方法-nextEventMatchingMask:untilDate:inMode:dequeue:discardEventsMatchingMask:beforeEvent:-在第一个参数中获取一个或多个事件类型掩码常量。
这些常量也在NSEvent.h中声明。


2.1 鼠标事件

鼠标事件由鼠标按钮状态的变化和鼠标光标在屏幕上位置的变化产生。
它们分为两个子类别,一个与鼠标点击和移动有关,另一个与鼠标跟踪和光标更新有关。


与鼠标点击和移动相关的事件

较大的鼠标事件类别包括按下或释放鼠标按钮以及移动鼠标而不被跟踪的事件。
它由以下与指定用户操作对应的鼠标事件类型组成:

  • NSLeftMouseDownNSLeftMouseUpNSRightMouseDownNSRightMouseUpNSOtherMouseDownNSOtherMouseUp
    用户单击了鼠标按钮。
    名称中带有“MouseDown”的常量表示用户按下了按钮;“MouseUp”表示用户释放了它。
    如果鼠标只有一个按钮,则只生成鼠标左键事件。
    通过向事件发送clickCount消息,您可以确定鼠标事件是否是单击、双击等。
    具有两个以上按钮的鼠标可以生成“其他鼠标”事件。
  • NSLeftMouseDraggedNSRightMouseDraggedNSOtherMouseDragged
    用户拖动鼠标。
    更具体地说,用户在按下一个或多个按钮的同时移动鼠标。
    NSLeftMouseDragged当鼠标左键向下移动或两个按钮都向下移动时,会生成事件,NSRightMouseDragged当鼠标仅右键向下移动时,会生成事件,当设备有两个以上按钮时,会生成NSOtherMouseDragged
    带有单个按钮的鼠标只会生成鼠标左键拖动事件。
    一系列鼠标拖动事件总是在鼠标向下事件之前,然后是鼠标向上事件。
  • NSMouseMoved
    用户在没有按住鼠标按钮的情况下移动了鼠标。
    鼠标移动的事件通常不会被跟踪,因为它们会很快淹没事件队列;使用NSWindow方法setAcceptsMouseMovedEvents:打开鼠标移动的跟踪。
  • NSScrollWheel
    用户操纵了鼠标的滚轮。
    使用NSEvent方法deltaXdeltaYdeltaZ找出它移动了多少。
    如果鼠标没有滚轮,则永远不会生成此事件。

注意: 从OS X v10.5开始,NSScrollWheel事件被发送到鼠标下的窗口,无论该窗口是活动的还是非活动的。
在操作系统的早期版本中,只有当该窗口具有键焦点时,滚轮事件才会发送到鼠标下的窗口(实用程序窗口除外,它接收这些事件,即使它们不活动)。

只要用户继续移动鼠标,就会重复生成鼠标拖动和鼠标移动事件。
如果鼠标是静止的,则在鼠标再次移动之前不会生成任何类型的事件。


重要提示: Application Kit不提供对三键鼠标的第三个按钮生成的事件的任何默认处理(类型NSOtherMouseDownNSOtherMouseDraggedNSOtherMouseUp)。

有关鼠标事件的更多信息,请参阅处理鼠标事件。


鼠标跟踪事件

因为精确跟踪鼠标的移动是一项昂贵的操作,所以Application Kit提供了一种不太密集的机制来跟踪鼠标的位置。
它通过允许应用程序定义窗口的区域(称为跟踪矩形)来实现这一点,这些区域在光标进入或离开时生成事件。
事件类型是NSMouseEnteredNSMouseExited,它们是在应用程序要求窗口服务器在窗口中设置跟踪矩形时生成的,通常使用NSTrackingArea对象或NSView方法addTrackingRect:owner:userData:assumeInside:.一个窗口可以有任意数量的跟踪矩形;NSEvent方法trackingNumber标识触发事件的矩形。

一种特殊的埋点是NSCursorUpdate事件。
这种类型用于实现NSView类的光标矩形机制。
当光标越过预定义矩形区域的边界时,会生成NSCursorUpdate事件。
NSApplication通常处理NSCursorUpdate事件并且不分派它们。

有关详细信息,请参阅使用跟踪区域对象。


2.2 关键事件

发送到应用程序的最常见事件是用户键盘操作的直接报告,由以下NSEventType常量标识:

  • NSKeyDown
    用户通过按键生成一个或多个字符。
  • NSKeyUp
    用户释放了一个密钥。
    此事件之前始终有一个NSKeyDown事件。
  • NSFlagsChanged
    用户按下或释放修改键,或打开或关闭大写锁定。

其中,键下事件对应用程序最有用。
当事件的类型是NSKeyDown时,下一步通常是使用字符方法获取由键下生成的characters

键入事件的使用频率较低,因为当发生键入事件时,它们几乎会自动跟随。
而且由于无论事件类型如何,NSEventmodifierFlags方法都会返回修改器键的状态,因此应用程序通常不需要接收标志更改的事件;它们仅对必须始终跟踪这些键状态的应用程序有用。

一些按键生成的按键事件不表示要作为文本插入的字符。
相反,它们表示等效键、键盘界面控制命令或键盘操作。
等效键和键盘界面控制命令通常由应用程序对象处理,并且不调用与NSKeyDown事件关联的NSResponder方法keyDown:
有关详细信息,请参阅键事件的路径。

有关关键事件的详细信息,请参阅处理关键事件。


2.3 平板电脑事件

平板设备生成Cocoa应用程序作为NSEvent对象接收的低级事件。
以下部分描述平板设备、平板事件的特征以及应用程序套件如何支持平板事件。

重要提示:平板电脑事件在OS X v10.4及更高版本的操作系统中可用。


平板设备概述

带有触控笔的平板电脑是一种输入设备,它产生的数据比鼠标更准确、更详细。
它使用户能够通过在表面(平板电脑)上操纵触控笔来绘图、书写或做出选择;然后应用程序可以捕捉和处理这些动作,并在其用户交互界面中反映它们。
平板电脑通常是连接到计算机系统的USB设备,触控笔是无线传感器。
信号从平板电脑发送到传感器,然后传感器将信号发送回平板电脑。
平板电脑使用该信号来确定传感器在平板电脑上的位置。
触控笔实际上可以是任何指示设备,例如钢笔、喷枪甚至冰球。

除了任何给定时刻的触控笔位置之外,触控笔传感器还可以报告许多其他数据,例如笔的倾斜、冰球的旋转,以及施加在触控笔上的压力。
压力尤其重要,因为只有这一小块数据,用户可以告诉应用程序改变正在绘制的线的厚度,或者它的不透明度,或者它的颜色。
一些触控笔设备还有按钮,可以为应用程序提供额外的信息。

OS X支持来自多个制造商的平板设备。
其中一些平板电脑可以同时响应其表面上的多个定点设备。


平板电脑事件的类型

Application Kit中有两种类型的平板电脑事件:接近事件和指针事件。
以下部分描述了它们是什么,它们如何相互关联,以及典型平板电脑会话中接近事件和指针事件的顺序。


邻近事件

接近事件是指当指点传感器(例如触控笔)靠近或远离平板电脑表面时,平板电脑设备产生的事件。
它表示一系列相关指针事件(会话)的开始或结束。
接近事件是NSTabletProximity类型的NSEvent对象。
应用程序可以通过向事件对象发送isEnteringProximity来确定平板电脑指针会话是开始还是结束。

接近事件的主要目的是为应用程序提供一组标识符,用于将平板电脑硬件项目(整个平板电脑设备或单个传感器)与当前指向会话相关联。
接近平板电脑事件还可以为应用程序提供有关特定设备功能的信息。

接近型平板事件带有一组标识符和设备属性,应用程序可以使用访问器方法获取这些标识符和设备属性。
其中包括以下内容:

  • 设备ID-平板设备的主要标识符,用于将指针类型事件与邻近事件相关联。
    会话中的所有平板指针事件都具有相同的设备ID。
    访问器:deviceID
  • 指点设备-为了帮助应用程序区分指点设备,您可以向接近事件询问设备的序列号、类型(例如,笔或橡皮擦),以及支持多个并发指点设备的平板电脑的设备ID。
    访问器:pointingDeviceSerialNumberpointingDeviceTypepointingDeviceID
  • 平板电脑-您可以向接近事件询问平板电脑的标识符(即其USB型号),如果有多个平板电脑连接到系统,则询问其系统平板电脑ID。
    访问器:tabletIDsystemTabletID
  • 供应商信息-具有邻近类型的NSEvent对象可能包含供应商的标识符和供应商选择的设备中的指示设备的标识符。
    访问:vendorIDvendorPointingDeviceType
  • 功能-位掩码,其设置位指示平板设备的功能。
    它是特定于供应商的。
    访问器:capabilityMask

通常,当应用程序接收到接近事件时,它会存储设备ID和任何其他标识符,这些标识符是区分会话中涉及的平板电脑硬件的各种项目所需要的。
然后,它在处理指针事件时引用这些标识符,以确保它正在处理正确的事件。
应用程序还可以从接近事件中提取设备信息(例如,平板电脑功能或指针类型),并使用这些信息来配置它如何处理指针事件。


指针事件

指针事件是触控笔接近平板电脑后,平板电脑设备产生的事件。
它表示传感器状态的变化。
例如,如果用户在平板电脑表面上移动触控笔传感器,或者增加压力或倾斜指点设备,就会产生指针事件。
指针事件是NSTabletPoint类型的NSEvent对象或表示鼠标向下、鼠标拖动或鼠标向上事件的对象,子类型为NSTabletPointEventSubtype

应用程序通常使用指针事件进行绘图或用户界面操作。
尽管您可以获得当前指针位置的绝对三维坐标,但这些坐标是全平板电脑分辨率的,需要您将它们缩放到屏幕位置。
使用NSEvent实例方法locationInWindowlocationInWindow``mouseLocation要简单得多。

除了指针位置,从指针事件中获得的信息还包括:

  • 压力,作为0.0到1.0之间的值;您可以使用press属性来设置颜色的不透明度。
    访问器:pressure
  • 旋转,以度为单位;您可以使用旋转属性来模拟书法笔。
    访问器:rotation
  • Tilt,一种NSPoint结构,两个轴的范围从-1到1;您可以使用tilt属性来提供不同的颜色,具体取决于倾斜的角度和方向。
    访问器:tilt
  • 切向压力,介于-1.0和1.0之间的值(仅在某些设备上)。
    访问器:tangentialPressure
  • 按下传感器按钮编号。
    访问器:buttonMask
  • 供应商定义的数据。
    访问者:vendorDefined

平板电脑事件的顺序

平板电脑接近事件表示一系列相关平板电脑指针事件的开始,随后的接近事件表示系列的结束。
因此,这两个接近事件为处理会话中的指针事件提供了一种框架。
第一个接近事件是当定点设备靠近平板电脑表面时产生的;第二个是当同一定点设备离开平板电脑的接近时产生的。

注意: 您可以通过向事件对象发送isEnteringProximity消息来确定接近事件是针对进入接近还是离开接近的指示设备。

接近事件和指针事件的序列对处理平板事件的应用程序具有特殊的意义。
由靠近平板的指示设备生成的平板事件让平板应用程序知道它应该存储标识符并为即将到来的平板会话设置配置变量。
应用程序处理指针事件,直到它接收到第二个接近事件,这告诉它这些标识符和配置不再有效。
因此,平板会话是与特定一对接近事件相关联的指针事件序列。

只要只有一个指点设备在起作用,接近事件和指针事件之间的关系就简单明了。
但是一个平板电脑表面上可以同时有多个指点设备,或者多个平板电脑设备可以连接到一个系统。
在这种情况下,应用程序必须存储它在所有初始接近事件中接收到的标识符,并使用这些标识符来区分各种系列的指针事件。

以一个平板设备为例,它一次在平板电脑表面上支持多个指针设备。
一个指针设备可能是用于线条绘制的触控笔;另一个指针设备可能应用喷枪绘画效果。
如图2-1所示,当绘图触控笔靠近平板电脑表面时,会生成一个接近事件。


图2-1 指针A靠近平板电脑,产生接近事件

在这里插入图片描述


在处理此接近事件时,应用程序会存储平板设备的设备ID、指点设备的设备ID,也许还会存储指点设备的序列号及其类型。
在收到具有这些相同标识符的下一个接近事件之前,它会处理为指点设备接收到的所有指针事件——在本例中是画线(图2-2)。


图2-2 应用程序接收指针事件

在这里插入图片描述


现在喷枪指示设备移动到平板电脑的表面上,产生另一个接近事件(图2-3)。
因为这个事件带有不同的标识符,所以应用程序知道它不是第一个平板电脑会话的终止接近事件。
相反,它宣布了另一个指点设备即将发生的一系列指针事件。
因此应用程序存储了这个会话的标识符和配置信息。


图2-3指针B靠近平板电脑,产生接近事件

在这里插入图片描述


在一段时间内,平板电脑应用程序正在处理两个不同系列的指针事件,使用存储的标识符来区分这两个事件。
然后,如图2-4所示,绘图指示设备离开平板电脑表面。
此操作生成一个接近事件。
应用程序检查该事件,发现指示设备的标识符与它最初为线条绘制设备存储的标识符相同。
它会取消存储的标识符,从而结束初始系列的指针事件。


图2-4指针A离开平板电脑表面,产生接近事件

在这里插入图片描述


2.4 其他类型的事件

Application Kit定义了几种次要事件类型。
其中一些很少使用,但如果您发现需要它们,它们是可用的。

周期性事件(类型为NSPeriodic)只是通知应用程序某个时间间隔已经过去。
周期性事件在没有生成输入事件但您希望生成输入事件的情况下特别有用。
例如,当用户将鼠标按住滚动按钮但不移动它时,鼠标按下事件后不会生成任何事件。
然后,Application Kit的滚动机制启动并使用周期性事件流来保持文档以合理的速度滚动,直到用户释放鼠标。
当鼠标向上事件发生时,滚动机制终止周期性事件流。

您可以使用NSEvent类方法startPeriodicEventsAfterDelay:withPeriod:来生成周期性事件,并将它们以特定的频率放置在事件队列中。
当您不再需要它们时,通过调用stopPeriodicEvents
与键和鼠标事件不同,周期性事件不会分派到窗口对象。
应用程序必须使用NSApplication方法nextEventMatchingMask:untilDate:inMode:dequeue:,通常在模态循环中。
应用程序只能为每个线程激活一个周期性事件流。
您使用周期性事件而不是计时器,因为NSPeriodic事件与其他事件一起传递;这使得响应器对象在鼠标向上和鼠标拖动事件的同时查找周期性事件。
例如在滚动期间完成。

其余的事件类型——NSAppKitDefinedNSSystemDefinedNSApplicationDefined——结构较少,只包含泛型子类型和数据字段。
在这三种杂项事件类型中,只有NSApplicationDefined对应用程序真正有用。
它允许应用程序生成自定义事件并将它们插入事件队列。
每个这样的事件都可以有一个子类型和两个额外的代码来区分它与其他事件。
NSEvent方法otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:创建这些事件之一,subtypedata1data2方法返回特定于这些事件的信息。


四、事件处理基础

在事件处理代码中发现的一些任务对于不止一种类型的事件是通用的。
以下部分描述了这些基本的事件处理任务。
本章中介绍的一些信息在创建自定义视图的视图编程指南一章中进行了详细讨论,但使用了不同的示例。


1、为接收事件准备自定义视图

尽管任何类型的响应对象都可以处理事件,但NSView对象是迄今为止最常见的事件接收者。
它们通常是第一个响应鼠标事件和键事件的对象,通常会根据这些事件更改它们的外观。
许多Application Kit类被实现来处理事件,但NSView本身并没有。
当您创建自己的自定义视图并希望它响应事件时,您必须向其添加事件处理功能。

为此所需的最低限度步骤很少:

  • 如果您的自定义视图应该是键事件或操作消息的第一响应者,请覆盖 acceptsFirstResponder
  • 实现一个或多个NSResponder方法来处理特定类型的事件。
  • 如果您的视图要处理通过响应者链传递给它的操作消息,请实现适当的操作方法。

作为第一响应者的视图在窗口中的其他对象之前接受键事件和操作消息。
它通常也参与键循环(参见键盘接口控制)。
默认情况下,视图对象通过在NO中返回acceptsFirstResponder来拒绝第一响应者状态。
如果您希望自定义视图响应键事件和操作,您的类必须覆盖此方法以返回YES

- (BOOL)acceptsFirstResponder {
    return YES;
}

注意:为了响应关键事件或操作消息而成为第一响应者的视图应通过在其周围绘制焦点环来反映此状态。
焦点环指示对象是关键事件的当前第一响应者。

上面列表中的第二项——覆盖NSResponder方法来处理事件——当然是一个很大的主题,也是本文档中大多数剩余章节的内容。
但是对于事件消息,有一些基本准则需要考虑:

  • 如有必要,检查传入的NSEvent对象以验证它是否是您处理的事件,如果是,请找出如何处理它。
    调用适当的NSEvent方法来帮助确定。
    例如,您可能会看到按下了哪些修饰符键(modifierFlags),找出鼠标按下事件是双击还是三击(clickCount),或者,对于键事件,获取关联的字符(characterscharactersIgnoringModifiers)。
  • 如果NSResponder方法(如mouseDown:)的实现完全处理事件,则不应调用该方法的超类实现。
    NSView继承了用于处理鼠标事件的方法的NSResponder实现;在这些方法中,NSResponder只是将消息向上传递到响应器链。
    响应器链中的这些对象之一可以实现一个方法,以确保您的自定义视图不会看到后续相关的鼠标事件。
    因此,自定义NSView对象不应该在它们的NSRespondermouse-event-handling方法的实现中调用super,例如mouseDown:mouseDragged:mouseUp:除非已知继承的实现提供了一些所需的功能。
  • 如果您不处理事件,则将其向上传递到响应者链。
    尽管您可以直接将消息转发给下一个响应者,但通常最好将消息转发给您的超类。
    如果您的超类不处理事件,它会将消息转发给它的超类,依此类推,直到到达NSResponder
    默认情况下,NSResponder将所有事件消息向上传递到响应者链。
    例如,而不是这样:
- (void)mouseDown:(NSEvent *)theEvent {
    // determine if I handle theEvent
    // if not...
    [[self nextResponder] mouseDown:theEvent];
}

这样做:

- (void)mouseDown:(NSEvent *)theEvent {
    // determine if I handle theEvent
    // if not...
    [super mouseDown:theEvent];
}

  • 如果您的子类有时需要处理特定的事件——也许只有一些键入的字符——那么它必须覆盖事件方法来处理它感兴趣的案例,否则调用超类实现。
    这允许超类捕获它感兴趣的案例,并最终允许事件在未处理的情况下沿着响应链继续前进。
  • 如果您故意想绕过NSResponder方法的超类实现-假设您的超类是NSForm-并且向上传递响应器链的事件,请将消息重新发送到[self nextResponder]

2、实施行动方法

动作消息通常由NSMenuItem对象或NSControl对象发送。
后者通常与一个或多个NSCell对象一起工作。
单元格对象存储一个方法选择器,标识要发送的动作消息和对目标对象的引用。
(菜单项封装了它自己的动作和目标数据。)当菜单项或控件对象被单击或以其他方式操作时,它会从控件的一个单元格中获取动作选择器和目标对象,并将消息发送给目标。

注意: 有关非目标操作消息及其占用响应者链的路径的信息,请参阅操作消息和操作消息的响应者链。

您可以分别使用方法setAction:setTarget:(由NSActionCellNSMenuItem和其他类声明)以编程方式设置操作选择器和目标。
但是,您通常会在Interface Builder中指定这些。
在此应用程序中,您通过从控制对象拖动到目标,然后选择要调用的目标的操作方法,将控制对象连接到nib文件中的另一个对象(目标)。
如果您希望操作消息没有目标,您可以通过编程方式将目标设置为nil或者在Interface Builder中,在菜单项或控件与nib文件窗口中的第一响应器图标之间建立连接,如图3-1所示。


图3-1在Interface Builder中连接非目标操作

在这里插入图片描述


从Interface Builder中,您可以为Xcode项目生成头文件和实现文件,其中分别包含为类定义的每个操作方法的声明和骨架实现。
这些Interface Builder定义的方法的返回“值”为IBAction,它充当标记以指示目标操作连接存档在nib文件中。
您还可以自己添加操作方法的声明和骨架实现;在这种情况下,返回类型为void。)签名的其余必需部分是一个参数,类型为id,并按惯例命名为sender


例3-1说明了一个操作方法的直接实现,该方法在用户单击按钮时切换时钟的AM-PM指示器。


例3-1 action方法的简单实现

- (IBAction)toggleAmPm:(id)sender {
    [self incrementHour:12 andMinute: 0];
}

动作方法与NSResponder事件方法不同,没有默认实现,因此响应者子类不应该盲目地将动作消息转发给super
在应用程序工具包中,向上传递动作消息仅仅取决于对象是否响应该方法,而不是传递事件消息。
当然,如果您知道超类确实实现了该方法,您可以从您的子类向上传递它,否则不要这样做。

操作消息的一个重要特性是,您可以将消息发送回sender以获得进一步的信息或相关数据。
例如,给定菜单中的菜单项可能表示分配给它们的对象;例如,标题为“红色”的菜单项可能有一个表示对象,该表示对象是一个NSColor对象。
您可以通过向sender发送representedObject来访问该对象。

您可以通过动态更改sender的目标、动作、标题和类似属性,将动作方法的回发消息功能更进一步。
下面是一个简单的测试用例:您想用一个按钮控制一个进度指示器(一个NSProgressIndicator对象);单击按钮启动指示器并将按钮的标题更改为“停止”,然后下一次单击停止指示器并将标题更改为“开始”。
例3-2显示了一种方法。

示例3-2 sender的重置目标和动作-良好的实现

- (IBAction)controlIndicator:(id)sender
{
    [[sender cell] setTarget:indicator]; // indicator is NSProgressIndicator
    if ( [[sender title] isEqualToString:@"Start"] ) {
        [[sender cell] setAction:@selector(startAnimation:)];
        [sender setTitle:@"Stop"];
    } else {
        [[sender cell] setAction:@selector(stopAnimation:)];
        [sender setTitle:@"Start"];
    }
    [[sender cell] performClick:self];
    // set target and action back to what they were
    [[sender cell] setAction:@selector(controlIndicator:)];
    [[sender cell] setTarget:self];
}

但是,此实现需要将目标和操作信息设置回通过performClick:发送重定向操作消息后的状态。
您可以通过直接调用应用程序对象(NSApp)用于分派操作消息的方法sendAction:to:from:来简化此实现(参见例3-3)。


例3-3sender的重置目标和动作-更好的实现

- (IBAction)controlIndicator:(id)sender
{
    SEL theSelector;
    if ( [[sender title] isEqualToString:@"Start"] ) {
        theSelector = @selector(startAnimation:);
        [sender setTitle:@"Stop"];
    } else {
        theSelector = @selector(stopAnimation:);
        [sender setTitle:@"Start"];
    }
    [NSApp sendAction:theSelector to:indicator from:sender];
}

在键盘动作消息中,通过键绑定机制解释特定的按键动作,就会调用动作方法。
因为这些消息与特定的键事件紧密相连,所以动作方法的实现可以通过向NSApp发送currentEvent来获取事件,然后查询NSEvent对象以获取详细信息。
例3-7给出了这种技术的一个例子。
有关键盘动作消息的摘要,请参见键事件的路径;有关该机制的描述,请参见键绑定。


3、获取事件的位置

您可以通过将locationInWindow发送到NSEvent对象来获取鼠标或平板指针事件的位置。
但是,正如该方法的名称所示,该位置(NSPoint结构)位于窗口的基本坐标系中,而不是在通常处理该事件的视图的坐标系中。
因此,视图必须使用方法convertPoint:fromView:将点转换为自己的坐标系,如例3-4所示。


例 3-4 Converting a mouse-dragged location to be in a view’s coordinate system

- (void)mouseDragged:(NSEvent *)event {
    NSPoint eventLocation = [event locationInWindow];
    center = [self convertPoint:eventLocation fromView:nil];
    [self setNeedsDisplay:YES];
}

第二个convertPoint:fromView:nil,表示转换是从窗口的基本坐标系。

请记住,locationInWindow方法不适用于键事件、周期性事件或除鼠标和平板指针事件之外的任何其他类型的事件。


4、测试事件类型和修改器标志

有时,您可能需要发现事件的类型。
但是,您不需要在NSResponder的事件处理方法中执行此操作,因为事件的类型从方法名称中是显而易见的:rightMouseDragged:keyDown:tabletProximity:,等等。
但是在应用程序的其他地方,您始终可以通过发送currentEventNSApp来获取当前处理的事件。
要找出这是什么类型的事件,请发送typeNSEvent对象,然后将返回值与NSEventType常量之一进行比较。
例3-5给出了一个例子。


例3-5事件类型测试

NSEvent *currentEvent = [NSApp currentEvent];
NSPoint mousePoint = [controlView convertPoint: [currentEvent locationInWindow] fromView:nil];
switch ([currentEvent type]) {
    case NSLeftMouseDown:
    case NSLeftMouseDragged:
        [self doSomethingWithEvent:currentEvent];
        break;
    default:
        // If we find anything other than a mouse down or dragged we are done.
         return YES;
}

在事件处理方法中执行的一个常见测试是确定特定的修饰符键是否与按键、鼠标单击或类似的用户操作同时按下。
修饰符键通常赋予事件特殊的意义。
例3-6中的代码示例显示了mouseDown:的实现,它确定单击鼠标时命令键是否被按下。
如果是,它将接收器(视图)旋转90度。
修饰符键的识别需要事件处理程序向传入的事件对象发送modifierFlags,然后使用NSEvent.h中声明的一个或多个修饰符掩码常量对返回值执行按位与运算。


例3-6测试按下的修饰符键-事件方法

- (void)mouseDown:(NSEvent *)theEvent {
 
    //  if Command-click rotate 90 degrees
    if ([theEvent modifierFlags] & NSCommandKeyMask) {
        [self setFrameRotation:[self frameRotation]+90.0];
        [self setNeedsDisplay:YES];
    }
}

您可以测试一个事件对象,通过使用修改器掩码常量执行按位或来确定是否按下了一组修改器键中的任何一个,如例3-7所示。
(另请注意,此示例显示了在键盘操作方法中使用currentEvent方法。)


例3-7测试按下的修饰符键-action方法

- (void)moveLeft:(id)sender {
    // Use left arrow to decrement the time.  If a shift key is down, use a big step size.
    BOOL shiftKeyDown = ([[NSApp currentEvent] modifierFlags] &
        (NSShiftKeyMask | NSAlphaShiftKeyMask)) !=0;
    [self incrementHour:0 andMinute:-(shiftKeyDown ? 15 : 1)];
}

此外,您可以通过将单个修饰符-键测试与逻辑AND运算符(&&)链接在一起来查找某些修饰符-键组合。
例如,如果例3-6中的示例方法要查找Command d-Shift-Click而不是Command d-Click,则完整的测试如下所示:

if ( ( [theEvent modifierFlags] & NSCommandKeyMask ) &&
        ( [theEvent modifierFlags] & NSShiftKeyMask ) )

除了测试单个事件类型的NSEvent对象之外,您还可以将从事件队列中获取的事件限制为指定的类型。
您可以在nextEventMatchingMask:untilDate:inMode:dequeue:method(NSApplicationandNSWindow)或NSWindownextEventMatchingMask:方法中执行此事件类型过滤。
所有这些方法的第二个参数采用NSEvent.h中声明的一个或多个事件类型掩码常量-例如NSLeftMouseDraggedMaskNSFlagsChangedMaskNSTabletProximityMask
您可以单独指定这些常量,也可以对它们执行按位或。

因为nextEventMatchingMask:untilDate:inMode:dequeue:方法几乎只在封闭循环中用于处理一系列相关的鼠标事件,所以在处理鼠标事件中描述了它的使用。


5、响应者相关任务

以下部分描述了与对象的第一响应者状态相关的任务。


确定第一响应者状态

通常情况下,NSResponder对象总是可以通过询问它的窗口(或者询问它自己,如果它是一个NSWindow对象)来确定它当前是否是第一个响应者,然后将它自己与该对象进行比较。
您可以通过向NSWindow对象发送firstResponder消息来询问第一个响应者。
对于NSView对象,此比较类似于以下代码:

if ([[self window] firstResponder] == self) {
  // do something based upon first-responder status
}

这个简单场景的一个复杂之处在于文本字段。
当文本字段具有输入焦点时,它不是第一个响应者。
相反,窗口的字段编辑器是第一个响应者;如果您将firstResponder发送到NSWindow对象,则返回一个NSTextView对象(字段编辑器)。
要确定给定的NSTextField当前是否处于活动状态,请从窗口中检索第一个响应者并找出它是一个NSTextView对象,以及它的委托是否等于NSTextField对象。
例3-8展示了如何做到这一点。


例3-8确定文本字段是否是第一响应者

if ( [[[self window] firstResponder] isKindOfClass:[NSTextView class]] &&
   [window fieldEditor:NO forObject:nil] != nil ) {
        NSTextField *field = [[[self window] firstResponder] delegate];
        if (field == self) {
            // do something based upon first-responder status
        }
}

字段编辑器正在编辑的控件始终是字段编辑器的当前委托,因此(如示例所示)您可以通过请求字段编辑器的委托来获取文本字段。
有关字段编辑器的更多信息,请参阅文本字段、文本视图和字段编辑器。


设置第一响应者

您可以通过向NSWindow对象发送makeFirstResponder:来以编程方式更改第一响应者;此消息的参数必须是响应者对象(即继承自NSResponder的对象)。
此消息启动一种协议,其中一个对象失去其第一响应者状态,另一个获得它。

  1. makeFirstResponder:总是询问当前的第一响应者是否准备好通过发送resignFirstResponder来放弃其状态。
  2. 如果当前第一个响应者在发送此消息时返回NO,则makeFirstResponder:失败并同样返回NO
    视图对象或其他响应者可能会出于多种原因拒绝退出第一响应者状态,例如当操作不完整时。
  3. 如果当前第一响应者返回YESresignFirstResponder,则向新的第一响应者发送becomeFirstResponder消息,通知它可以是第一响应者。
  4. 这个对象可以返回NO来拒绝分配,在这种情况下,NSWindow本身成为第一个响应者。

图3-2和图3-3说明了该协议的两种可能结果。


图3-2使视图成为第一响应者-当前视图拒绝退出状态

在这里插入图片描述


图3-3使视图成为第一响应者-新视图成为第一响应者

在这里插入图片描述


例3-9显示了一个自定义NSCell类(在本例中是NSActionCell的子类)实现resignFirstResponderbecomeFirstResponder来操作其超类的键盘焦点环。


例3-9辞职并成为第一响应者

- (BOOL)becomeFirstResponder {
    BOOL okToChange = [super becomeFirstResponder];
    if (okToChange) [self setKeyboardFocusRingNeedsDisplayInRect: [self bounds]];
    return okToChange;
}
 
- (BOOL)resignFirstResponder {
    BOOL okToChange = [super resignFirstResponder];
    if (okToChange) [self setKeyboardFocusRingNeedsDisplayInRect: [self bounds]];
    return okToChange;
}

您还可以设置窗口的初始第一响应者,即窗口首次放置在屏幕上时设置的第一响应者。
您可以通过向NSWindow对象发送setInitialFirstResponder:以编程方式设置初始第一响应者。
您也可以在Interface Builder中创建用户交互界面时进行设置。
为此,请完成以下步骤。

  1. 控制从nib文件窗口中的窗口图标拖动到用户交互界面中的NSView对象。
  2. 在检查器的连接窗格中,选择initialFirstResponder出口,然后单击连接。

在对nib文件中的所有对象调用awakeFromNib后,NSWindow将第一个响应者设置为nib文件中的初始第一个响应者。
请注意,您不应该向awakeFromNib中的NSWindow对象发送setInitialFirstResponder:并期望消息有效。


五、处理鼠标事件

鼠标事件是应用程序处理的两种最常见的事件之一(当然,另一种是键事件)。
鼠标点击——涉及用户按下然后释放鼠标按钮——通常表示选择,但选择的含义取决于响应事件的对象。
例如,鼠标点击可以告诉响应对象更改其外观,然后发送操作消息。
鼠标拖动通常表示接收视图应该在其范围内移动自身或绘制的对象。
以下部分描述了如何处理鼠标向下、鼠标向上和鼠标拖动事件。

注意: 本章讨论NSWindow对象通过事件分派机制传递给其视图的鼠标事件:鼠标向下、鼠标向上、鼠标拖动和(在较小程度上)鼠标移动事件。
它没有描述如何处理鼠标输入和鼠标退出事件,这些事件(可能与鼠标移动事件一起)用于鼠标跟踪。
有关此主题的信息,请参阅使用跟踪区域对象。


1、鼠标事件概述

在进入鼠标事件处理的“操作方法”之前,让我们回顾一下事件架构、事件对象和类型以及事件处理基础中讨论的鼠标事件的一些核心事实:

  • 鼠标事件由NSWindow对象分派到发生事件的NSView对象。
  • 如果该视图对象接受第一响应者状态,则后续的键事件将被分派到该视图对象。
  • 如果用户单击不在键窗口中的视图,则默认情况下窗口向前移动并制作键,但不分派鼠标事件。
  • 鼠标事件是与按下的鼠标按钮(左、右、其他)和鼠标按钮上的动作性质相关的各种事件类型。
    每种事件类型又与NSResponder方法相关,如表4-1中左键鼠标事件所示。
行动事件类型(鼠标左键)调用鼠标事件方法(鼠标左键)
按下按钮NSLeftMouseDownmouseDown:
按下按钮时移动鼠标NSLeftMouseDraggedmouseDragged:
松开按钮NSLeftMouseUpmouseUp:
不按任何按钮移动鼠标NSMouseMovedmouseMoved:

注意: 因为鼠标移动事件发生得如此频繁,以至于它们会迅速淹没事件调度机制,默认情况下,NSWindow对象不会从全局NSApplication对象接收它们。
但是,您可以通过向NSWindow对象发送参数为YESsetAcceptsMouseMovedEvents:消息来请求这些事件。

鼠标右键事件由Application Kit定义以打开上下文菜单,但您可以在必要时覆盖此行为(例如,在rightMouseDown:中)。
Application Kit没有为第三个(“其他”)鼠标按钮定义任何默认行为。

  • 鼠标事件的一般顺序是:鼠标向下、鼠标拖动(多次)、鼠标向上。
    如果打开了它们的分派,则鼠标移动事件可能只发生在鼠标上移和下一次鼠标下移之间。
    它们不会发生在鼠标下移和随后的鼠标上移之间。
  • 鼠标事件(如NSEvent对象)可以有子类型。
    目前,这些子类型仅限于平板电脑事件(尤其是平板电脑指针事件),但将来可能会添加更多子类型。
    (有关详细信息,请参阅平板电脑事件。)

2、处理鼠标点击

在处理鼠标按下事件时,最早要考虑的事情之一是接收NSView对象是否应该成为第一个响应者,这意味着它将是后续键事件和操作消息的第一个候选者。
处理用户可以选择的图形元素(例如绘制形状或文本)的视图通常应该通过覆盖返回YESacceptsFirstResponder方法 来接受鼠标按下事件的第一个响应者状态,如为接收事件准备自定义视图中所述。

默认情况下,不是键窗口的窗口中的鼠标向下事件只是将窗口向前移动并使其成为键;该事件不会发送到发生鼠标单击的NSView对象。
然而,NSView可以通过覆盖acceptsFirstMouse:返回YES来声明初始鼠标向下事件。
此方法的参数是非键窗口中发生的鼠标向下事件,视图对象可以检查该事件以确定它是否想要接收鼠标事件并可能成为第一响应者。
例如,您希望此方法的默认行为在影响窗口中选定对象的控件中。
但是,在某些情况下,覆盖此行为是合适的,例如对于应该接收的控件mouseDown:即使窗口处于非活动状态时也会发送消息。
支持这种单击行为的控件示例是窗口的标题栏按钮。

NSResponder方法的实现中,您通常要做的第一件事是检查传入的NSEvent对象,以确定是否要处理该事件。
如果是要处理的事件,那么您可能需要从NSEvent对象中提取信息来帮助您处理它。
具体来说,您可以从NSEvent对象中获取以下信息:

  • 以基本坐标(locationInWindow)获取鼠标事件的位置,然后将此位置转换为接收视图的坐标系;有关详细信息,请参阅获取事件的位置。
  • 查看单击鼠标按钮时是否按下了任何修改器键(modifierFlags);此过程在测试事件类型和修改器标志中描述。
    应用程序可以定义修改器键来更改鼠标事件的重要性。
  • 找出快速连续发生了多少次鼠标点击(clickCount);多次鼠标点击在概念上被视为狭窄时间阈值内的单个鼠标按下事件(尽管它们以一系列mouseDown:消息的形式到达)。
    与修改键一样,双击或三击可以更改鼠标事件对应用程序的重要性。
    (有关示例,请参见例4-3。)
  • 如果鼠标单击之间的间隔很重要,您可以向NSEvent对象发送timestamp并记录每个事件发生的时刻。
  • 如果后续事件之间鼠标位置的变化很重要,您可以分别使用deltaXdeltaXdeltaY找出x坐标和y坐标的增量值。

Application Kit中的许多视图对象(如控件和菜单项)会响应鼠标按下事件而改变其外观,有时直到随后的鼠标按下事件。
这样做可以向用户提供视觉确认,即他们的操作是有效的,或者现在选择了单击的对象。
例4-1显示了一个简单的例子。


例4-1鼠标单击的简单处理-更改视图的外观

- (void)mouseDown:(NSEvent *)theEvent {
    [self setFrameColor:[NSColor redColor]];
    [self setNeedsDisplay:YES];
}
 
- (void)mouseUp:(NSEvent *)theEvent {
    [self setFrameColor:[NSColor greenColor]];
    [self setNeedsDisplay:YES];
}
 
- (void)drawRect:(NSRect)rect {
    [[self frameColor] set];
    NSRectFill(rect);
}

但是许多视图对象,尤其是控件和单元格,不仅仅是为了响应鼠标点击而改变它们的外观。
一种常见的范例是被点击的视图向目标对象发送操作消息(其中操作和目标都是视图的可设置属性)。
如例4-2所示,视图通常在mouseUp:而不是mouseDown:上发送消息,从而使用户有机会在点击中途改变主意。


例4-2鼠标单击的简单处理-发送操作消息

- (void)mouseDown:(NSEvent *)theEvent {
    [self setFrameColor:[NSColor redColor]];
    [self setNeedsDisplay:YES];
}
 
- (void)mouseUp:(NSEvent *)theEvent {
    [self setFrameColor:[NSColor greenColor]];
    [self setNeedsDisplay:YES];
    [NSApp sendAction:[self action] to:[self target] from:self];
}
 
- (SEL)action {return action; }
 
- (void)setAction:(SEL)newAction {
    action = newAction;
}
 
- (id)target { return target; }
 
- (void)setTarget:(id)newTarget {
    target = newTarget;
}

例4-3给出了一个更复杂的真实示例。(它来自Sketch应用程序的示例项目。)
mouseDown:的实现确定用户是否双击了图形对象,如果双击了,则启用该对象的编辑。
否则,如果选择了调色板对象,它会在鼠标单击的位置创建该对象的实例。


例4-3处理鼠标向下事件-Sketch应用程序

- (void)mouseDown:(NSEvent *)theEvent {
    Class theClass = [[SKTToolPaletteController sharedToolPaletteController] currentGraphicClass];
    if ([self editingGraphic]) {
        [self endEditing];
    }
    if ([theEvent clickCount] > 1) {
        NSPoint curPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
        SKTGraphic *graphic = [self graphicUnderPoint:curPoint];
        if (graphic && [graphic isEditable]) {
            [self startEditingGraphic:graphic withEvent:theEvent];
            return;
        }
    }
    if (theClass) {
        [self clearSelection];
        [self createGraphicOfClass:theClass withEvent:theEvent];
    } else {
        [self selectAndTrackMouseWithEvent:theEvent];
    }
} 

实现控件的Application Kit的类为您管理此目标操作行为。


3、处理鼠标拖动操作

鼠标向下和鼠标向上事件是与鼠标点击相关的事件,并不是应用程序中唯一分派的鼠标事件类型。
当用户在按下鼠标按钮时移动鼠标时,视图也可以接收鼠标拖动事件。
视图通常将鼠标拖动事件解释为通过更改其框架位置来移动自身或通过重绘来移动其边界内的区域的命令。
然而,鼠标拖动事件的其他解释是可能的;例如,视图可以通过放大鼠标指针拖动的区域来响应鼠标拖动事件。

使用Application Kit,您可以在处理鼠标拖动事件时采用两种通用方法之一。
第一种方法是覆盖三个NSResponder方法mouseDown:mouseDragged:mouseUp:(用于鼠标左键操作)。
对于每个拖动序列,Application Kit发送一个mouseDown:消息到响应对象,然后发送一个或多个mouseDragged:消息,并以mouseUp:消息结束序列。
三方法方法描述了这种方法。

另一种方法将拖动序列中的鼠标事件视为单个事件,从鼠标向下,通过拖动,再到鼠标向上。
为此,响应对象通常必须通过进入事件跟踪循环来短路应用程序的正常事件循环,以仅过滤和处理感兴趣的鼠标事件。
例如,NSButton对象在鼠标向下事件时突出显示自己,然后在拖动期间跟随鼠标位置,当鼠标在内部时突出显示,当鼠标在外部时取消突出显示。
如果鼠标在鼠标向上事件中处于内部,则按钮对象发送其操作消息。
这种方法在鼠标跟踪循环方法.

这两种方法都有其优点和缺点。
建立鼠标跟踪循环可以让您更好地控制拖动操作期间其他事件与应用程序交互的方式。
但是,应用程序的主线程在事件跟踪循环期间无法处理任何其他请求,计时器可能不会按预期触发。
鼠标跟踪方法更有效,因为它通常需要更少的代码,并允许所有拖动变量都是本地的。
然而,如果子类不重新实现所有拖动代码,实现它的类变得更难扩展。

在编写事件驱动的应用程序时,实现单独的mouseDown:mouseDragged:mouseUp:方法通常是更好的设计选择。
每个方法都有明确定义的范围,这通常会导致更清晰的代码。
这种方法还使子类更容易覆盖处理鼠标向下、鼠标拖动和鼠标向上事件的行为。
然而,这种技术可能需要更多的代码和实例变量。


三种方法

要处理鼠标拖动操作,您可以覆盖标记鼠标拖动操作的离散阶段的三个NSResponder方法:mouseDown:mouseDragged:mouseUp:(或者,对于鼠标右键拖动,rightMouseDown:rightMouseDragged:rightMouseUp:)。
鼠标拖动序列由一个mouseDown:消息组成,后跟(通常)多个mouseDragged:消息和一个结束mouseUp:消息。

实现这些方法的子类通常必须声明实例变量,以保存连续事件之间各种事物的变化值或状态。
这些事物可以是几何实体,如矩形或点(对应视图中的视图框架或区域),也可以是简单的布尔值,例如指示选择了一个对象。
mouseDown:方法中,视图通常会初始化任何与拖动相关的实例变量;在mouseDragged:方法中,它可能会在执行操作之前更新这些实例变量或检查它们;在mouseUp:方法中,它通常会将这些实例变量重置为初始值。

因为鼠标拖动操作经常在用户拖动对象的增量变化位置重绘对象,所以三个鼠标拖动方法的实现通常需要在视图的坐标系中找到每个鼠标事件的位置。
如获取事件的位置中所述,这需要视图通过自动显示机制(假设该机制未关闭)发送locationInWindow到传入的NSEvent对象,然后使用convertPoint:fromView:方法将生成的位置转换为本地坐标系。
当位置或几何形状的变化需要视图重绘自身或自身的一部分时,视图使用setNeedsDisplay:setNeedsDisplayInRect:方法,稍后要求视图重绘自身(在drawRect:)。


例4-4演示了与拖动相关的实例变量的使用,获取局部坐标中的鼠标位置(并测试该位置是否在特定区域中),并标记接收视图的该区域以重新显示。


例4-4处理鼠标拖动操作-三方法方法

- (void)mouseDown:(NSEvent *)theEvent {
    // mouseInCloseBox and trackingCloseBoxHit are instance variables
    if (mouseInCloseBox = NSPointInRect([self convertPoint:[theEvent locationInWindow] fromView:nil], closeBox)) {
        trackingCloseBoxHit = YES;
        [self setNeedsDisplayInRect:closeBox];
    }
    else if ([theEvent clickCount] > 1) {
        [[self window] miniaturize:self];
        return;
    }
}
 
- (void)mouseDragged:(NSEvent *)theEvent {
    NSPoint windowOrigin;
    NSWindow *window = [self window];
 
    if (trackingCloseBoxHit) {
        mouseInCloseBox = NSPointInRect([self convertPoint:[theEvent locationInWindow] fromView:nil], closeBox);
        [self setNeedsDisplayInRect:closeBox];
        return;
    }
 
    windowOrigin = [window frame].origin;
 
    [window setFrameOrigin:NSMakePoint(windowOrigin.x + [theEvent deltaX], windowOrigin.y - [theEvent deltaY])];
}
 
- (void)mouseUp:(NSEvent *)theEvent {
    if (NSPointInRect([self convertPoint:[theEvent locationInWindow] fromView:nil], closeBox)) {
        [self tryToCloseWindow];
        return;
    }
    trackingCloseBoxHit = NO;
    [self setNeedsDisplayInRect:closeBox];
}


鼠标跟踪循环方法

用于处理鼠标拖动操作的鼠标跟踪技术应用于单个方法,通常(但不一定)在mouseDown:中。
实现响应对象首先声明并可能初始化一个或多个局部变量以在循环中使用。
这些变量中的一个通常包含一个值,通常是布尔值,用于控制循环。
当满足某些条件时,通常是收到鼠标启动事件时,变量值会更改;当下次通过循环测试此变量时,控制退出循环。

鼠标跟踪循环的中心方法是NSApplication方法nextEventMatchingMask:untilDate:inMode:dequeue:和同名的NSWindow方法。
这些方法从事件队列中获取由一个或多个类型掩码常量指定的类型的事件;对于鼠标拖动,这些常量通常是NSLeftMouseDraggedMaskNSLeftMouseUpMask
其他类型的事件留在队列中。
(两者的运行循环模式参数nextEventMatchingMask:untilDate:inMode:dequeue:方法应该是NSEventTrackingRunLoopMode。)

接收到鼠标向下事件后,在循环中获取后续鼠标事件nextEventMatchingMask:untilDate:inMode:dequeue:
处理NSLeftMouseDragged事件,就像在mouseDragged:方法中处理它们一样(在The Trio-method方法中描述);类似地,处理NSLeftMouseUp事件,就像在mouseUp:中处理它们一样。
通常鼠标向上事件表明执行控制应该脱离循环。


例4-5中的mouseDown:方法模板显示了一种可能的模态事件循环。


例4-5在鼠标跟踪循环中处理鼠标拖动-简单示例

- (void)mouseDown:(NSEvent *)theEvent {
    BOOL keepOn = YES;
    BOOL isInside = YES;
    NSPoint mouseLoc;
 
    while (keepOn) {
        theEvent = [[self window] nextEventMatchingMask: NSLeftMouseUpMask |
                NSLeftMouseDraggedMask];
        mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil];
        isInside = [self mouse:mouseLoc inRect:[self bounds]];
 
        switch ([theEvent type]) {
            case NSLeftMouseDragged:
                    [self highlight:isInside];
                    break;
            case NSLeftMouseUp:
                    if (isInside) [self doSomethingSignificant];
                    [self highlight:NO];
                    keepOn = NO;
                    break;
            default:
                    /* Ignore any other kind of event. */
                    break;
        }
 
    };
 
    return;
}

此循环转换鼠标位置并检查它是否在接收器内。
它使用虚构的highlight:方法突出显示自己,并且在接收到鼠标向上事件时,它调用doSomethingSignificant来执行重要操作。
自定义NSView对象可能会移动选定的对象,根据鼠标的位置绘制图形图像,等等,而不仅仅是突出显示。


例4-6是一个稍微复杂一点的示例,其中包括使用自动释放池和测试修饰符键。


例4-6在鼠标跟踪循环中处理鼠标拖动-复杂示例

- (void)mouseDown:(NSEvent *)theEvent
{
    if ([theEvent modifierFlags] & NSAlternateKeyMask)  {
        BOOL                dragActive = YES;
        NSPoint             location = [renderView convertPoint:[theEvent locationInWindow] fromView:nil];
        NSAutoreleasePool   *myPool = nil;
        NSEvent*            event = NULL;
        NSWindow            *targetWindow = [renderView window];
 
        myPool = [[NSAutoreleasePool alloc] init];
        while (dragActive) {
            event = [targetWindow nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)
                                untilDate:[NSDate distantFuture]
                                inMode:NSEventTrackingRunLoopMode
                                dequeue:YES];
            if(!event)
                continue;
            location = [renderView convertPoint:[event locationInWindow] fromView:nil];
            switch ([event type]) {
                case NSLeftMouseDragged:
                    annotationPeel = (location.x * 2.0 / [renderView bounds].size.width);
                    [imageLayer showLens:(annotationPeel <= 0.0)];
                    [peelOffFilter setValue:[NSNumber numberWithFloat:annotationPeel] forKey:@"inputTime"];
                    [self refresh];
                    break;
 
                case NSLeftMouseUp:
                    dragActive = NO;
                    break;
 
                default:
                    break;
            }
        }
        [myPool release];
    } else {
        // other tasks handled here......
    }
}

只有当用户实际移动鼠标时,才会驱动鼠标跟踪循环。
例如,如果用户按下鼠标按钮但从不移动鼠标本身,它就不起作用,从而导致持续滚动。
为此,您的循环应该使用NSEvent类方法startPeriodicEventsAfterDelay:withPeriod:,并在传递给nextEventMatchingMask:的掩码位字段中添加NSPeriodicMask
switch语句中,实现视图对象可以检查类型为NSPeriodic并采取它需要的任何操作——例如,滚动文档视图或移动动画中的步骤。
如果您需要在周期性事件期间检查鼠标位置,您可以使用NSWindow方法mouseLocationOutsideOfEventStream


在鼠标跟踪操作期间过滤掉关键事件

鼠标跟踪代码的一个潜在问题是用户在进行跟踪操作时按下命令的组合键,例如Command d-z(撤消)。
因为您的鼠标跟踪代码(在三方法方法或鼠标跟踪循环中)没有寻找该键事件,代码可能不知道如何处理该键命令,或者可能错误地处理它,从而产生不受欢迎的后果。

鼠标跟踪循环的问题可能不太明显,因为毕竟nextEventMatchingMask:untilDate:inMode:dequeue:循环确保只传递鼠标跟踪事件。
为什么键事件会成为问题?考虑例4-7中的代码。
当这个鼠标跟踪循环处于活动状态时,假设用户发出一些等效于键的命令。


例4-7典型的鼠标跟踪循环

- (void)mouseDown:(NSEvent *)theEvent {
  NSPoint pos;
 
  while ((theEvent = [[self window] nextEventMatchingMask:
    NSLeftMouseUpMask | NSLeftMouseDraggedMask])) {
 
    NSPoint pos = [self convertPoint:[theEvent locationInWindow]
                            fromView:nil];
 
    if ([theEvent type] == NSLeftMouseUp)
      break;
 
    // Do some other processing...
  }
}

鼠标跟踪循环结束后会发生什么?用户松开鼠标按钮后,应用程序会处理与用户在循环期间按下的助记符相对应的所有待处理命令。
这种延迟处理的影响可能是不可取的。
您可以通过过滤关键事件的事件流然后显式忽略它们来防止这种情况。
例4-8显示了如何修改上面的代码来实现这一点(斜体中的新代码)。


例4-8典型的鼠标跟踪循环-键事件为负数

- (void)mouseDown:(NSEvent *)theEvent {
  NSPoint pos;
 
  while ((theEvent = [[self window] nextEventMatchingMask:
      NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSKeyDownMask])) {
 
    NSPoint pos = [self convertPoint:[theEvent locationInWindow]
                            fromView:nil];
 
    if ([theEvent type] == NSLeftMouseUp)
        break;
         else if ([theEvent type] == NSKeyDown) {
                NSBeep();
                continue;
    }
 
    // Do some other processing...
  }
}

对于使用三种方法处理的拖动操作,同时发生鼠标和按键事件的情况有点不同。
在这种情况下,AppKit处理键盘事件,就像它在跟踪期间通常做的那样。
如果用户按下命令助记符,即使跟踪操作正在进行,应用程序对象也会向其目标发送相应的消息。
因此,例如,如果在绘图应用程序中,用户拖动他们刚刚创建的蓝色圆圈,然后(可能是偶然的)在鼠标按钮仍然向下时按下Command d-x(剪切),则处理剪切操作的代码将运行,删除用户正在拖动的对象。

这个问题的解决方案涉及几个步骤:

  • 声明一个反映拖动操作何时进行的布尔实例变量。
  • mouseDown:中将此变量设置为YES,并在mouseUp:中将其重置为NO
  • 覆盖performKeyEquivalent:检查实例变量的值,如果发生拖动操作,则丢弃键事件。

例4-9显示了它的实现代码(isBeingManipulated是布尔实例变量)。


例4-9使用三方法方法在拖动操作期间丢弃键事件

@implementation MyView
 
- (BOOL)performKeyEquivalent:(NSEvent *)anEvent
{
  if (isBeingManipulated) {
    if ([anEvent type] == NSKeyDown) // Can get NSKeyUp here too
      NSBeep ();
    return YES; // Claim we handled it
  }
 
  return NO;
}
 
- (void)mouseDown:(NSEvent *)anEvent
{
  isBeingManipulated = YES;
  // other code goes  here...
}
 
- (void)mouseUp:(NSEvent *)anEvent
{
  isBeingManipulated = NO;
  // other code goes here ...
}
 
@end

这个问题的解决方案被简化了,仅用于一般说明。
在实际应用程序中,您可能希望检查事件类型,有条件地设置isBeingManipulated变量,并有选择地处理等效键。


六、处理关键事件

当用户按下键盘上的一个键或同时按下几个键时,OS X系统会生成键事件。
当按下多个键时,这些键中的一个或多个会修改所按下的“主”键的重要性。
最常用的修改键是命令、控制、选项(Alt)和移位键。
在某些上下文和组合中,按键代表对操作系统或最前面的应用程序的命令,而不是要插入文本的字符。

本章讨论如何处理关键事件,尤其是关键事件。


1、关键事件概述

在关键事件和关键事件的路径中,关于关键事件的突出事实如下:

  • 键事件有三种特定类型(NSEventType),每种类型都与一个NSResponder方法相关联:
    | 事件类型常量 | 事件法 |
    | :— | :-- |
    | NSKeyDown | keyDown: |
    | NSKeyUp | keyUp: |
    | NSFlagsChanged | flagsChanged: |

flagsChanged:方法可用于检测修改键的按下,而无需同时按下任何其他键。
例如,如果用户自己按下Option键,您的响应对象可以在其flagsChanged:的实现中检测到这一点。

  • 大多数键事件——即表示要作为文本插入的字符的事件——由与键窗口关联的NSWindow对象分派给第一个响应者。
  • 如果第一个响应者不处理事件,它会将事件向上传递到响应者链(请参阅响应者链)。
  • 键事件的传递路径根据事件是代表字符、等效键、键盘动作还是键盘界面控制命令而变化。
    全局应用程序对象(NSApp)首先查找等效键,然后是键盘界面控制命令并对其进行特殊处理(详见键事件的路径)。
    如果事件不是这两者,则将其分派到代表键窗口的NSWindow对象,该对象又将事件分派给keyDown:消息中的第一个响应者。
  • 响应者对象确定键事件表示什么并适当地处理它。
    此时,键事件可以表示以下任何一种:
    • 键盘操作,它是绑定到键绑定字典中的操作消息选择器的键或键组合(请参阅键绑定)。
    • application-specific的命令或动作(不使用键绑定字典的命令或动作)
    • 要插入文本的一个或多个字符
      有关详细信息,请参阅覆盖keyDown:方法。
  • 与键事件消息一样,如果第一个响应者不处理它,则键盘操作消息会向上传递到响应者链。

2、覆盖keyDown:方法

大多数响应对象(如自定义视图)通过覆盖NSResponder声明的keyDown:方法来处理键事件。
对象可以以它认为合适的任何方式处理事件。
文本对象通常将消息解释为插入文本的请求,而绘图对象可能只对几个键感兴趣,例如Delete和箭头键作为删除和移动选定项目的命令。
与鼠标事件一样,响应对象通常希望查询传入的NSEvent对象以了解有关事件的更多信息并获取处理它所需的数据。
一些更有用的键事件NSEvent方法如下:

  • characterscharactersIgnoringModifiers-响应者可以提取与事件关联的Unicode字符数据,并将其插入为文本或解释为命令。
    charactersIgnoringModifiers方法在返回字符数据时忽略任何修饰符击键(Shift除外)。
    请注意,这两个方法名称都是复数,因为击键可以产生多个字符(例如,“à”由“a”和 ‘`‘).
  • modifierFlags——使用这种方法,响应者可以确定是否按下了任何修改键。
  • isARepeat-此方法告诉响应者是否连续快速按下相同的键。

keyDown:方法的实现中,响应者可以提取关联的NSEvent对象所包含的字符数据并将其插入到显示的文本中;或者它可以将字符数据解释为绑定到键盘操作或请求某些application-specific行为的键或组合键。
然而,应用程序工具包为此提供了一些方便的快捷方式,如下所述。


处理键盘操作和插入文本

处理文本的响应程序对象,例如文本视图,必须准备好处理可以是要插入的字符或键盘操作的键事件。
如键事件的路径中所述,键盘操作是一种特殊类型的操作消息,它依赖于键绑定机制,该机制将特定的击键(例如Control-e)绑定到与文本相关的特定命令(例如,将插入点移动到当前行的末尾)。
这些命令在NSResponder定义的方法中实现,以对这些物理击键给出每个视图的功能解释。

注意: 接收和编辑文本的视图必须符合NSTextInput协议。
采用该协议允许自定义视图与文本输入管理系统正确交互。
Application Kit类NSTextNSTextView实现NSTextInput,因此如果您对这些类进行分类,您可以“免费”获得协议一致性


在处理keyDown:中的键事件时,期望插入文本的视图对象首先确定NSEvent对象的一个或多个字符是否代表键盘操作。
如果是,它会发送相关的操作消息;如果不是,它会将字符作为文本插入。
具体来说,视图可以在其实现中做以下两件事之一:

  • 它可以使用NSEventcharacters方法提取事件对象的字符,并解释这些字符以查看它们是否与已知的键盘操作相关联。
    如果是,它将调用自身的适当操作方法或超级视图。
    不鼓励这种方法。
  • 它可以通过调用NSResponder方法将事件传递给Cocoa的文本输入管理系统interpretKeyEvents:
    输入管理系统根据所有相关的键绑定字典中的条目检查按下的键,如果匹配,则发送doCommandBySelector:消息返回给视图。
    否则,它发送insertText:消息返回给视图,视图实现此方法来提取和显示文本。

例5-1显示了第二种方法在代码中的外观。


例5-1使用输入管理系统解释关键事件

- (void)keyDown:(NSEvent *)theEvent {
    [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]];
}
 
// The following action methods are declared in NSResponder.h
- (void)insertTab:(id)sender {
    if ([[self window] firstResponder] == self) {
        [[self window] selectNextKeyView:self];
    }
}
 
- (void)insertBacktab:(id)sender {
    if ([[self window] firstResponder] == self) {
        [[self window] selectPreviousKeyView:self];
    }
}
 
- (void)insertText:(id)string {
    [super insertText:string];  // have superclass insert it
}

请注意,此示例包括insertText:的覆盖,它只是调用超类实现。
这样做是为了阐明输入管理器的角色,但实际上并不是必要的。
doCommandBySelector:的默认(NSResponder)实现确定视图是否响应键盘操作选择器,如果视图确实响应,它将调用操作方法;如果视图没有响应,doCommandBySelector:被发送到下一个响应者(依此类推)。
因此,视图应该只实现与其想要处理的操作相对应的操作方法。
输入管理器行为的另一个含义是,如果键绑定字典将物理击键与键盘操作相匹配,响应者对象只需覆盖相关的操作方法来处理该击键。
例如,为了处理Escape键的默认绑定,响应者将覆盖NSRespondercancelOperation:方法。

超级视图处理子视图启动的键盘操作消息的一个例子是NSScrollView对象处理分页命令的方式。
这个滚动视图对象是一个复合对象,由文档视图、剪辑视图(一个NSClipView对象)和滚动器(一个NSScroller对象)组成。
因为它是包含或协调对象,所以NSScrollView对象是该分组中所有其他对象的超级视图。
现在假设您的自定义视图是滚动视图的文档视图。
如果您实现keyDown:发送interpretKeyEvents:到输入管理器,但没有实现scrollPageDown:操作方法,当用户按下Page Down键(或对该功能有效的任何键绑定)时,文档视图仍将在滚动视图中滚动。
发生这种情况是因为查询响应器链中的每个下一个响应器以查看它是否响应scrollPageDown:
NSScrollView类提供默认实现,因此调用此实现。

处理文本的其他应用程序可以使用输入管理系统。
例如,绘图应用程序中的自定义视图可能会使用箭头键来“轻推”图形对象的精确距离。
在标准键绑定字典中,箭头键绑定到NSRespondermoveUp:moveDown:moveLeft:moveRight:方法。
因此,类似于例5-2中所示的代码可以用来轻推图形对象。


例5-2使用输入管理系统处理箭头键字符

- (void)keyDown:(NSEvent *)theEvent {
    // Arrow keys are associated with the numeric keypad
    if ([theEvent modifierFlags] & NSNumericPadKeyMask) {
        [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]];
    } else {
        [super keyDown:theEvent];
    }
}
 
-(IBAction)moveUp:(id)sender
{
    [self offsetLocationByX:0 andY: 10.0];
    [[self window] invalidateCursorRectsForView:self];
}
 
-(IBAction)moveDown:(id)sender
{
    [self offsetLocationByX:0 andY:-10.0];
    [[self window] invalidateCursorRectsForView:self];
}
 
-(IBAction)moveLeft:(id)sender
{
    [self offsetLocationByX:-10.0 andY:0.0];
    [[self window] invalidateCursorRectsForView:self];
}
 
-(IBAction)moveRight:(id)sender
{
    [self offsetLocationByX:10.0 andY:0.0];
    [[self window] invalidateCursorRectsForView:self];
}

在大多数情况下,interpretKeyEvents:方法比解释自己的方法更可取。
这一建议尤其适用于那些在应用程序中的自定义视图,如文字处理器和图形编辑器,它们承担了大部分工作。
支持使用文本输入管理系统的主要因素是,有了它,您不需要将功能硬连接到物理键上。
如果用户使用的便携式计算机缺少应用程序硬连接的功能键怎么办?更好、更灵活的方法是在字典中指定替代键绑定。
文本输入管理系统的另一个优点是它允许将键事件解释为键盘上不直接可用的文本,如汉字和一些重音字符。

注意: 有关键绑定和文本输入系统的更多信息,请参阅文本系统默认值和键绑定。


特别解释击键

尽管使用文本输入管理系统是有利的,但您可以自己在keyDown:中解释物理键,并以application-specific的方式处理它们。
NSEvent类声明了几十个常量,这些常量通过键符号或键函数来识别特定的键;该类的参考留档中的常量描述了这些常量。
例5-3显示了一个示例。


例5-3 NSResponse der定义的一些关键常量

enum {
    NSUpArrowFunctionKey            = 0xF700,
    NSDownArrowFunctionKey          = 0xF701,
    NSLeftArrowFunctionKey          = 0xF702,
    NSRightArrowFunctionKey         = 0xF703,
    NSF1FunctionKey                 = 0xF704,
    NSF2FunctionKey                 = 0xF705,
    NSF3FunctionKey                 = 0xF706,
    // other constants here
    NSUndoFunctionKey               = 0xF743,
    NSRedoFunctionKey               = 0xF744,
    NSFindFunctionKey               = 0xF745,
    NSHelpFunctionKey               = 0xF746,
    NSModeSwitchFunctionKey         = 0xF747
};

此外,文本系统定义了表示常用Unicode字符的常量,例如制表符、删除和回车符。
有关这些常量的列表,请参阅*NSText类参考*中的常量。

在您的keyDown:实现中,您可以将这些常量之一与键事件对象的字符数据进行比较,以确定是否按下了某个键,然后采取相应的行动。
您可能还记得,characterscharactersIgnoringModifiers方法返回键值的NSString对象,而不是字符,因为击键可能会生成多个字符。
(事实上,如果按下了死键——一个没有字符映射到它的键——,这些方法甚至可以返回一个空字符串。)如果您的实现keyDown:正在处理单个字符的键值,例如箭头键,您可以检查返回字符串的长度,如果是单个字符,则使用NSString方法characterAtIndex:索引为0。
然后根据NSResponder常量之一测试该字符。


例5-4显示了如何执行与例5-2中相同的图形对象“轻推”,但这次响应对象本身确定是否按下了箭头键。


例5-4通过解释物理键来处理箭头键字符

- (void)keyDown:(NSEvent *)theEvent {
 
    if ([theEvent modifierFlags] & NSNumericPadKeyMask) { // arrow keys have this mask
        NSString *theArrow = [theEvent charactersIgnoringModifiers];
        unichar keyChar = 0;
        if ( [theArrow length] == 0 )
            return;            // reject dead keys
        if ( [theArrow length] == 1 ) {
            keyChar = [theArrow characterAtIndex:0];
            if ( keyChar == NSLeftArrowFunctionKey ) {
                [self offsetLocationByX:-10.0 andY:0.0];
                [[self window] invalidateCursorRectsForView:self];
                return;
            }
            if ( keyChar == NSRightArrowFunctionKey ) {
                [self offsetLocationByX:10.0 andY:0.0];
                [[self window] invalidateCursorRectsForView:self];
                return;
            }
            if ( keyChar == NSUpArrowFunctionKey ) {
                [self offsetLocationByX:0 andY: 10.0];
                [[self window] invalidateCursorRectsForView:self];
                return;
            }
            if ( keyChar == NSDownArrowFunctionKey ) {
                [self offsetLocationByX:0 andY:-10.0];
                [[self window] invalidateCursorRectsForView:self];
                return;
            }
            [super keyDown:theEvent];
        }
    }
    [super keyDown:theEvent];
}

您还可以将NSResponder常量转换为字符串对象,然后将该对象与characterscharactersIgnoringModifiers返回的值进行比较,如下例所示:

unichar la = NSLeftArrowFunctionKey;
NSString *laStr = [[[NSString alloc] initWithCharacters:&la length:1] autorelease];
if ([theArrow isEqual:laStr]) {
    [self offsetLocationByX:-10.0 andY:0.0];
    [[self window] invalidateCursorRectsForView:self];
    return;
} 

但是,这种方法更加占用内存。


3、处理关键等价物

等效键是绑定到窗口中某个视图的字符。
当用户键入该字符时,此绑定会导致该视图执行指定操作,通常是在按下修饰符键(在大多数情况下是命令键)时。
等效键必须是可以在没有修饰符键或仅使用Shift的情况下键入的字符。

应用程序通过首先向下发送窗口的视图层次结构来路由键等效事件。
全局NSApplication对象在其sendEvent:方法中分派它识别为潜在键等效的事件(基于修饰符标志的存在)。
它向键NSWindow对象发送performKeyEquivalent:消息。
该对象通过调用NSView的默认实现performKeyEquivalent:将消息转发到其每个子视图(包括上下文和弹出菜单),直到一个响应YES;如果没有,则返回NO
如果视图层次结构中没有对象处理等效的键,NSApp会向菜单栏中的菜单发送performKeyEquivalent:
不鼓励NSWindow子类覆盖performKeyEquivalent:

注意: 从OS X v10.5开始,如果无法识别密钥等效项,NSWindow将其作为NSKeyDown事件发送给第一个响应者。
此行为启用带有Command d-key修饰符的自定义键绑定条目。
此外,NSApplication通过performKeyEquivalent:向键窗口发送Control键事件,然后通过响应者链将其作为NSKeyDown事件发送。
此行为允许更可靠地使用Control键事件作为菜单键等效项。

一些Cocoa类,如NSButtonNSMenuNSMatrixNSSavePanel,提供performKeyEquivalent:的默认实现。
例如,您可以将返回键设置为NSButton对象的键等价物,当按下该键时,该按钮就像被单击了一样。
但是,其他Application Kit类(包括自定义视图)的子类需要提供它们自己的实现performKeyEquivalent:.实现应该使用charactersIgnoringModifiers方法从传入的NSEvent对象中提取等价键的字符,然后检查它们以确定它们是否是它识别的等价键。
它处理等价键就像处理keyDown:(参见覆盖keyDown:方法)。
处理等效键后,实现应该返回YES
如果它不处理等效键,它应该调用performKeyEquivalent:的超类实现,或者(如果您知道超类不处理等效键)返回NO以指示等效键应该向下传递到视图层次结构或菜单栏中的菜单。


4、键盘接口控制

Cocoa事件分派架构将某些键事件视为命令,用于将控制焦点移动到窗口中的不同用户界面对象,模拟鼠标单击对象,关闭模式窗口,以及在允许选择的对象中进行选择。
这种功能称为键盘界面控制。
键盘界面控制中涉及的大多数用户界面对象都是NSControl对象,但不是控件的对象也可以参与。
当对象具有控制焦点时,Application Kit会在对象的边界周围绘制一个浅蓝色的键焦点环。
如果启用了完全键盘访问,则表5-1中列出的键具有所述效果。

钥匙效果
标签移动到下一个键视图。
Shift-Tab移动到上一个键视图。
太空选择,例如在复选框中使用鼠标单击或切换状态。
在选择列表中,选择或取消选择突出显示的项目。
箭头键在复合视图中移动,例如NSForm对象。
控制选项卡(控制-移位-选项卡)从制表符具有其他意义的视图(例如,NSTextView对象)转到下一个(上一个)键视图。
选项或转移扩展选择,不影响其他选定项目。

Interface Builder调色板上的某些对象不参与键盘界面控制,例如NSImageViewWebViewPDFView对象。

除了键视图循环之外,窗口还可以有一个默认的按钮单元格,它使用返回(或回车)键作为其等效键。
通过编程,您可以发送setDefaultButtonCell:NSWindow对象来设置此按钮单元格;您也可以通过在获取信息窗口的属性窗格中将按钮单元格的键设置为“\r”来在Interface Builder中设置它。
默认按钮单元格将自己绘制为键盘界面控制的焦点元素,除非另一个按钮单元格被聚焦。
在这种情况下,它会暂时将自己绘制为正常并禁用其等效键。
转义键是窗口中键盘界面控件的另一个默认键;它会立即中止模式循环。

在窗口中连接在一起的用户界面对象构成了窗口的键视图循环。
键视图循环是一系列NSView对象通过它们的nextKeyView属性(反向时为previousKeyView属性)相互连接。
此序列中的最后一个视图“循环”回第一个视图。
默认情况下,NSWindow分配一个初始第一响应者,并使用它找到的对象构建一个键视图循环。
如果您想更好地控制键视图循环,您应该使用Interface Builder进行设置。
有关过程的详细信息,请参阅Interface Builder的帮助页面。

要使其实例参与键视图循环,自定义视图必须返回YESfromacceptsFirstResponder
通过这样做,它会影响canBecomeKeyView方法返回的值。
acceptsFirstResponder方法控制响应者是否在其窗口要求时接受第一响应者状态(即在使用响应者作为参数调用makeFirstResponse der:时)。
canBecomeKeyView方法控制Application Kit是否允许选项卡到视图。
它调用acceptsFirstResponder,但它也会在确定要返回的值之前检查其他信息,例如视图是否隐藏以及是否打开了完整的键盘访问。
canBecomeKeyView方法很少被覆盖,而acceptsFirstResponder经常被覆盖。

NSViewNSWindow类中定义了许多以编程方式设置和遍历键视图循环的方法。
表5-2列出了一些更有用的方法。

nextKeyViewNSViewpreviousKeyViewNSView返回键视图循环中的下一个和上一个视图对象。
setNextKeyView:NSView设置循环中的下一个键视图。
selectNextKeyView:NSWindowselectPreviousKeyView:NSWindow在视图层次结构中搜索候选下一个(上一个)键视图,如果找到一个,则将其设为第一个响应者。
canBecomeKeyViewNSView返回接收器是否可以成为键视图。
nextValidKeyViewNSViewpreviousValidKeyViewNSView返回跟随接收者并接受第一响应者状态的键视图循环中最近的视图对象。

例5-5中的代码说明了如何使用其中一些方法来操作键视图循环。


例5-5操作键视图循环

- (void)textDidEndEditing:(NSNotification *)notification {
    NSTextView *text = [notification object];
    unsigned whyEnd = [[[notification userInfo] objectForKey:@"NSTextMovement"] unsignedIntValue];
    NSTextView *newKeyView = text;
 
    // Unscroll the previous text.
    [text scrollRangeToVisible:NSMakeRange(0, 0)];
 
    if (whyEnd == NSTabTextMovement) {
        newKeyView = (NSTextView *)[text nextKeyView];
    } else if (whyEnd == NSBacktabTextMovement) {
        newKeyView = (NSTextView *)[text previousKeyView];
    }
 
    // Set the new key view and select its whole contents.
    [[text window] makeFirstResponder:newKeyView];
    [newKeyView setSelectedRange:NSMakeRange(0, [[newKeyView textStorage] length])];
}

七、使用跟踪区域对象

一个NSTrackingArea类的实例定义了一个视图的区域,该区域响应鼠标的移动。
当鼠标光标进入该区域,在其中移动并离开时,Application Kit会向指定对象发送(取决于指定的选项)鼠标跟踪、鼠标移动和光标更新消息。

重要提示: OS X v10.5中引入了NSTrackingArea类。
有关早期版本中用于鼠标跟踪和光标更新的API的讨论,请参阅附录使用Tracking-Area对象。
另请参阅下面的兼容性问题,以讨论遗留方法的当前行为。

当鼠标光标进入和退出窗口的某个区域时,Application Kit会向对象发送鼠标跟踪消息mouseEntered:mouseExited:
鼠标跟踪使拥有该区域的视图能够做出响应,例如,通过绘制高亮颜色或显示工具提示。
Application Kit还会向对象发送mouseMoved:消息,如果NSMouseMoved请求事件类型。
光标更新事件是一种特殊的鼠标埋点,Application Kit会自动处理。
当鼠标指针进入光标矩形时,Application Kit会在矩形下显示适合该视图类型的光标图像;例如,当鼠标指针进入文本视图时,会显示一个工字梁光标。

本章中的部分描述了如何创建NSTrackingArea对象、将它们附加到视图、响应相关事件以及在视图几何发生更改时管理对象。


1、创建NSTrackingArea对象

一个NSTrackingArea对象定义了一个对鼠标移动敏感的视图区域。
当鼠标进入、移动和退出该区域时,Application Kit会发送鼠标跟踪、鼠标移动和光标更新消息。
该区域是在其关联视图的本地坐标系中指定的矩形。
消息的接收者(所有者)是在创建跟踪区域对象时指定的;尽管所有者可以是任何对象,但它通常是与跟踪区域对象关联的视图。

创建NSTrackingArea对象时,必须指定一个或多个选项。
这些选项是类声明的枚举常量,用于配置跟踪区域行为的各个方面。
它们分为三类:

  • 发送的事件消息类型
    您可以请求mouseEntered:mouseExited:消息(NSTrackingMouseEnteredAndExited);您可以请求mouseMoved:消息(NSTrackingMouseMoved);您可以请求cursorUpdate:消息(NSTrackingCursorUpdate)。
    您不仅限于此集合中的单个选项;您可以执行按位或操作来请求多种类型的消息。
  • 跟踪区域消息的活动范围
    您必须指定以下选项之一以请求跟踪区域何时应主动生成事件:
    • 当关联视图是第一响应者(NSTrackingActiveWhenFirstResponder)时
    • 当关联视图在键窗口(NSTrackingActiveInKeyWindow)中时
    • 当关联视图在活动应用程序中时(NSTrackingActiveInActiveApp
    • 无论何时激活应用程序(NSTrackingActiveAlways
  • 跟踪区域行为的改进
    您可以请求在鼠标光标首次离开跟踪区域时发送第一条消息(NSTrackingAssumeInside);您可以请求跟踪区域与关联视图的可见矩形相同(NSTrackingInVisibleRect);您可以请求鼠标拖入和拖出跟踪区域生成mouseEntered:mouseExited:事件(NSTrackingEnabledDuringMouseDrag)。
    您不仅限于此集合中的单个选项;您可以执行按位或运算来请求对行为进行多次改进。

您可以初始化已分配的NSTrackingArea实例,使用initWithRect:options:owner:userInfo:方法。
创建对象后,您必须通过调用NSView方法addTrackingArea:将其与视图相关联。
您可以创建一个NSTrackingArea实例并在任何时候将其添加到视图中,因为(与早期版本中的鼠标跟踪API不同),成功的创建不依赖于添加到窗口中的视图。
例6-1显示了在自定义视图的initWithFrame:方法中创建和添加NSTrackingArea实例;在这种情况下,拥有的视图请求Application Kit发送mouseEntered:mouseExited:mouseMoved:消息,只要它的窗口是键窗口。


例6-1初始化一个NSTrackingArea实例

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        trackingArea = [[NSTrackingArea alloc] initWithRect:eyeBox
            options: (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow )
            owner:self userInfo:nil];
        [self addTrackingArea:trackingArea];
    }
    return self;
}

最后一个参数initWithRect:options:owner:userInfo:是一个字典,您可以使用它将任意数据传递给鼠标跟踪和光标更新消息的接收者。
接收对象可以通过将userData发送到传递到NSEvent``mouseEntered:mouseExited:cursorUpdate:方法的userData消息发送到mouseMoved:方法会导致断言失败。
(在例6-1中的示例中,userInfo参数设置为nil。)

跟踪矩形边界包括顶部和左侧边缘,但不包括底部和右侧边缘。
因此,如果您有一个未翻转的视图,其中跟踪矩形覆盖了其边界,并且视图的框架具有几何形状frame.origin = (100, 100), frame.size = (200, 200),那么跟踪矩形处于活动状态的区域是frame.origin = (100, 101), frame.size = (199, 199),在帧坐标中。


2、管理跟踪区域对象

因为NSTrackingArea对象归其视图所有,所以当视图从窗口中添加或删除时,或者当视图在其窗口中改变位置时,Application Kit可以自动重新计算跟踪区域。
但是在Application Kit无法重新计算受影响的跟踪区域(或多个区域)的情况下,它会向关联的视图发送updateTrackingAreas,要求它重新计算和重置这些区域。
一种情况是视图位置的变化会影响视图的可见矩形(visibleRect)-除非该视图的NSTrackingArea对象是使用NSTrackingInVisibleRect选项创建的,在这种情况下,Application Kit会处理重新计算。
请注意,Application Kit会向每个视图发送updateTrackingAreas,无论它是否有跟踪区域。

您可以重写updateTrackingAreas方法,如例6-2所示,从视图中删除当前跟踪区域,释放它们,然后将新的NSTrackingArea对象添加到具有重新计算区域的相同视图中。


例6-2重置跟踪区域对象

- (void)updateTrackingAreas {
    NSRect eyeBox;
    [self removeTrackingArea:trackingArea];
    [trackingArea release];
    eyeBox = [self resetEye];
    trackingArea = [[NSTrackingArea alloc] initWithRect:eyeBox
        options: (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow)
        owner:self userInfo:nil];
    [self addTrackingArea:trackingArea];
}

如果您的类不是自定义视图类,您可以将类实例注册为通知 NSViewFrameDidChangeNotification的观察者,并让它在收到通知时重新建立跟踪矩形。


3、响应鼠标跟踪事件

使用NSTrackingMouseEnteredAndExited选项创建的NSTrackingArea对象的所有者会收到一个mouseEntered:每当鼠标光标进入跟踪区域对象定义的视图区域时;随后,当鼠标离开该区域时,它会收到一个mouseExited:消息。
如果还为跟踪区域对象指定了NSTrackingMouseMoved选项,则所有者还会收到一个或多个mouseMoved:每个mouseEntered:mouseExited:消息之间的消息。
您可以覆盖相应的NSResponder方法来处理这些消息,以执行诸如突出显示视图、显示自定义工具提示或在另一个视图中显示相关信息等任务。

重要提示: 不能保证应用程序中跟踪区域对象接收到的鼠标输入和鼠标退出事件的正确顺序。
例如,如果您将鼠标光标从一个跟踪区域移动到另一个跟踪区域并返回,事件的顺序(作为消息)可以是:mouseEntered:mouseEntered:mouseExited:mouseExited:


例6-2中的跟踪代码用于使“眼球”在进入跟踪矩形时跟随鼠标指针的移动。


例6-3处理鼠标输入、鼠标移动和鼠标退出事件

- (void)mouseEntered:(NSEvent *)theEvent {
    NSPoint eyeCenter = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    eyeBox = NSMakeRect((eyeCenter.x-10.0), (eyeCenter.y-10.0), 20.0, 20.0);
    [self setNeedsDisplayInRect:eyeBox];
    [self displayIfNeeded];
}
 
- (void)mouseMoved:(NSEvent *)theEvent {
    NSPoint eyeCenter = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    eyeBox = NSMakeRect((eyeCenter.x-10.0), (eyeCenter.y-10.0), 20.0, 20.0);
    [self setNeedsDisplayInRect:eyeBox];
    [self displayIfNeeded];
}
 
- (void)mouseExited:(NSEvent *)theEvent {
    [self resetEye];
    [self setNeedsDisplayInRect:eyeBox];
    [self displayIfNeeded];
} 

就像使用鼠标向下和鼠标向上消息一样,您可以查询传入的NSEvent对象以获取与事件相关的信息。


4、管理游标更新事件

NSTrackingAreaNSTrackingArea的一个常见用途是在不同类型的视图上更改光标图像。
例如,文本通常需要工字梁光标。
许多Application Kit类提供适合其视图实例的光标图像;您可以“免费”获得此行为。
但是,您可能希望为自定义视图子类的实例指定特定(或不同)的光标图像。

在重写NSResponder方法cursorUpdate:中更改视图的光标图像。
要接收此消息,您必须通过调用initWithRect:options:owner:userInfo:对象来NSTrackingArea创建一个NSTrackingCursorUpdate(以及任何其他所需选项)的初始化器。
然后将创建的对象添加到具有addTrackingArea:的视图中。
此后,鼠标进入跟踪区域会生成一个NSCursorUpdate事件;NSWindow对象通过向跟踪区域的所有者(通常是视图本身)发送cursorUpdate:消息来处理此事件。
cursorUpdate:的实现应使用适当的NSCursor方法来设置标准或自定义光标图像。

光标矩形可以重叠或完全嵌套,一个在另一个内。
光标更新的仲裁遵循正常的响应链机制。
鼠标光标下视图的光标矩形首先接收cursorUpdate:消息。
它可以显示光标,也可以将消息向上传递响应链,其中具有重叠光标矩形的视图可以响应消息。


例6-4显示了cursorUpdate:的实现,它将视图的光标设置为十字图像。
请注意,没有必要将光标图像重置回鼠标退出跟踪区域时的状态。
Application Kit通过向退出光标矩形时鼠标光标移动的视图发送cursorUpdate:消息来自动为您处理此问题。


例6-4处理游标更新事件

-(void)cursorUpdate:(NSEvent *)theEvent
{
    [[NSCursor crosshairCursor] set];
}

如果拥有track-zone对象的响应者没有实现cursorUpdate:方法,默认实现将消息向上转发到响应者链。
如果响应者实现了cursorUpdate:但决定不处理当前事件,它应该调用cursorUpdate:cursorUpdate:

与任何其他类型的NSTrackingArea对象一样,当关联视图的位置或大小发生变化时,您可能偶尔需要重新计算并重新创建用于光标更新的跟踪区域对象。
有关详细信息,请参阅管理一个跟踪区域对象。


5、兼容性问题

从OS X v10.5开始,NSTrackingArea类和相关的NSView方法addTrackingArea:removeTrackingArea:updateTrackingAreastrackingAreas替换了NSView的以下方法,这些方法被视为遗留API,但仍然支持兼容性:

  • addTrackingRect:owner:userData:assumeInside:
  • removeTrackingRect:
  • addCursorRect:cursor:
  • removeCursorRect:cursor:
  • discardCursorRects
  • resetCursorRects

NSTrackingRectTagNSTrackingRectTag类型,由addTrackingRect:owner:userData:assumeInside:返回并传递给removeTrackingRect:方法,也是遗留类型。
在内部,这种类型与NSTrackingArea *相同。

遗留方法的底层实现基于NSTrackingArea,产生以下影响:

  • 调用addTrackingRect:owner:userData:assumeInside:方法创建一个NSTrackingArea对象,其中设置了选项NSTrackingMouseEnteredAndExitedNSTrackingActiveAlways
    如果函数的最后一个参数是YES,它还包括NSTrackingAssumeInside选项。
    创建对象后,它将其添加到接收器(addTrackingArea:)并将其作为标签返回。
  • resetCursorRectsresetCursorRects在updateTrackingAreaupdateTrackingAreas

此外,以下与NSWindow游标相关的方法是遗留API,但为了兼容性而进行了维护:

  • discardCursorRects
  • invalidateCursorRectsForView:
  • resetCursorRects


八、处理平板电脑事件

以下部分讨论与处理平板电脑事件相关的问题。


1、平板电脑事件的包装

平板设备驱动程序将低级事件打包为本机平板事件或鼠标事件,通常取决于它们是接近事件还是指针事件。
接近事件始终是本机平板事件。
应用程序套件(在NSEvent.h中)为本机平板事件声明以下事件类型常量:

typedef enum _NSEventType {
    // ...
    NSTabletPoint     = 23,
    NSTabletProximity = 24,
    // ...
} NSEventType;

驱动程序几乎总是将平板指针事件打包为鼠标事件的子类型。
Application Kit为与鼠标事件相关的所有事件类型(NSLeftMouseDownNSRightMouseUpNSMouseMoved等)的平板子类型声明以下常量:

enum {
    NSMouseEventSubtype           = NX_SUBTYPE_DEFAULT,
    NSTabletPointEventSubtype      = NX_SUBTYPE_TABLET_POINT,
    NSTabletProximityEventSubtype = NX_SUBTYPE_TABLET_PROXIMITY
};

在一些特殊情况下,驱动程序可能会将低级平板指针事件打包为NSTabletPoint事件类型,而不是鼠标事件子类型。
这些包括以下内容:

  • 在鼠标向下(即触笔向下)和随后的拖动事件之间的间隔期间,例如,只有压力在变化。
  • 当有两个同时活动的指点设备时,不移动光标的指点设备会生成NSTabletPoint事件。
  • 如果由于某种原因,平板驱动程序被告知不要移动光标,则驱动程序会以本机形式打包平板事件。

出于这个原因,建议您的代码检查作为本机事件类型和鼠标子类型传递的平板指针事件。


2、平板电脑事件和响应链

与任何NSEvent对象一样,平板电脑事件在响应者链中被路由,直到它们被处理。
应用程序中的响应者对象(即从NSResponse der继承的对象)可以覆盖适当的NSResponse der方法并处理在该方法中传递给它们的NSEvent对象。
或者他们可以将事件传递给链中的下一个响应者。
(如果您不覆盖这些方法之一,事件会自动传递给下一个响应者。

打算处理平板电脑事件的应用程序应覆盖至少五个NSResponse der方法:

  • tabletProximity:tabletPoint:
    实现这些方法来处理本机接近和指针平板电脑事件。
  • mouseDown:mouseDragged:,和mouseUp:
    实现这些方法来处理子类型为NSTabletPointEventSubtypeNSTabletProximityEventSubtype 的鼠标事件。

推荐的方法是将这些方法中的平板电脑事件集中到两个常见的处理程序,一个用于接近事件,另一个用于指针事件。
如果您的应用程序中有不在响应链中的对象,并且您希望这些对象在到达时了解平板电脑事件,您可以实现您的事件处理程序例程,以便它向所有感兴趣的观察者发布通知。


九、处理触控板事件

当用户在MacBook Air和更新型号的MacBook Pro的触控板上触摸和移动手指时,OS X会生成多点触控事件、手势事件和鼠标事件。
触控板硬件包括内置支持,用于解释常见手势以及将手指的运动映射到鼠标事件。
此外,操作系统提供手势的默认处理;您可以在触控板系统首选项中观察OS X如何处理这些手势。
您还可以创建以独特方式接收和响应手势和多点触控事件的应用程序。

应用程序不应该依赖手势事件或触摸事件处理作为解释任何关键功能的用户操作的唯一机制,因为用户可能没有使用触控板。
触摸事件功能应该补充传统的传达用户命令的方式。

手势事件是多点触控事件的一种,因为它们基于对一系列触摸的解释。
换句话说,手势是触控板识别为构成手势的一系列多点触控事件。
手势和触摸事件要求您采用不同的方法来处理它们。
要理解这些方法,了解事件是如何以及何时生成的是有用的,它们是如何传递到您的应用程序的,以及它们包含哪些关于触控板上实际触摸的信息。


1、手势是由触控板解释的触摸动作

手势是手指在触敏表面上的特定动作,例如触控板,具有传统意义。
触控板驱动程序将其中一些动作解释为特定手势:

  • 捏合动作(放大或缩小)是表示缩小或放大(也称为放大)的手势。
  • 两个手指以相对的半圆移动是一个手势,意思是旋转。
  • 三个手指在一个共同的方向上刷过触控板表面是一个滑动手势。
  • 两根手指垂直或水平移动是滚动手势。

系统将表示特定手势的低级事件传递到活动应用程序,在活动应用程序中,这些事件被打包为NSEvent对象并放置在应用程序的事件队列中。
它们沿着与鼠标事件相同的路径传递到鼠标指针下的视图。
NSWindow对象通过调用手势的适当NSResponder方法将手势事件传递到视图:magnifyWithEvent:rotateWithEvent:swipeWithEvent:
如果视图不处理手势,它会沿着响应器链向上移动,直到另一个对象处理它或丢弃它。


手势事件和手势序列的类型

AppKit框架声明手势的NSEventType常量,例如NSEventTypeMagnifyNSEventTypeQuickLook

当用户触摸并在触控板上移动手指时,触控板驱动程序会生成一个手势序列,它与中描述的多点触控序列大致并发触控事件代表触控板上的手指但包含在其中。
手势序列在驱动程序第一次检测到手势时开始,并在驱动程序确定手势已结束时结束。
在大多数情况下,您不需要知道手势序列;您只需要实现处理特定手势的NSResponder方法。
但是,如果您想提交与特定手势相关的更改,例如注册撤消操作或执行详细绘图,您可以为该手势实现NSResponder方法并检查手势的phase属性。
因为手势可以结束或被取消,所以您需要准备好处理NSEventPhaseEndedNSEventPhaseCancelled


图8-1多点触控序列中的手势序列

在这里插入图片描述


如果您需要提交与特定手势相关的更改,您应该了解手势检测的一些行为特性。
首先,在单个多点触控序列中可能会出现多个手势序列。
例如,用户可能会首先捏住一个视图,然后滑动它,而无需将所有手指从触控板上移开。
此外,在手势序列的开始和结束之间,触控板驱动程序可能会首先将一个动作解释为一个手势,然后将其解释切换为另一个手势。
然而,目前这种情况只发生在放大和旋转手势上。
滚动和滑动手势一旦开始,就会锁定到该手势,直到手势结束。

当触控板硬件生成手势时,它也可能发出构成手势的触摸事件;然后,系统将触摸事件与手势事件一起路由到应用程序。
然而,一些触控板不支持此功能。

注意: NSGestureRecognizer对象不接收触控板触摸。

触控板首选项窗格包括滚动手势选项:两个手指移动滚动视图的内容视图。
从技术上讲,滚动手势不是特定的手势,而是鼠标事件。
与手势不同,滚轮事件(即NSScrollWheel类型的事件)可以同时具有phase属性和momentumPhase属性。
momentumPhase属性可帮助您检测动量滚动,即使用户不再物理滚动,硬件仍会继续发出滚轮事件。
Magic Mouse和Multi-Touch触控板等设备支持动量滚动。

在非动量滚动期间,AppKit将每个滚轮事件路由到该事件的指针下方的视图。
在非动量滚轮事件中,momentumPhase的值为NSEventPhaseNone

在动量滚动期间,AppKit将每个滚动轮事件路由到动量滚动开始时指针下方的视图。
在动量滚动轮事件中,phase的值为NSEventPhaseNone
当设备从用户执行的滚动事件切换到动量滚动轮事件时,momentumPhase设置为NSEventPhaseBegan
对于后续动量滚动轮事件,momentumPhase设置为NSEventPhaseChanged,直到动量消退,或者用户停止动量滚动;最终动量滚动轮事件的momentumPhase值为NSEventPhaseEnded

根据用户使用的滚动设备,了解事件路由行为的差异如何影响您接收到的事件非常重要。
使用Multi-Touch触控板时,用户必须在物理上停止滚动才能移动指针,因此您在滚动期间不会收到任何鼠标移动事件。
相比之下,Magic Mouse允许用户移动指针并单击滚动中间。
因为直到用户抬起手指,滚动才会结束,所以放在Magic Mouse上的手指的任何额外移动仍然被视为原始滚动的一部分,这意味着事件的phase属性是NSEventPhaseChanged而不是NSEventPhaseBegan,如您所料。
如果您直接处理滚轮事件,请准备好在使用Magic Mouse时接收NSEventPhaseBeganNSEventPhaseEnded``NSRightMouseDragged(例如NSLeftMouseDownNSOtherMouseUpNSLeftMouseDown)。


处理手势事件

要处理特定手势的事件(例如旋转、捏合或滑动运动),请在自定义视图中实现适当的NSResponder方法,即rotateWithEvent:magnifyWithEvent:swipeWithEvent:
与处理多点触控事件的过程不同,您的自定义视图不必“选择加入”。
当鼠标指针在您的视图上并且用户做出手势时,将调用相应的NSResponder方法来处理事件。
如果响应链中较早的视图不处理事件,也会调用它。

每个手势处理方法都有一个NSEvent参数。
您可以查询事件对象以获取与手势相关的信息。
对于已识别的手势,以下事件对象属性具有特殊重要性:

  • 放大或缩小(NSEventTypeMagnify)-magnification访问器方法返回一个浮点(CGFloat)值,表示放大系数。
  • Rotation(NSEventTypeRotate)-rotation访问器方法返回一个表示逆时针旋转度的浮点值。
  • 滑动(NSEventTypeSwipe)-deltaXdeltaY访问器方法以浮点(CGFloat)值的形式返回滑动的方向。
    非零deltaX值表示水平滑动;-1表示向右滑动,1表示向左滑动。
    非0 deltaY表示垂直滑动;-1表示向下滑动,1表示向上滑动。

旋转和放大手势是相对事件。
也就是说,每个rotateWithEvent:magnifyWithEvent:消息都带有自该类型的最后一个手势事件以来旋转或放大率的变化。
对于放大或缩小,您将magnification访问器的值添加到1.0以获取比例因子。
对于旋转,您将最新的旋转度添加到视图的当前旋转值。
例8-1说明了如何做到这一点。


例8-1处理放大和旋转手势

- (void)magnifyWithEvent:(NSEvent *)event {
    [resultsField setStringValue:
        [NSString stringWithFormat:@"Magnification value is %f", [event magnification]]];
    NSSize newSize;
    newSize.height = self.frame.size.height * ([event magnification] + 1.0);
    newSize.width = self.frame.size.width * ([event magnification] + 1.0);
    [self setFrameSize:newSize];
}
 
- (void)rotateWithEvent:(NSEvent *)event {
    [resultsField setStringValue:
        [NSString stringWithFormat:@"Rotation in degree is %f", [event rotation]]];
    [self setFrameCenterRotation:([self frameCenterRotation] + [event rotation])];
}

注意: 如例8-1所示的NSEventTypeGesture常量表示“通用”手势,当触控板上的移动不会导致特定手势时,触控板驱动程序会生成该手势。
您可以安全地忽略此事件类型。

要处理滑动手势,只需通过分析deltaXdeltaY值来确定滑动方向。
例8-2中的代码通过为实现视图设置填充颜色来响应滑动。


例8-2处理滑动手势

- (void)swipeWithEvent:(NSEvent *)event {
    CGFloat x = [event deltaX];
    CGFloat y = [event deltaY];
    if (x != 0) {
        swipeColorValue = (x > 0)  ? SwipeLeftGreen : SwipeRightBlue;
    }
    if (y != 0) {
        swipeColorValue = (y > 0)  ? SwipeUpRed : SwipeDownYellow;
    }
    NSString *direction;
    switch (swipeColorValue) {
        case SwipeLeftGreen:
            direction = @"left";
            break;
        case SwipeRightBlue:
            direction = @"right";
            break;
        case SwipeUpRed:
            direction = @"up";
            break;
        case SwipeDownYellow:
        default:
            direction = @"down";
            break;
    }
    [resultsField setStringValue:[NSString stringWithFormat:@"Swipe %@", direction]];
    [self setNeedsDisplay:YES];
}

您还可以查询传入的NSEvent对象以获取与手势事件相关的其他信息,包括鼠标指针在窗口坐标中的位置(locationInWindow)、事件的时间戳以及用户按下的任何修饰符键。

因为手势事件是从多点触控序列派生的,所以通过调用touchesMatchingPhase:inView:来查询NSEvent对象的触摸似乎是合理的。
但是,返回的NSTouch对象可能不是当前正在播放的触摸的准确反映。
因此,您不应该在手势处理方法中检查触摸对象。
唯一可靠的触摸对象集是在触摸事件处理方法中返回的,例如touchesBeganWithEvent:
有关这些方法的更多信息,请参阅触控事件代表触控板上的手指。

如手势事件的类型和手势序列中所述,您还可以使用手势响应器的phase属性来执行操作,例如合并手势起点和终点之间的所有手势更改,以便您可以撤消完整的序列,而不仅仅是序列的一个步骤。

如果您的自定义视图处理API支持的手势之一,如果您的视图在响应链中的其他对象之前,则调用您的视图实现而不是任何其他实现。
但是,有某些系统范围的手势,例如四指滑动,系统实现优先于应用程序执行的任何手势处理。

注意: 您永远不应该通过在跟踪循环中按类型检查事件来处理手势事件——也就是说,由nextEventMatchingMask:untilDate:inMode:dequeue:等方法控制的循环。


2、触控事件代表触控板上的手指

您可以选择跟踪和处理构成手势的“原始”触摸,而不是处理手势。
但是您为什么会做出这样的选择呢?一个明显的原因是OS X无法识别您感兴趣的特定手势——也就是说,除了放大(捏进捏出)、旋转或滑动之外的其他东西。
或者您希望您的视图响应系统支持的手势,但您需要比AppKit框架当前提供的更多关于手势的信息;例如,您希望为缩放操作提供锚点。
除非您有这些原因,否则您应该更喜欢手势而不是原始触摸事件。

以下部分讨论在抽象意义上划分触摸事件的多点触控序列,指出重要的触摸事件属性,并向您展示如何处理触摸事件。


多点触控序列

当用户用一个或多个手指触摸触控板并将这些手指移动到触控板上时,硬件会生成代表触控板上每个手指的低级事件。
与所有类型的事件一样,事件流是连续的。
然而,有一个逻辑触摸单元,它们一起代表多点触控序列。
当用户将一个或多个手指放在触控板上时,多点触控序列就开始了。
手指可以在触控板上向不同方向移动,其他手指可以触摸触控板。
多点触控序列直到所有手指都从触控板上抬起才结束。

在多点触控序列中,触控板上的手指通常会经历不同的阶段:

  • 它接触到触控板。
  • 它可以以不同的速度向不同的方向移动。
  • 它可以保持静止。
  • 它从触控板上抬起。

AppKit框架使用NSTouch类的对象来表示多点触控序列的各个阶段的触摸。
也就是说,NSTouch对象是触控板上特定手指-触摸-在特定阶段的快照。
例如,当触摸向某个方向移动时,AppKit用一个NSTouch实例来表示它;当它从触控板上抬起时,它使用另一个NSTouch对象来表示同一个手指。

注意: 这种行为不同于iOS上的触摸对象,其中表示屏幕上手指的UITouch对象通过多点触摸序列持续存在,并在整个序列中发生突变。

每个touch对象都包含其相位作为一个属性,其值是例8-3中所示的NSTouchPhase常量之一。
它还有一个identity属性,用于通过多点触控序列跟踪NSTouch实例。
(Touch Identity和其他属性更详细地描述了触摸标识。)图8-2说明了多点触控序列以及这些属性在该序列中的作用。


图8-2 OS X上的多点触控序列

在这里插入图片描述


当序列中的第一个手指触摸触控板时,AppKit会将多点触控序列中的触摸附加到鼠标指针下方的视图上。
触摸会一直附加到该视图上,直到它结束(即手指抬起)或多点触控序列被取消。
对多点触控序列中触摸的任何解释都必须引用该视图,并且仅引用该视图。
您不能将触摸与同一多点触控序列中的不同视图相关联。
NSTouch对象表示的触摸在其视图中没有相应的屏幕位置或位置。
但是您可以确定它在触控板上不断变化的位置,并从该映射一个增量值来转换视图。


例8-3触摸阶段的常量

enum {
    NSTouchPhaseBegan           = 1U << 0,
    NSTouchPhaseMoved           = 1U << 1,
    NSTouchPhaseStationary      = 1U << 2,
    NSTouchPhaseEnded           = 1U << 3,
    NSTouchPhaseCancelled       = 1U << 4,
 
    NSTouchPhaseTouching        = NSTouchPhaseBegan | NSTouchPhaseMoved | NSTouchPhaseStationary,
    NSTouchPhaseAny             = NSUIntegerMax
};
typedef NSUInteger NSTouchPhase;


触摸身份和其他属性

对于NSTouch的实例,定义了许多属性。
一个特别重要的属性是identity
您可能还记得前面的讨论,应用程序创建了一个NSTouch对象来表示触控板上的同一手指,用于多点触控序列中的每个阶段。
尽管这些对象不同,但它们具有相同的identity对象值,使您能够在多点触控序列中跟踪触摸的变化。
您可以通过将两个触摸对象与isEqual:方法进行比较来确定它们是否引用触控板上的特定手指:



触摸标识对象和NSTouch对象本身都采用NSCopying协议。
因此,您可以复制它们。
此功能意味着您可以将触摸标识对象用作NSDictionary集合中的键。

另外两个重要且相关的NSTouch对象属性是normalizedPositiondeviceSize
触摸在屏幕上没有可见的位置。
(鼠标指针只能识别第一次接收触摸事件的视图,如果它实现了所需的方法。)然而,触摸在触控板上有一个位置。
触控板有一个坐标系,由触控板左下角的原点(0,0)定义,高度和宽度由deviceSize定义。
它在这个坐标系中的位置由normalizedPosition返回。
使用这两个属性,您可以导出触摸运动的增量值,并将其应用于被操纵视图的转换。
这在例8-6中的代码示例中进行了说明。


休息触摸

触控板驱动程序可能会将一个或多个手指(和拇指)识别为静止状态。
这种“静止”状态意味着手指或拇指实际上在数字转换器上,但驱动程序确定它不应该用于输入。
例如,对于无按钮触控板,用户可以将拇指放在触控板的底部,就像将手臂放在扶手上一样。
这个数字作为输入被忽略,它的移动不会移动鼠标指针。

驾驶员也可能在任何时候根据其评估将触摸转换为或退出静止状态。
静止的手指或拇指的运动并不总是决定因素。
移动的静止手指不一定会脱离“静止”。
另一方面,静止的手指或拇指根本不需要物理移动就可以进入和退出“静止”。

即使触摸被标记为“静止”以表示应该忽略它,驱动程序仍然会为它生成触摸数据,并在每个触摸阶段向应用程序发送事件。
默认情况下,这些事件不会传递给视图,也不包含在事件的触摸集中。
但是,您可以通过调用NSView方法setWantsRestingTouches:并使用YES的参数来打开此功能。
如果您不接受静止触摸,请注意NSTouchPhaseBeganNSTouchPhaseEnded事件是在触摸从静止状态转换到静止状态时生成的。


处理多点触控事件

默认情况下,视图不接受触摸事件。
要处理触摸事件,自定义视图必须首先调用NSView方法setAcceptsTouchEvents:,参数为YES
然后视图必须实现用于触摸事件处理的NSResponder方法:

- (void)touchesBeganWithEvent:(NSEvent *)event;
- (void)touchesMovedWithEvent:(NSEvent *)event;
- (void)touchesEndedWithEvent:(NSEvent *)event;
- (void)touchesCancelledWithEvent:(NSEvent *)event;

对于NSView的自定义子类,您应该实现这些方法中的每一个。
如果您子类处理触摸的类,您不需要实现所有这些方法方法,但您应该在您覆盖的方法中调用超类实现。

当触摸进入一个阶段时,应用程序会在视图上调用这些方法中的每一个——也就是说,当手指向下触摸时,当它在触控板上移动时,当它从触控板上抬起时,以及当操作系统出于某种原因取消多点触摸序列时。
可以为同一事件调用以下多个方法。
应用程序首先将这些消息发送到鼠标指针下的视图。
如果该视图不处理该事件,则该事件将向上传递到响应者链。

这些响应方法的单个参数是一个NSEvent对象。
您可以通过向事件对象发送touchesMatchingPhase:inView:并传递该阶段的常量来获取与给定阶段相关的NSTouch对象集。
例如,在touchesBeganWithEvent:方法中,您可以通过类似于以下的调用获得表示刚刚触摸的手指的触摸对象:

NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseBegan inView:self];

当外部事件(例如应用程序停用)中断当前的多点触控序列时,操作系统调用touchesCancelledWithEvent:
您应该实现touchesCancelledWithEvent:方法来释放分配给触摸处理的资源或将触摸处理中使用的瞬态重置为初始值。

其余代码示例取自*LightTable*sample-code项目。
首先,该项目声明了两个静态数组来保存初始触摸和当前触摸:

NSTouch *_initialTouches[2];
NSTouch *_currentTouches[2];

例8-4说明了touchesBeganWithEvent:方法的实现。
在这个方法中,视图获取与事件关联的所有触摸,如果有两个触摸,则将它们存储在两个静态数组中。


例8-4 中的处理触摸touchesBeganWithEvent:

- (void)touchesBeganWithEvent:(NSEvent *)event {
    if (!self.isEnabled) return;
 
    NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self.view];
 
    if (touches.count == 2) {
        self.initialPoint = [self.view convertPointFromBase:[event locationInWindow]];
        NSArray *array = [touches allObjects];
        _initialTouches[0] = [[array objectAtIndex:0] retain];
        _initialTouches[1] = [[array objectAtIndex:1] retain];
        _currentTouches[0] = [_initialTouches[0] retain];
        _currentTouches[1] = [_initialTouches[1] retain];
    } else if (touches.count > 2) {
        // More than 2 touches. Only track 2.
        if (self.isTracking) {
            [self cancelTracking];
        } else {
            [self releaseTouches];
        }
    }
}

当一个或两个手指移动时,调用touchesMovedWithEvent:方法。
LightTable中的视图实现了这个方法,如例8-5所示。
同样,它获取与事件关联的所有触摸对象,如果正好有两个触摸,它将当前触摸与它们的初始对应物匹配。
然后它计算原点和大小的增量值,如果这些值超过某个阈值,则调用一个操作方法来执行转换。


例8-5 处理触摸touchesMovedWithEvent:

- (void)touchesMovedWithEvent:(NSEvent *)event {
    if (!self.isEnabled) return;
    self.modifiers = [event modifierFlags];
    NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self.view];
    if (touches.count == 2 && _initialTouches[0]) {
        NSArray *array = [touches allObjects];
        [_currentTouches[0] release];
        [_currentTouches[1] release];
 
        NSTouch *touch;
        touch = [array objectAtIndex:0];
        if ([touch.identity isEqual:_initialTouches[0].identity]) {
            _currentTouches[0] = [touch retain];
        } else {
            _currentTouches[1] = [touch retain];
        }
        touch = [array objectAtIndex:1];
        if ([touch.identity isEqual:_initialTouches[0].identity]) {
            _currentTouches[0] = [touch retain];
        } else {
            _currentTouches[1] = [touch retain];
        }
        if (!self.isTracking) {
            NSPoint deltaOrigin = self.deltaOrigin;
            NSSize  deltaSize = self.deltaSize;
            if (fabs(deltaOrigin.x) > _threshold ||
                fabs(deltaOrigin.y) > _threshold ||
                fabs(deltaSize.width) > _threshold ||
                fabs(deltaSize.height) > _threshold) {
                self.isTracking = YES;
                if (self.beginTrackingAction)
                    [NSApp sendAction:self.beginTrackingAction to:self.view from:self];
            }
        } else {
            if (self.updateTrackingAction)
                [NSApp sendAction:self.updateTrackingAction to:self.view from:self];
        }
    }
}

例8-6显示了项目如何计算增量值来转换视图的原点。
注意代码如何在计算中使用NSTouch属性normalizedPositiondeviceSize中的值。

示例8-6 获取一个delta值,使用normalizedPositiondeviceSize

- (NSPoint)deltaOrigin {
    if (!(_initialTouches[0] && _initialTouches[1] &&
        _currentTouches[0] && _currentTouches[1])) return NSZeroPoint;
 
    CGFloat x1 = MIN(_initialTouches[0].normalizedPosition.x, _initialTouches[1].normalizedPosition.x);
    CGFloat x2 = MAX(_currentTouches[0].normalizedPosition.x, _currentTouches[1].normalizedPosition.x);
    CGFloat y1 = MIN(_initialTouches[0].normalizedPosition.y, _initialTouches[1].normalizedPosition.y);
    CGFloat y2 = MAX(_currentTouches[0].normalizedPosition.y, _currentTouches[1].normalizedPosition.y);
 
    NSSize deviceSize = _initialTouches[0].deviceSize;
    NSPoint delta;
    delta.x = (x2 - x1) * deviceSize.width;
    delta.y = (y2 - y1) * deviceSize.height;
    return delta;
}

最后,视图实现了touchesEndedWithEvent:touchesCancelledWithEvent:方法,如例8-7所示,主要用于取消对事件的跟踪。


例8-7处理结束和取消的触摸

- (void)touchesEndedWithEvent:(NSEvent *)event {
    if (!self.isEnabled) return;
    self.modifiers = [event modifierFlags];
    [self cancelTracking];
}
 
- (void)touchesCancelledWithEvent:(NSEvent *)event {
    [self cancelTracking];
} 

3、鼠标事件和触控板

操作系统将在触控板上移动的单指解释为埋鼠点,并以相应的速度和方向移动鼠标指针。
单指移动会生成手势事件和鼠标事件,尽管除非视图接受触摸,否则手势事件将被忽略。
在触控板上移动多个手指不会移动指针,尽管它们仍然可能导致滚动中使用的鼠标事件的生成。
无按钮触控板是一个例外,在无按钮触控板上,当指针移动时,两个手指可能会物理接触触控板,因为其中一个手指正在休息。

如果鼠标事件是由触摸事件生成的,则鼠标事件的子类型是NSTouchEventSubtype
您可以评估鼠标事件对象以确定何时忽略鼠标事件以支持触摸事件。
(此建议不适用于滚轮事件。)

您不应该尝试使用touchesMatchingPhase:inView:方法从鼠标事件中提取触摸。
尽管您可以检查事件对象的子类型以查看触摸是否生成了鼠标事件,但您不能将鼠标事件与任何特定的触摸相关联。
此外,您不能查询触摸事件以确定它是否也生成了鼠标事件。


十、监控事件

AppKit框架允许您安装事件监视器,该对象在应用程序在其sendEvent:方法中分派特定类型(或多种类型)的用户输入事件时查找它们。
例如,监视器可以查找鼠标向上、向下或滑动手势事件,甚至所有事件。

有两种类型的事件监视器,每种监视器的监控范围和功能都不同:

  • 全局事件监视器查找分派到安装它的应用程序以外的应用程序的用户输入事件。
    监视器不能修改事件或阻止其正常传递。
    它只能在启用可访问性或应用程序被信任可访问性的情况下监控关键事件。
    使用NSEvent方法addGlobalMonitorForEventsMatchingMask:handler:安装全局事件监视器。
  • 一个本地事件监视器查看正在分派到安装监视器的应用程序的用户输入事件。
    对于感兴趣的给定事件对象,本地监视器可以返回未修改的对象,创建并返回一个新的NSEvent对象,或者返回nil以停止事件的分派。
    使用NSEvent方法addLocalMonitorForEventsMatchingMask:handler:安装本地事件监视器。

注意:本地和全局事件监视器是互斥的。
例如,全局监视器不观察安装它的应用程序的事件流。
本地事件监视器只观察其应用程序的事件流。
要监视来自所有应用程序的事件,包括“当前”应用程序,您必须安装两个事件监视器。

这两种monitor-installation方法的参数几乎相同。
第一个参数是一个事件掩码,用于按类型指定感兴趣的事件。
第二个参数定义了一个块,用于执行监控事件的处理;对于与指定类型之一匹配的每个新事件,都会调用它。
对于这两种方法,NSEvent对象是块的唯一参数。
但是,addLocalMonitorForEventsMatchingMask:handler:的块处理程序被键入为返回一个NSEvent对象,而全局方法的块处理程序返回void
处理程序总是在主线程上调用。
两个类方法都返回调用对象不拥有的监视器对象(因此不需要保留或释放)。

重要提示: 完成监视事件后,您应该通过调用NSEvent类方法removeMonitor:删除监视器对象。
垃圾收集的应用程序不应该在finalize方法中删除事件监视器。
尽管内存管理的应用程序可以在dealloc方法中删除监视器,但不鼓励这样做。
在这两种情况下,事件监视器的删除应该在应用程序生命周期的早期进行。

在许多情况下,事件监视器可能对应用程序有用。
一个例子是一个弹出窗口,其作用类似于菜单。
应用程序想知道用户何时单击该窗口之外的窗口,以便关闭它。
它还想知道用户是按Escape键(在不保存更改的情况下关闭它)还是按Enter键(关闭它并保存更改)。
AnimatedTableView 示例代码项目安装了一个本地事件监视器(在ATColorTableController.m中)来执行这些功能。
例9-1显示了它是如何做到这一点的。


例 9-1 Installing a local event monitor

- (void)editColor:(NSColor *)color locatedAtScreenRect:(NSRect)rect {
 
    // code unrelated to event monitoring deleted here.....
 
    // Start watching events to figure out when to close the window
    NSAssert(_eventMonitor == nil, @"_eventMonitor should not be created yet");
    _eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:
            (NSLeftMouseDownMask | NSRightMouseDownMask | NSOtherMouseDownMask | NSKeyDownMask)
            handler:^(NSEvent *incomingEvent) {
        NSEvent *result = incomingEvent;
        NSWindow *targetWindowForEvent = [incomingEvent window];
        if (targetWindowForEvent != _window) {
            [self _closeAndSendAction:NO];
        } else if ([incomingEvent type] == NSKeyDown) {
            if ([incomingEvent keyCode] == 53) {
                // Escape
                [self _closeAndSendAction:NO];
                result = nil; // Don't process the event
            } else if ([incomingEvent keyCode] == 36) {
                // Enter
                [self _closeAndSendAction:YES];
                result = nil;
            }
        }
        return result;
    }];
}

当窗口关闭时,应用程序不再需要事件监视器。
所以它在关闭窗口时发布通知。
作为此通知的结果,调用例9-2中的方法,该类实现它以删除事件监视器(除其他外)。


例9-2删除事件监视器

- (void)_windowClosed:(NSNotification *)note {
    if (_eventMonitor) {
        [NSEvent removeMonitor:_eventMonitor];
        _eventMonitor = nil;
    }
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:_window];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidResignActiveNotification object:nil];
}

虽然事件监控可能是某些问题的理想解决方案,但对于其他问题,它可能不是最好的。
例如,*AnimatedTableView*应用程序安装了一个本地事件监视器,它可以检测发送到应用程序的鼠标事件,但无法检测发送到其他应用程序的鼠标事件。
但是如果用户在另一个应用程序中单击,应用程序需要关闭窗口。
为此,AnimatedTableView观察NSApplicationDidResignActiveNotification通知,而不是安装全局事件监视器。
全局事件监视器将无法检测命令选项卡或系统警报,这两者都应该导致窗口被关闭。
只有在没有其他方法可以解决您的问题时才应使用事件监视器。


十一、文本系统默认值和键绑定

本文档揭示了有关可用于自定义Cocoa文本系统行为的各种默认值的一些提示和技巧。
它还描述了如何自定义文本系统支持的键绑定。

重载子类可能会改变文本系统的部分或全部功能,使部分或全部这些功能处于非活动状态。


键绑定

文本系统使用了一种通用的键绑定机制,用户可以完全重新映射,尽管不支持动态定义自定义键绑定(即在应用程序运行时)。
键绑定在一个字典文件中指定,该文件的扩展名必须为.dict;该文件的格式应该是一个XML属性列表,但文本系统也可以理解旧式(NeXT时代)属性列表。
标准键绑定在/System/Library/Frameworks/AppKit.framework/Resources/StandardKeyBinding.dict
这些标准绑定包括大量与Emacs兼容的控制键绑定、所有各种箭头键绑定、用于使字段编辑器和一些键盘UI工作的绑定以及许多功能键的支持绑定。

要自定义绑定,您需要在~/Library/KeyBindings/中创建一个名为DefaultKeyBinding.dict的文件,并指定用于增强或替换标准绑定的绑定。
您可以使用标准绑定文件作为模板。
建议您使用属性列表编辑器应用程序来编辑绑定字典。
您可以使用其他应用程序,如TextEdit或Xcode,但如果这样做,您必须确保保存文件的编码为UTF8。

键绑定是键值对,键是指定物理键的字符串,值标识按下键时要调用的操作方法。
(其中许多操作方法由NSResponder声明。)您可以使用以下元素组合物理键字符串:

  • 出现在美国键盘上的字母数字字符。
    例如,“f”或 “>”. (如下所述,一些特殊字符保留给修饰符标志。)
  • 对于一些键,如Escape、Tab和反向删除(BS),ASCII表中标识键的八进制数。
    例如,标识Escape键(有时用作修饰符键)的八进制数是\033
  • 分配唯一Unicode值的enum常量,用于标识功能键。
    这些常量在NSEvent.h中定义。
    这些常量的示例是NSF7FunctionKeyNSHomeFunctionKeyNSHelpFunctionKey
  • 一个或多个键修饰符,它必须位于其他键标识符元素之一之前。
    以下特殊字符用于修饰符标志:
    • ^ 表示控制
    • ~ 为选项
    • $ 代表Shift
    • # 用于数字小键盘
      例如,当同时按下Control键时,以下字符串将标识数字小键盘上的0(零)键:“^#0”。

文本系统支持通过嵌套绑定字典指定多个击键绑定。
例如,Escape可以绑定为cancel:或者它可以绑定到整个字典,然后包含Escape之后下一次击键的绑定。

以下示例绑定文件说明了如何自定义绑定。
第一个示例为一些常见的Emacs行为添加了Option键绑定。
这在Option键绑定不是标准的情况下可能很有用。
对于这些绑定,需要键入“Control-Q, Option-f”才能键入florin字符而不是向前移动一个单词。
此示例还显式绑定了Escape以complete:(在OS X中,这是默认值,因此此覆盖不会更改任何内容。)

/* ~/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 */
 
    /* Escape should really be complete: */
    "\033" = "complete:";  /* Escape */
}

下面的示例展示了如何进行多击键绑定。
它使用Escape作为元键而不是Option修饰符绑定了许多Emacs元绑定。
所以Escape后跟“f”键表示moveWordForward:这里。
此示例绑定Escape-Escape以complete:
注意嵌套的字典

/* ~/Library/KeyBindings/DefaultKeyBinding.dict */
{
    /* Additional Emacs bindings */
    "\033" = {
        "\033" = "complete:";  /* ESC-ESC */
        "f" = "moveWordForward:";  /* ESC-f */
        "b" = "moveWordBackward:";  /* ESC-b */
        "<" = "moveToBeginningOfDocument:";  /* ESC-< */
        ">" = "moveToEndOfDocument:";  /* ESC-> */
        "v" = "pageUp:";  /* ESC-v */
        "d" = "deleteWordForward:";  /* ESC-d */
        "^h" = "deleteWordBackward:";  /* ESC-Ctrl-H */
        "\010" = "deleteWordBackward:";  /* ESC-backspace */
        "\177" = "deleteWordBackward:";  /* ESC-delete */
    };
}

完成指定键绑定后,您必须保存文件并重新启动应用程序,绑定才能生效。
通过键绑定和默认设置的正确组合,应该可以根据您的偏好定制文本系统。


1、选择和编辑的标准操作方法

这个NSResponder类声明了许多标准操作方法的方法原型,这些方法几乎都与操作选择和编辑文本有关。
这些方法通常通过doCommandBySelector:作为输入管理器解释的结果被调用。
它们分为以下几类:

  • 选择运动和扩展
  • 文本插入
  • 元素的一般删除
  • 修改选定的文本
  • 滚动文档

在大多数情况下,动作方法的意图从它的名字就很清楚了。
本规范中的各个方法描述也提供了关于这种方法通常应该做什么的详细信息。
然而,一些一般概念适用于其中许多方法,并在此处进行解释。


选型方向

一些方法涉及空间方向;左、右、上、下。
这些都是字面意思,尤其是在文本中。
为了适应不同于拉丁文字的方向性的书写系统,使用了向前、开始、向后和结束等术语。


选择和插入点

引用移动、删除或插入的方法意味着响应器中的某些元素被选中,或者在某个位置(插入点)有一个长度为零的选择。
这两件事必须始终一致处理。
例如,insertText:方法被定义为用提供的文本替换选择。
moveForwardAndModifySelection:方法扩展或收缩选择,即使选择只是一个插入点。
当第一次修改选择时,它必须始终被扩展。
因此,moveForward...消息从末尾扩展选择,而moveBackward...消息从开头扩展选择。


Marks

许多编辑文本的操作方法模仿了Emacs的point(插入点)和mark(图形界面中选择通常处理的较大操作的锚点)概念。
setMark:方法在当前选择处建立标记,然后该标记保持有效,直到再次更改标记。
selectToMark:方法扩展选择以包括标记以及选择和标记之间的所有字符。


The kill buffer

与Emacs一样,影响行、段落和标记的删除方法隐式地将删除的文本放入与粘贴板分开的缓冲区中,您可以稍后从中检索它。
方法如deleteToBeginningOfLine:将文本添加到此缓冲区,以及yank:将选择替换为杀死缓冲区中的项目。


2、文本系统默认值


NSMnemonicsWorkInText

允许值:“是”或“否”。

此默认值控制文本系统是否接受带有Option键的键事件。
默认值为NO
值为YES意味着任何带有Option位的键事件将被向上传递到响应器链,最终被视为助记符,而不是被文本接受为文本输入或键绑定命令。
如果此默认值设置为NO,则设置了Option位的键事件将通过文本系统的正常键输入序列传递。
这将允许任何涉及Option的键绑定工作(例如Emacs风格的绑定,如Option-f用于文字转发),并允许键入特殊的国际和符号字体字符。


NSRepeatCountBinding

允许值:键绑定样式字符串。

此默认值控制数字参数绑定。
默认值是不支持数字参数。
如果您为此默认值提供绑定,则启用该功能。
这允许您重复给定次数的键盘命令。
例如,Control-U 10 Control-F 表示向前移动十个字符。


NSQuotedKeystrokeBinding

允许值:键绑定样式字符串。

此默认值控制引用绑定。
默认值为“^q”(即Control-Q)。
此绑定允许您按字面意思输入字符,否则这些字符将被解释为命令。
例如,Control-Q Control-F 将在文档中插入一个Control-F字符,而不是执行命令moveForward:


NSTextShowsInvisibleCharacters

允许值:“是”或“否”。

默认值控制文本对象是否默认显示不可见字符,如制表符、空格和使用某些可见字形的回车符。
默认值为NO
它仅控制NSLayoutManager对象的默认设置(可以通过编程方式修改)。
为了使其工作,生成字形的规则书必须支持该功能。
目前我们的规则书不支持此功能,因此目前此默认值不是很有用。


NSTextShowsControlCharacters

允许值:“是”或“否”。

默认值控制文本对象是否会在默认情况下显示控制字符(通常通过在文本中将Control-C表示为“^C”)。
默认值为NO
它只控制NSLayoutManager对象的默认设置(可以通过编程方式进行修改)。
为了使其工作,生成字形的规则书必须支持该功能。
此功能需要付出代价。
它将大大增加包含控制字符的文档所需的内存。
小心使用它。


NSTextSelectionColor

允许的值:颜色对象或说明符。

此默认值控制选定文本的背景颜色。
默认情况下,这是浅灰色。
接受颜色的默认值以三种方式之一接受它们。
要么作为存档的NSColor对象,要么作为三个RGB组件,要么作为可以解析为NSColor上的工厂选择器的字符串,该字符串将返回所需的颜色(例如,“redColor”)。
请注意,使用字段编辑器编辑文本的NSTextField对象和其他控件控制自己的选择属性以符合UI。


NSMarkedTextAttribute和NSMarkedTextColor

允许的值:颜色对象/说明符或“下划线”。

此默认值控制标记文本的显示方式。
NSMarkedTextAttribute可以是“Background”或“Underline”。
如果是“Background”,则NSMarkedTextColor表示用于标记文本的背景颜色。
如果NSMarkedTextAttribute是“Underline”,NSMarkedTextColor表示用于标记文本的前景色(标记文本将以指定的颜色绘制并带下划线)。
默认情况下,标记文本使用黄色背景颜色绘制。
接受颜色的套件默认值以三种方式之一接受它们。
要么作为存档的NSColor对象,要么作为三个RGB组件,要么作为可以解析为NSColor上的工厂选择器的字符串,这将返回所需的颜色(例如,“redColor”)。
如果NSMarkedTextAttribute默认值包含颜色而不是字符串“Background”或“Underline”之一,则该颜色用作标记文本的背景颜色,并且忽略NSMarkedTextColor属性。


NSTextKillRingSize

允许值:数字字符串。

此默认值控制终止环的大小(如在Emacs Control-Y中)。
默认值为1(根本不是真正的环,只是一个缓冲区)。
如果您将其设置为大于1的值,您还需要将Control-Y重新绑定到yankAndSelect:而不是yank:才能正常工作(请注意,yankAndSelect:未在任何标题中列出)。
有关绑定的更多信息,请参阅键绑定。


十二、鼠标跟踪和光标更新事件

当鼠标指针(没有按下鼠标按钮)进入和退出窗口的区域时,鼠标跟踪消息被发送到对象。
该区域称为跟踪矩形或跟踪区域。
鼠标跟踪使拥有该区域的视图能够做出响应,例如,通过绘制高亮颜色或显示工具提示。
光标更新事件是一种特殊的鼠标埋点,应用程序工具包会自动处理。
当鼠标指针进入光标矩形时,应用程序工具包会显示适合矩形下视图类型的光标图像;例如,当鼠标指针进入文本视图时,会显示一个工字形光标。

本章中的部分描述了如何设置跟踪矩形并响应鼠标跟踪事件。
他们还讨论了如何为光标更新事件指定和管理矩形。

重要提示: 本附录描述了用于处理鼠标跟踪和光标更新事件的遗留API。
这些任务的首选API是由NSTrackingArea类定义的API以及在NSView类中声明的相关方法。
有关详细信息,请参阅使用Tracking-Area对象。


1、处理鼠标跟踪事件

为跟踪鼠标移动而设置的视图区域称为跟踪矩形。
当鼠标光标进入跟踪矩形时,Application Kit会向拥有该矩形的对象(不一定是视图本身)发送鼠标输入事件(类型NSMouseEntered);当光标离开矩形时,Application Kit会发送对象鼠标退出事件(类型NSMouseExitedNSMouseExed)。
这些事件分别对应于NSRespondermouseEntered:mouseExited:方法。
鼠标跟踪对于显示上下文相关消息或突出显示光标下的图形元素等任务非常有用。
NSView对象可以有任意数量的跟踪矩形,它们可以重叠或嵌套在另一个中;为表示鼠标跟踪事件而生成的NSEvent对象包括一个标签(通过trackingNumber方法访问),该标签标识与事件关联的矩形。

重要提示: 不能保证应用程序中跟踪矩形接收到的鼠标输入和鼠标退出事件的正确顺序。
例如,如果您将鼠标光标从视图中的一个跟踪矩形移动到另一个跟踪矩形并向后移动,事件(作为消息)的顺序可以是:mouseEntered:mouseEntered:mouseExited:mouseExited:

要创建跟踪矩形,请向与该矩形关联的NSView对象发送addTrackingRect:owner:userData:assumeInside:消息,如管理跟踪区域对象。
此方法为跟踪矩形注册所有者,以便所有者接收跟踪事件消息。
所有者通常是视图对象本身,但不一定是。
该方法返回跟踪矩形的标记,以便您可以将其存储在事件处理方法中以供以后参考mouseEntered:mouseExited:
要删除跟踪矩形,请使用removeTrackingRect:方法,该方法将要删除的跟踪矩形的标记作为参数。


例A-1向视图区域添加跟踪矩形

- (void)viewDidMoveToWindow {
    // trackingRect is an NSTrackingRectTag instance variable
    // eyeBox is a region of the view (instance variable)
    trackingRect = [self addTrackingRect:eyeBox owner:self userData:NULL assumeInside:NO];
}

在上面的示例中,自定义视图在viewDidMoveToWindow方法中添加了跟踪矩形,而不是initWithFrame:
尽管NSView实现了addTrackingRect:owner:userData:assumeInside:方法,视图的窗口维护跟踪矩形列表。
当调用视图的initWithFrame:初始化程序时,视图还没有与窗口关联,因此跟踪矩形还不能添加到窗口列表中。
因此,最初添加跟踪矩形的最佳位置是在viewDidMoveToWindow方法中。

跟踪矩形边界包括顶部和左侧边缘,但不包括底部和右侧边缘。
因此,如果您有一个未翻转的视图,其中跟踪矩形覆盖了其边界,并且视图的框架具有几何形状frame.origin = (100, 100), frame.size = (200, 200),那么跟踪矩形处于活动状态的区域是frame.origin = (100, 101), frame.size = (199, 199),在帧坐标中。

跟踪矩形也可用于向mouseMoved:方法中的视图提供NSMouseMoved事件。
但是,要让视图接收NSMouseMoved事件,必须发生两件事:

  • 视图必须是第一个响应者。
  • 视图的窗口必须发送一个参数为YESsetAcceptsMouseMovedEvents:消息。

如事件对象和类型以及处理鼠标事件中所述,默认情况下,NSWindow对象不会接收NSMouseMoved事件,因为它们很容易淹没事件队列。
如果您只想在鼠标悬停视图时接收鼠标移动的消息,则应在鼠标跟踪会话完成时再次关闭它们。

您通常会在mouseEntered:的实现中发送setAcceptsMouseMovedEvents:消息(参数为YES)。
如果您想在跟踪会话结束后关闭它们,您可以在mouseExited:的实现中再次发送参数为NO的消息。
但是,您还应该将窗口状态设置回打开鼠标移动事件之前的状态,以确保窗口不会停止接收鼠标移动事件,如果它需要它们用于其他目的的话。

在例6-2中的跟踪代码中,当一个“眼球”进入一个跟踪矩形时,它会跟随鼠标指针的移动。
注意,mouseEntered:实现使用wasAcceptingMouseEvents实例变量来捕获窗口在为当前跟踪会话打开鼠标移动事件之前的当前状态;稍后,在mouseExited:中,这个实例变量的值被用作setAcceptsMouseMovedEvents:的参数,从而重置窗口状态。


例A-2处理鼠标输入、鼠标移动和鼠标退出事件

- (void)mouseEntered:(NSEvent *)theEvent {
    wasAcceptingMouseEvents = [[self window] acceptsMouseMovedEvents];
    [[self window] setAcceptsMouseMovedEvents:YES];
    [[self window] makeFirstResponder:self];
    NSPoint eyeCenter = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    eyeBox = NSMakeRect((eyeCenter.x-10.0), (eyeCenter.y-10.0), 20.0, 20.0);
    [self setNeedsDisplayInRect:eyeBox];
    [self displayIfNeeded];
}
 
- (void)mouseMoved:(NSEvent *)theEvent {
    NSPoint eyeCenter = [self convertPoint:[theEvent locationInWindow] fromView:nil];
    eyeBox = NSMakeRect((eyeCenter.x-10.0), (eyeCenter.y-10.0), 20.0, 20.0);
    [self setNeedsDisplayInRect:eyeBox];
    [self displayIfNeeded];
}
 
- (void)mouseExited:(NSEvent *)theEvent {
    [[self window] setAcceptsMouseMovedEvents:wasAcceptingMouseEvents];
    [self resetEye];
    [self setNeedsDisplayInRect:eyeBox];
    [self displayIfNeeded];
}

因为跟踪矩形是由NSWindow对象维护的,所以跟踪矩形是一个静态实体;当NSView对象移动或改变其大小时,它不会移动或改变其大小。
如果使用跟踪矩形,则应确保在更改包含它们的视图对象的框架矩形时删除并重新建立它们。
如果要创建NSView的自定义子类,则可以覆盖setFrame:setBounds:方法来执行此操作,如兼容性问题所示。
如果您的类不是自定义视图类,您可以将类实例注册为通知 NSViewFrameDidChangeNotification的观察者,并让它在收到通知时重新建立跟踪矩形。


例A-3重置跟踪矩形

- (void)setFrame:(NSRect)frame {
    [super setFrame:frame];
    [self removeTrackingRect:trackingRect];
    [self resetEye];
    trackingRect = [self addTrackingRect:eyeBox owner:self userData:NULL assumeInside:NO];
}
 
- (void)setBounds:(NSRect)bounds {
    [super setBounds:bounds];
    [self removeTrackingRect:trackingRect];
    [self resetEye];
    trackingRect = [self addTrackingRect:eyeBox owner:self userData:NULL assumeInside:NO];
}

当视图从其窗口中删除时,您还应该删除跟踪矩形,这可能是因为视图被移动到不同的窗口,也可能是因为视图作为释放的一部分被删除。
一个地方是viewWillMoveToWindow:方法,如兼容性问题所示。


例A-4当视图从其窗口中删除时删除跟踪矩形

- (void)viewWillMoveToWindow:(NSWindow *)newWindow {
    if ( [self window] && trackingRect ) {
        [self removeTrackingRect:trackingRect];
    }
}


2、管理游标更新事件

跟踪矩形的一个常见用途是在不同类型的图形元素上更改光标图像。
例如,文本通常需要一个工字梁光标。
更改光标是如此常见的操作,以至于NSView定义了几种方便的方法来简化该过程。
由这些方法生成的跟踪矩形称为光标矩形。
应用程序套件本身假定光标矩形的所有权,因此当用户将鼠标移动到矩形上时,光标会自动更改为适当的图像。
与一般的跟踪矩形不同,光标矩形可能不会部分重叠。
然而,它们可能完全嵌套,一个在另一个内。

因为游标矩形需要随着视图的大小和图形元素的变化而经常重置,NSView定义了一个方法resetCursorRects,该方法在需要重新建立游标矩形时随时调用。
一个具体的子类覆盖此方法,调用addCursorRect:cursor:对于它希望设置的每个游标矩形(如例A-5所示)。
此后,可以通过调用NSWindow方法invalidateCursorRectsForView:.在调用resetCursorRects之前,拥有的视图会自动发送一个disableCursorRects消息以删除现有的游标矩形。


例6-4显示了resetCursorRects的resetCursorRects


例A-5重置光标矩形

-(void)resetCursorRects
{
    [self addCursorRect:[self calculatedItemBounds] cursor:[NSCursor openHandCursor]];
}

虽然可以使用removeCursorRect:cursor:临时删除单个游标矩形,但应该很少需要这样做。
每当需要重建游标矩形时,NSView都会调用resetCursorRects,这样就可以只建立需要的游标矩形。
如果以这种方式实现resetCursorRects,那么就可以简单地修改此方法用于构建其游标矩形的状态,然后调用NSWindow方法invalidateCursorRectsForView:

无论何时,NSView对象的光标矩形都会自动重置:

  • 它的框架或边界矩形变化,无论是通过setFrame...setBounds...消息还是通过自动调整大小。
  • 它的窗口被调整大小。
    在这种情况下,所有窗口的视图对象都将重置其光标矩形。
  • 它在视图层次结构中移动。
  • 它在NSScrollViewNSClipView对象中滚动。

您可以使用NSWindow方法disableCursorRects暂时禁用窗口中的所有光标矩形disableCursorRects并使用enableCursorRects方法再次enableCursorRects它们。
NSWindowareCursorRectsEnabled方法告诉您它们当前是否已启用。


2024-06-16(日)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/734732.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何通过防泄密U盘,实现数据传输的安全性及可控性?

随着信息技术的发展&#xff0c;U盘作为重要的数据存储和传输工具&#xff0c;其安全性越来越受到关注。在日常办公中&#xff0c;经常会遇到这类情况&#xff1a;员工为了方便&#xff0c;随意使用U盘拷贝公司的机密资料。一旦U盘丢失或者被窃取&#xff0c;公司的机密资料就有…

Part 8.2 最短路问题

很多题目都可以转化为最短路的模型。因此&#xff0c;掌握最短路算法非常重要。 >最短路模板< 【模板】全源最短路&#xff08;Johnson&#xff09; 题目描述 给定一个包含 n n n 个结点和 m m m 条带权边的有向图&#xff0c;求所有点对间的最短路径长度&#xff…

vue-Router实现原理

http://localhost:8080/home 三、HashHistory hash("#")的作用是加载 URL 中指示网页中的位置。# 号后面的 hash值&#xff0c;可通过 window.location.hash 获取 特点&#xff1a; hash 不会被包括在 http 请求中&#xff0c;&#xff0c;对服务器端完全无用&…

《黑悟空》抢先版

当《西游记》的古老传说与现代潮流碰撞&#xff0c;一个全新的西游世界在《黑神话悟空》中缓缓展开。你&#xff0c;作为被选中的“天命人”&#xff0c;将踏上一段寻找真相的奇幻旅程。在这里&#xff0c;中国神话的深邃与东方魔幻的绚丽交织&#xff0c;构建出一个令人叹为观…

VBA技术资料MF165:关闭当前打开的所有工作簿

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

Origin较好用的科研绘图软件

推荐自己也在用的科研绘图软件Origin图所示&#xff1a; 图1 图2 图3

Android 装逼技术之暗码启动应用

AdapterView.OnItemClickListener, TextWatcher, PopupMenu.OnMenuItemClickListener, DialpadKeyButton.OnPressedListener { //…… Override public void afterTextChanged(Editable input) { // When DTMF dialpad buttons are being pressed, we delay SpecialChar…

FPGA国内”薪“赛道-在医疗领域的应用

mian 免 ze 责 sheng 声 ming 明 以下观点仅代表个人观点&#xff0c;不代表任何公司或者行业 从下游应用市场来看&#xff0c;通信和工业市场份额位居FPGA芯片一二位&#xff0c;同时通信市场份额有望持续提升。但是目前通信和工业市场趋于稳定&#xff0c;FPGA厂商一直推AI市…

408计算机组成原理

todo:有逻辑的分门别类的整理笔记&#xff0c;方便复习 总 理解不了就直接背下来&#xff0c;学越多就越能理解 计算机系统概述 简要目录 基本概念 字长 MAR MDR PC IR CU ALU 通用寄存器、标志寄存器、标志控制器 ACC 地址译码器 通用寄存器 PU C语言编译过程 数据通路带…

FlinkCDC sink paimon 暂不支持exactly-once写入,而通过 幂等写

幂等写入&#xff1a; 一个幂等操作无论执行多少次都会返回同样的结果。例如&#xff0c;重复的向hashmap中插入同样的key-value对就是幂等操作&#xff0c;因为头一次插入操作之后所有的插入操作都不会改变这个hashmap&#xff0c;因为hashmap已经包含这个key-value对了。另一…

基于matlab的BP神经网络分类预测

1.神经网络结构 本文网络结构如图1所示&#xff1a; 图1 网络结构 图1给出的并不是单纯的bp神经网络结构这里设置了三个隐藏层&#xff0c;神经元个数分别为6&#xff0c;3&#xff0c;3&#xff0c;输入层12个特征输入&#xff0c;输出层输出4个类型结果。 2.代码 %% 清空环…

自动驾驶仿真Carla -ACC功能测试

我将详细说明如何使用Carla进行ACC&#xff08;自适应巡航控制&#xff09;测试&#xff0c;确保每个步骤贴合实际的Carla自动驾驶仿真标准&#xff0c;并提供相应的代码示例。 使用Carla进行ACC测试的步骤&#xff1a; 1. 环境设置和启动Carla 首先&#xff0c;确保你已经安装…

bug记录——C语言中运算符前假后面不执行

A&&B A为真&#xff0c;才会判断B&#xff0c; 所以如果B访问越界的情况下必有A为假&#xff0c;那么代码是正确的 像这里&#xff0c;当child 1 > n时&#xff0c;a[child 1]越界访问&#xff0c; 但由于&&前面判断了child 1 < n为假&#xff0c;所以…

element-ui里message抖动问题

由于element默认屏蔽滚动条&#xff0c;导致取消时弹message时 侧边滚动栏突然回来后引起抖动问题 是由于打开弹窗时出现遮罩层dialog对话框 时引起了元素内容超出自身尺寸 对应的overflow样式内容为hidden&#xff0c;且新建了一个class类内容为增加17 内右边距&#xff0c;当…

QML 实现上浮后消失的提示框

基本效果&#xff1a;上浮逐渐显示&#xff0c;短暂停留后上浮逐渐消失 为了能同时显示多个提示框&#xff0c;一是需要动态创建每个弹框 Item&#xff0c;二是弹出位置问题&#xff0c;如果是底部为基准位置就把已经弹出的往上移动。 效果展示&#xff1a; 主要实现代码&…

区块链中nonce是什么,什么作用

目录 区块链中nonce是什么,什么作用 区块链中nonce是什么,什么作用 Nonce在以太坊中是一个用于确保交易顺序性和唯一性的重要参数。以下是对Nonce的详细解释: 定义 Nonce是一个scalar值,它等于从该地址发送的交易数量,或在具有关联代码的账户的情况下,由该账户创建的合…

掌握Three.js:学习路线,成为3D可视化开发的高手!

学习Three.js可以按照以下路线进行&#xff1a; 基础知识&#xff1a; 首先要了解基本的Web开发知识&#xff0c;包括HTML、CSS和JavaScript。如果对这些知识已经比较熟悉&#xff0c;可以直接进入下一步。 Three.js文档&#xff1a; 阅读Three.js官方文档是学习的第一步。官…

192.回溯算法:电话号码的字母组合(力扣)

代码解决 class Solution { public:// 定义每个数字对应的字母映射const string letterMap[10] {"", // 0"", // 1"abc", // 2"def", // 3"ghi", // 4"jkl", // 5"mno", // 6"pqrs&…

软件测试----用例篇(设计测试用例保姆级教程✅)

文章目录 前言一、测试用例概念 二、如何设计测试用例三、设计测试用例的方法3.1基于需求的设计方法3.2具体的设计方法等价类边界值正交法判定表法场景法错误猜测法 前言 在软件开发过程中&#xff0c;测试用例是至关重要的一环。它们帮助软件开发人员和测试人员确定软件是否按…