本文是Apple Documentation Archive View Programming Guide的学习笔记。

创建一个自定义视图

Creating a Custom View

NSView是所有视图类型的抽象基类,虽然它定义了视图的很多行为,比如如何摆放视图、以及如何跟用户交互,但是NSView的绘制功能是缺失的。一般来说,需要从NSView中派生出一个新的类,来处理相关的绘制。

这个章节用一个NSView的派生类的例子:DraggableItemView来展示以下行为:

  • 分配和回收视图
  • 绘制视图内容
  • 让视图的位置随着外部条件变化
  • 响应用户鼠标和键盘事件
  • 根据鼠标所在位置改变鼠标形状
  • 实现NSResponder
  • 添加key-value-coding功能

该示例的代码可以从DragItemAround获取。

创建一个视图

使用NSView的initWithFrame:方法可以实例化一个视图。派生类必须提供这个方法。就算派生类提供了其他变体,比如:initWithFrame:textContainer:。也必须尊从initWithFrame:先调用,变体后调用的规则。

加载InterfaceBuilder创建的nib,并从中创建视图的时候,并不需要调用initWithFrame:,因为nib文件中保存有类实例的副本,直接复制并恢复成一个视图就行了。在这个过程中会调用awakeFromNib,可以自定义这个方法,提供一些初始化操作。

对于自定义视图,InterfaceBuilder支持代理功能(Custom View proxy),这样做InterfaceBuilder不需要知道自定义视图的类型,也能为它在窗口找到一席之地。但是这样依赖,nib中保存的就不是视图对象的副本了,而是代理信息。在加载nib的时候,根据代理信息实时创建视图类,这样initWithFrame:就会得到调用,然后awakeFromNib也会被调用。

还有另一种情况,自定义类是从InterfaceBuilder已知的视图类型中派生出来的,比如从NSScrollView派生出MyScrollView。你可以告诉InterfaceBuilder把MyScrollView归档到nib中,但是由于InterfaceBuilder只知道基类,也就是NSScrollView,随意只会归档NSScrollView的副本。派生的部分需要nib加载的时候在awakeFromNib里面初始化。派生类的initWithFrame:不会被调用。

绘制视图

setNeedsDisplay:setNeedsDisplayInRect:可以无效化视图的全部或者部分画界,从而请求Cocoa在合适的时候自动绘制失效的视图(可以通过NSWindow的 NSWindow setAutodisplay:来关闭自动绘制)。由于绘制操作影响比较大,Cocoa会及延迟绘制。即便需要绘制,也尽量锁定在局部必要的视图上。需要绘制的视图的drawRect:会得到调用。但是可以通过NSWindow或者NSView的display... 系列方法来强制绘制。你可以锁定视图的焦点,避免交互,绘制完之后再释放。如果要避免重绘背景视图,可以调用displayRectIgnoringOpacity:displayIfNeededIgnoringOpacity以及displayIfNeededInRectIgnoringOpacity:等。

在打印的时候每个视图要自己提供打印的内容,具体参考Printing Programming Guide for Mac

drawRect:是NSView的每个派生类需要实现的,它在给定画界内绘制一个二维像素数组。实际情况下,若干个视图的drawRect:请求可能会被合并成一个,以提高绘制效率。

从MVC的角度,最好视图自己来控制哪部分需要绘制,而外部只是提供信息。Cocoa提供了一个键值变化通知框架,可以用来服务于这个目的。具体参考Key-Value Observing Programming Guide

display...系列方法在调用的时候,需要找到一个前继的isOpaque为YES的视图,然后从那里开始向后续视图绘制。当一个视图能保证自己所以区域内没有透明的部分(不会影响到其他视图),那么它的isOpaque就可以返回YES。NSView的isOpaque默认返回NO。isOpaque方法在绘制的时候可能会被调用多次,所以要避免做太多复杂的操作在里面。

相应用户事件以及行动指示

NSView是从NSResponder派生出来的,所以NSView可以响应用户事件,以及接收其他模块发送过来的行动指示(Action)。在事件或者行动发生时,承担第一响应者(first responder)的NSView负责响应。第一响应者处于反应链的头部。对于所有的视图(除了窗口的主视图之外),视图的下一响应者是其父视图。当视图被插入一个视图层叠,它们之间的响应链关系也会随着更新。虽然NSView提供了setNextResponder:,但是你不需要调用它。如果你需要添加对象到窗口的反应链,要么在窗口没有委托的时候从窗口类中派生一个自定义类,要么在窗口的委托中处理。

用户事件一般包括鼠标事件和键盘事件,鼠标事件在指针所在的视图触发,然后沿着反映了回溯;键盘事件在第一响应者触发,然后沿着反应链触发。

参考Cocoa Event Handling Guide对此有更详细说明。

第一响应者除了响应键盘事件外,还响应其他模块发来的行动指示。视图可以通过在acceptsFirstResponder返回YES来声明自己可以当第一响应者(默认此方法返回NO)。如果一个视图不是第一响应者的话,它只会接收到鼠标按下的消息。当一个视图即将变成第一响应者的时候,窗口会给它发送becomeFirstResponder消息;当窗口想取消某个视图的第一响应者资格的话,会发送resignFirstResponder消息,视图可以拒绝这条消息。成为第一响应者的视图通常会画一个焦点圈(focus ring)来告诉用户它获得了焦点。

可以成为第一响应者的视图通常会被组织在一起,成为一个键视图环(key-view loop),处于这个环中的视图可以用Tab或者Shift-Tab切换谁成为第一响应者。NSView提供了一些列方法来获取键视图环中的视图。在Interface Builder中可以通过nextKeyView这个outlet来把视图组织成键视图环。

自定义视图可以自行对鼠标事件做出解释。传递给视图的鼠标事件基本有四大类:鼠标按下、鼠标拖动、鼠标释放、鼠标移动。

前面提到的,非焦点视图默认不会收到鼠标按下(mouseDown:rightMouseDownotherMouseDown:)事件,默认情况下鼠标按下的时候只会使视图所在窗口变为键窗口,但是鼠标按下事件本身却会被抛弃。但是可以通过acceptsFirstMouse返回YES来使窗口成为键窗口,并且把鼠标按下这个事件传递给那个视图。窗口通过NSView的hitTest:来测试视图层叠中的哪个视图应该获得事件。事件的locationInWindow方法可以返回其窗口坐标。使用nil参数调用convertPoint:fromView:可以将窗口坐标转化为视图坐标。isPointInItem: 可以测试事件是否发送在视图的画界。

对于鼠标移动事件,视图默认不接收,触发窗口必须主动通过setAcceptsMouseMovedEvents:表明接收此类事件。这个事件是窗口级别的,单个视图无法自定义。但是,单个视图可以做的是通过addTrackingRect:owner:userData:assumeInside:注册跟踪一个矩形区域,这样视图可以接收到mouseEntered:mouseExited:这两个事件。注意,这两个事件可以绑定userData,并且采用的系统是视图的坐标系统。removeTrackingRect:可以取消跟踪。注意的是,这个被跟踪的矩形区域不会随着窗口位置和大小的调整而调整。需要自定义对resetCursorRects消息的处理来对矩形进行调整。 invalidateCursorRectsForView:可以促使窗口发出resetCursorRects消息。addCursorRect:cursor:允许视图在鼠标经过某个矩形区域的时候改变鼠标形状。 removeCursorRect:cursor: 允许你删除某个相符的矩形区域。而discardCursorRects则删除所有的矩形区域。

对于输入事件响应者而言,NSResponder提供了两个相应的通知,第一个是keyDown:第二个是performKeyEquivalent:。NSResponder还提供了一些预定义的行为,让使用者不需要去处理麻烦的keyDown:performKeyEquivalent:主要是用来绑定输入,比如把回车键绑定到一个按键控件上。

NSResponder并不是唯一能够给反应链输入事件的对象。任何控件,只要自定义了行动指令,都可以发送事件。前面提到的Cocoa Event Handling Guide的The Responder Chain中有更好的说明。

自定的是视图类必须提供对其所有的公有属性提供获取健值编码(key-value-coding)的访问者方法(accessor method)。这在一定程度上提供封装。

当视图不被其他对象持有时,它的dealloc方法会被调用。应用程序不必显示调用dealloc方法。

(本篇完)