本文翻译整理自:View Programming Guide(更新:2013-08-08
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaViewsGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40002978-CH1-SW1
文章目录
- 一、查看Cocoa编程指南简介
- 谁应该阅读此文档
- 本文件的组织
- 另见
- 二、什么是视图?
- 1、NSView的作用
- 2、Cocoa提供的视图类
- 容器视图
- 文本系统
- 控件
- 非石英图形环境
- 三、View Geometry
- 1、视图坐标系
- 2、了解视图的框架和边界
- 3、变换坐标系
- 四、使用视图层次结构
- 1、什么是视图层次结构?
- 2、视图层次结构的好处
- 3、在视图层次结构中定位视图
- 4、从层次结构中添加和删除视图
- 5、重新定位和调整视图大小
- 以编程方式移动和调整视图大小
- 子视图的自动调整
- 通知
- 6、隐藏视图
- 7、在视图层次结构中转换坐标
- 8、转换视图坐标到和从基空间
- 9、查看标签
- 五、创建自定义视图
- 1、分配视图
- 初始化在Interface Builder中创建的视图实例
- 2、绘图查看内容
- 实现Drarect:方法
- 将视图标记为需要显示
- 查看不透明度
- 3、响应用户事件和操作
- 成为第一反应者
- 处理鼠标单击和拖动事件
- 跟踪鼠标移动
- 在视图中处理关键事件
- 通过响应链处理操作方法
- 4、Property Accessor Methods
- 6、取消分配视图
- 六、高级自定义视图任务
- 1、确定输出设备
- 2、绘图外部绘图:
- 七、优化视图绘图
- 1、避免过度使用视图
- 2、指定视图不透明度
- 3、使视图的某些部分无效
- 4、约束绘图以提高性能
- 5、抑制默认裁剪
- 6、在动态窗口调整大小期间绘图
- 最小绘制
- Cocoa Live调整大小通知
- 保留窗口内容
一、查看Cocoa编程指南简介
视图实例负责在窗口的矩形区域中绘制和响应用户操作。
本文档描述了视图在Cocoa应用程序中的作用、如何操作窗口中的视图以及如何为应用程序创建自定义视图子类。
谁应该阅读此文档
您应该阅读本文档以了解如何在Cocoa应用程序中使用视图。
您应该熟悉Cocoa开发,包括Objective-C语言和内存管理。
创建自定义视图这篇文章希望开发人员熟悉 Cocoa事件处理指南 中描述的Cocoa事件模型以及 Cocoa绘图指南 中描述的图形绘制环境。
本文件的组织
查看Cocoa编程指南由以下章节组成:
- 什么是视图?描述了视图在Cocoa应用程序中的作用,并概述了Cocoa提供的视图。
- 视图几何描述了视图如何建立它们的基本坐标系。
- 使用视图层次结构描述了应用程序如何从视图层次结构中插入和删除视图。
- 创建自定义视图描述了应用程序可以子类化的
NSView
的各个方面,并提供了自定义NSView
子类的剖析。 - 高级自定义视图任务描述高级视图子类绘图任务。
- 优化视图绘图描述了优化视图绘图的技术。
另见
本文档未完全介绍的其他技术对于在应用程序中使用视图至关重要。
有关更多详细信息,请参阅这些文档:
- Cocoa事件处理指南 描述了Cocoa应用程序使用的事件模型,并解释了您的对象如何处理事件并参与响应链。
- 可可绘图指南 介绍了用于绘制曲线、填充形状和修改坐标系的基本方法。
- 拖放编程主题 描述了如何在视图子类中实现拖放。
还有一些示例代码提供了视图使用的详细示例。
/Developer/Examples/Appkit
中安装了以下示例代码:
- DotView是一个简单的应用程序,它实现了一个基本的
NSView
子类。 - Sketch是一个可编写脚本的图形应用程序。
它提供了对复杂视图子类的查看,而不是处理许多类型的事件。 - Worm提供了三种不同的
NSView
实现,它们演示了提高视图性能的技术。
其他示例代码可通过Apple Developer Connection获得:
- 绑定操纵杆实现了一个“操纵杆”用户交互界面项,它说明了
NSView
的支持绑定的子类。 - 彩色采样演示使用
lockFocus
从视图中读取像素颜色。 - 减少器演示了核心图像、
NSAnimation
类和视图绘图重定向的使用。
包括一个启用Cocoa绑定的可折叠NSView
子类。
二、什么是视图?
在Cocoa中,视图是包含在窗口中的屏幕的矩形部分。
它负责处理其框架内的所有绘图和用户发起的事件。
Cocoa提供了NSView
类作为抽象视图实现,子类使用它作为实现自定义显示和用户交互的基础。
1、NSView的作用
Cocoa提供了一个高级类NSView
,它实现了基本的视图行为。
NSView
实例定义视图的矩形位置,称为其框架矩形,并负责在该区域内呈现其数据的可视化表示。
除了绘图之外,视图还负责处理指向视图的用户事件。
NSView
是一个抽象类,它提供了具体子类实现其自身行为的底层机制。
它提供了可以被覆盖以支持绘图和打印的方法。
NSView
是NSResponder
的子类,因此,它还提供了可以被覆盖以响应用户发起的鼠标和键盘事件的方法。
除了绘制内容和响应用户事件之外,NSView
实例还充当其他视图的容器。
通过将视图嵌套在其他视图中,应用程序创建视图层次结构。
这种视图层次结构为视图如何相对于彼此绘制以及如何将消息从一个视图传递到另一个视图,直到封闭窗口,然后传递到应用程序进行处理提供了一个明确定义的结构。
2、Cocoa提供的视图类
除了NSView
类之外,Cocoa还提供了许多视图子类,这些子类提供了许多应用程序共有的用户交互界面元素。
您的应用程序可以直接使用这些视图类,对它们进行子类化以提供附加功能,或者创建全新的自定义视图类。
其他框架提供了可在Cocoa应用程序中使用的附加视图,包括Web Kit、QuickTime Kit和Quartz Composer。
Cocoa视图子类可以分为几组:容器视图、作为文本系统一部分的视图、用户控件和支持非Quartz图形环境的视图。
容器视图
容器视图增强了其他视图的功能,或者提供了内容的额外视觉分离。
NSBox
类提供了在其功能中相关的子视图组的视觉分离。
NSTabView
类提供了一种将不同视图交换到其内容视图中和之外的方法,以便标签视图的内容区域可以被多组视图重用。
NSSplitView
类允许应用程序水平或垂直堆叠多个视图,由用户可以拖动的分隔符条分隔以调整视图的大小。
这个NSScrollView
类显示视图的一部分内容,这些内容太大而无法在窗口中显示。
它协调许多其他视图和控件来提供它的功能。
滚动视图的滚动器,NSScroller
类的实例,允许用户滚动文档。
滚动视图的内容视图,NSClipView
类的一个实例,实际上管理内容视图在滚动视图内容滚动时的定位和重绘。
NSRulerView
类提供了跟随文档视图滚动的水平和垂直标尺。
所有这些视图协同工作,为应用程序提供滚动。
文本系统
Cocoa文本系统的前端是NSTextView
类。
它绘制由几个底层模型和控制器类管理的文本,并处理用户事件以选择和修改其文本。
文本类在其界面的丰富性和复杂性方面超过了Application Kit中的大多数其他类。
NSTextView
是NSText
的子类,而NSText又是NSView
的子类。
应用程序使用NSTextView
类而不是NSText
。
有关Cocoa文本系统的更多信息,请参阅 Cocoa文本架构指南 。
在视图层次结构中使用时,NSTextView
实例通常设置为滚动视图的文档视图。
对于简单的文本字段样式界面项,Cocoa定义了许多更简单的文本控件,包括NSTextField
类。
控件
通常,应用程序的大部分用户交互界面都是使用控件创建的。
作为NSView
的后代,控件负责绘制其内容和处理用户发起的事件。
控件通常显示特定值并允许用户更改该值。
单个控件提供上下文外的值编辑;将数据放入上下文的是整个用户交互界面。
控件充当称为单元格的轻量级对象的容器。
单元格实际上负责控件提供的大部分视觉表示和事件处理。
这种委托允许单元格在更复杂的用户交互界面对象中重用。
例如,NSTextField
实例使用NSTextFieldCell
类的实例来显示文本和响应用户编辑。
NSTableView
实例也使用相同的NSTextFieldCell
类来允许编辑列中的数据。
这种责任委托是控件和视图之间的关键区别之一。
控件还支持目标-操作范例。
控件向目标对象发送操作消息以响应用户操作。
例如,单击按钮会导致将操作消息发送到按钮的目标对象(如果已设置),或者如果未指定特定目标对象,则会向上发送响应器链。
一些控件子类的名称中实际上有“视图”,这可能会令人困惑。
NSImageView
、NSTableView
和NSOutlineView
类都是NSControl
的子类,尽管它们间接继承自NSView
。
正是因为这些对象依赖单元格来提供大部分功能,所以它们是NSControl
而不是NSView
的子类。
请注意,NSTableView
和NSOutlineView
类不直接提供滚动;它们被设置为滚动视图的文档视图。
非石英图形环境
该NSView
类支持OS X中的标准绘图环境,即Quartz图形环境。
然而,Cocoa还支持几个非Quartz绘图环境,以实现额外的功能和兼容性。
NSOpenGLView
类允许应用程序使用OpenGL渲染内容。
应用程序子类NSOpenGLView
并覆盖drawRect:
方法来显示OpenGL内容。
与其超类不同,NSOpenGLView
类不支持子视图。
QTMovieView
是NSView
的子类,可用于显示、编辑和控制QuickTime电影。
QTMovieView
类是QuickTime Kit框架而不是Cocoa框架的一部分。
三、View Geometry
视图负责窗口矩形区域中的绘图和事件处理。
为了指定责任矩形,您可以使用坐标系将其位置定义为原点和大小。
本章描述视图使用的坐标系,如何指定视图的位置和大小,以及视图的大小如何与其内容交互。
1、视图坐标系
从一开始,Quartz图形环境就被设计为跨输出设备独立于分辨率。
也就是说,1个单位正方形不一定直接对应于1个像素。
当涉及到对分辨率独立性的支持时,Quartz与NSView
的结合自动提供了您需要的大部分支持。
当视图绘制其内容时,分辨率独立性缩放因子会自动管理。
视图的位置使用Quartz图形环境使用的相同坐标系表示。
默认情况下,图形环境原点(0.0,0.0)位于左下角,值指定为在坐标系单位中向上和向右增加的浮点数。
坐标系单位,单位正方形,大小为1.0 x 1.0矩形。
每个视图实例都定义和维护自己的坐标系,所有的绘图都是相对于这个坐标系完成的。
鼠标事件在封闭窗口的坐标系中提供,但很容易转换为视图的坐标系。
视图的坐标系应被视为视图所有内容的基本坐标系,包括其子视图。
2、了解视图的框架和边界
在图形上,视图可以被视为框架画布。
框架在其超级视图中定位视图,定义其大小,并将绘图剪辑到其边缘,而画布托管实际绘图。
框架可以在超级视图中移动、调整大小和旋转,视图的内容也会随之移动。
同样,画布可以移动、拉伸和旋转,视图内容在框架内移动。
视图使用两个矩形跟踪其大小和位置:框架矩形和边界矩形。
框架矩形使用Superview的坐标系定义视图在Superview中的位置和大小。
边界矩形定义绘制视图内容时使用的内部坐标系,包括原点和缩放。
图2-1显示了左侧的框架矩形和右侧的边界矩形之间的关系。
图2-1视图的框架矩形和边界矩形之间的关系
当使用initWithFrame:
方法以编程方式创建视图实例时,将指定视图的框架。
框架矩形作为参数传递。
NSView
方法frame
返回接收方的框架矩形。
初始化视图时,边界矩形设置为起源于(0.0,0.0),边界大小设置为与视图框架相同的大小。
如果应用程序更改视图的边界矩形,它通常会在初始化后立即这样做。
视图的边界矩形由方法bounds
返回。
如果边界矩形的大小不同于框架矩形,则拉伸或压缩内容,以便边界内的所有内容都显示在视图中。
图2-2显示了当框架矩形是边界矩形宽度的两倍时的显示结果。
视图的内容被水平拉伸以填充框架矩形的宽度。
图2-2视图的边界内容被拉伸以适合框架矩形
尽管边界矩形表示视图内容在视图框架中显示的部分,但在某些情况下,仅显示视图内容的一部分——例如,如果框架运行在Superview框架之外。
发生这种情况时,内容将被裁剪,如图2-3左侧所示。
图2-3查看剪辑到其超级视图的内容
根据视图的边界坐标系(图2-3中右侧的矩形),视图的可见矩形反映了实际显示的内容部分。
知道可见矩形是什么通常并不重要,因为显示机制会自动将绘图限制在视图的可见部分。
如果子类必须执行昂贵的预计算来构建其图像,它可以使用visibleRect
方法将其工作限制在实际需要的范围内。
3、变换坐标系
默认情况下,视图的坐标系基于其边界矩形左下角的(0.0,0.0),其单位正方形(1.0 x 1.0矩形的大小)与其超级视图的大小相同,其轴与框架矩形的轴平行。
视图的坐标系可以通过四种不同的方式更改:它可以平移、缩放、翻转或旋转。
要平移或缩放坐标系,您需要更改视图的边界矩形。
更改边界矩形会设置视图执行的所有绘图开始时使用的基本坐标系。
NSView
的具体子类通常会根据需要在其initWithFrame:
方法中或在加载包含视图的nib文件时立即更改边界矩形。
更改边界矩形的方法是setBounds:
,它既可以定位画布,也可以缩放画布。
提供给setBounds:
的矩形原点成为边界矩形的左下角,矩形的大小适合框架矩形,有效地缩放视图绘制的图像。
在图2-4中,图2-1中的边界矩形被移动并变大一倍;结果出现在右侧。
图2-4更改视图的边界
您还可以使用setBoundsOrigin:和setBoundsOrigin:``setBoundsSize:
。
**注意:**一旦应用程序使用任何setBands.setBounds...
方法显式设置视图的边界矩形,更改视图的框架矩形不再自动更改边界矩形。
有关详细信息,请参阅重新定位和调整视图大小。
另一组方法以相对术语平移和缩放坐标系;如果您重复调用它们,它们的效果会累积。
这些方法是translateOriginToPoint:
和scaleUnitSquareToSize:
。
平移视图的边界矩形会随着视图内容的绘制而移动所有子视图。
缩放也会影响子视图的绘制,因为它们的坐标系继承并建立在这些更改的基础上。
视图还可以指定其坐标系被翻转。
翻转坐标系基于原点(0.0,0.0)在其边界矩形的左上角,如图2-5所示。
图2-5翻转视图坐标
当视图的内容自然起源于视图的顶部时,翻转坐标系很有用,并向下流动。
例如,当新文本出现时向上滚动文本并离开屏幕的视图最好使用翻转坐标系来实现。
指定视图子类使用翻转坐标系是通过重写isFlipped
方法来完成的。
NSView
的默认实现返回NO
,这意味着坐标系的原点位于默认边界矩形的左下角,y轴从下到上运行。
当子类重写此方法以返回YES
时,视图机器会自动调整自身以假设左上角是原点。
翻转坐标系会影响翻转视图本身中的所有绘图以及所有直接子视图的框架矩形的位置。
它不会影响这些子视图的坐标系或它们执行的绘图。
也可以在边界矩形(不是边界矩形本身的原点)内围绕原点旋转坐标系。
setBoundsRotation:
方法将坐标系的旋转设置为作为参数传递的角度(以度为单位)。
rotateByAngle:
方法允许您指定相对于坐标系当前旋转的旋转角度。
旋转视图的坐标系也会扩大可见矩形以考虑旋转,因此它以旋转坐标表示,但完全覆盖了框架矩形的可见部分。
这增加了必须绘制但永远不会显示的区域(如图2-6所示的三角形区域)。
图2-6旋转视图的可见矩形
注意: 出于性能原因,不鼓励旋转边界矩形。
旋转坐标系比旋转边界坐标系drawRect:
方法中使用图形运算符。
有关详细信息,请参阅 可可绘图指南 中的坐标系和变换。
视图实例可以在其框架或边界矩形更改时向感兴趣的对象提供通知。
有关详细信息,请参阅使用视图层次结构中的通知。
四、使用视图层次结构
除了它们自己对绘图和事件处理的直接责任之外,视图还充当其他视图的容器,创建视图层次结构。
本章描述视图层次结构、它的好处以及如何在层次结构中使用视图。
1、什么是视图层次结构?
除了负责绘制和处理用户事件之外,视图实例还可以充当容器,封闭其他视图实例。
这些视图链接在一起,创建视图层次结构。
与定义类沿袭的类层次结构不同,视图层次结构定义了视图相对于其他视图的布局。
窗口实例维护对单个顶级视图实例的引用,称为内容视图。
内容视图充当窗口中可见视图层次结构的根。
视图中包含的视图实例称为子视图。
包含视图的父视图称为其超级视图。
虽然视图实例可以有多个子视图,但它只能有一个超级视图。
为了让用户可见视图及其子视图,必须将视图插入窗口的视图层次结构中。
图3-1显示了一个示例应用程序窗口及其视图层次结构。
图3-1视图层次结构
此窗口的视图层次结构包含以下部分。
- 窗口由
NSWindow
实例表示。 - 内容视图充当窗口视图层次结构的根。
- 内容视图包含一个子视图,一个自定义类的实例(看起来非常像
NSBox
,但不是。)。 - 自定义视图实例,它又有两个子视图,一个
NSButton
实例和一个NSTextField
实例。 - 按钮和文本字段的超级视图是
NSBox
对象。
自定义视图容器实际上包含了按钮和文本字段视图。
2、视图层次结构的好处
将视图作为层次结构管理有利于应用程序设计:
- 复杂的视图功能可以通过使用更简单的
NSView
子类来组装,避免单一和复杂的视图类。
例如,图形键盘可能是一个NSView
子类,它对每个键使用NSButton
子视图。 - 每个子视图的坐标系相对于其超级视图的坐标系定位。
NSView
实例位于其超级视图内,因此当移动NSView
实例或转换其坐标系时,其所有子视图都会随之移动和转换。
类似地,缩放NSView
实例会导致所有子视图相对于超级视图缩放其绘图。
由于每个视图都在自己的坐标系内绘图,因此无论它或它的超级视图在屏幕上移动到哪里或如何缩放,其绘图指令都保持不变。 - 视图层次结构为事件处理提供了明确的责任定义。
当视图接收到它没有响应的事件时,该事件通过超级视图向上转发到视图层次结构进行处理。
键窗口的视图层次结构参与应用程序的响应链。 - 视图层次结构还提供了一个定义的结构来管理窗口内容的重绘。
当NSView
实例接收到显示请求时,它会绘制自己,然后依次将绘制责任传递给它的每个子视图。
视图层次结构的每个分支在下一个分支开始之前完成绘制。 - 视图层次结构是动态的。
它可以在应用程序运行时重新配置。
视图实例可以从一个窗口移动到另一个窗口,并首先作为一个超级视图的子视图安装,然后作为另一个超级视图的子视图安装。
3、在视图层次结构中定位视图
丰富的方法选择允许应用程序访问视图的层次结构。
superview
方法返回包含接收器的视图,而subviews
方法返回包含视图直接后代的数组。
如果视图是视图层次结构的根,则在请求其超级视图时返回nil
。
向视图发送window
消息会返回视图所在的窗口,如果视图当前不在窗口的视图层次结构中,则返回nil
。
图3-2说明了视图层次结构中对象的关系,如图3-1所示。
图3-2层次结构中对象之间的关系
其他方法允许您检查视图之间的关系:isDescendantOf:
确认接收器的包含;ancestorSharedWithView:
查找包含接收器和指定为参数的视图实例的公共容器。
例如,假设视图层次结构如图3-2所示,发送viewC
一个isDescendentOf:
消息,contentView
作为参数返回YES
。
发送viewB
的ancestorSharedWithView:
消息,传递viewC
作为参数,返回viewA
。
opaqueAncestor
返回最近的父视图,保证绘制接收器帧中的每个像素(可能是接收器本身)。
4、从层次结构中添加和删除视图
使用initWithFrame:
方法创建视图子类会建立一个NSView
对象的框架矩形,但不会将其插入到窗口的视图层次结构中。
您可以通过向预期的超级视图发送addSubview:
消息来做到这一点,将要插入的视图作为参数传递。
然后根据超级视图解释框架矩形,通过它在视图层次结构中的位置和在超级视图窗口中的位置来正确定位新视图。
视图层次结构中的现有视图可以通过向超级视图发送replaceSubview:with:
消息来替换,将要替换的视图和替换的视图作为参数传递。
一个附加的方法,addSubview:positioned:relativeTo:
,允许您指定视图的顺序。
注意:出于性能原因,Cocoa不会在兄弟视图之间强制剪裁,也不会保证兄弟视图重叠时正确的无效和绘图行为。
如果您希望一个视图在另一个视图前面绘制,您应该将前视图设为后视图的子视图(或后代)。
您可以通过向视图层次结构发送removeFromSuperview
消息将视图从视图层次结构中删除。
removeFromSuperviewWithoutNeedingDisplay
方法类似,从其超级视图中删除接收者,但不会导致超级视图重绘。
当NSView
对象作为另一个视图的子视图添加时,它会自动调用viewWillMoveToSuperview:
和viewWillMoveToWindow:
方法。
您可以覆盖这些方法以允许实例查询其新的超级视图或窗口的相关状态并相应地更新自身。
重要提示:在考虑内存管理时,视图层次结构应被视为任何其他Cocoa集合对象。
当一个项目被添加到集合中时,它会被保留。
当它被删除时,它会被释放。
具体来说,当您使用addSubview:
或addSubview:positioned:relativeTo:
方法将视图作为子视图插入时,它将由接收视图保留。
相反,当您通过向视图的超级视图发送removeFromSuperview
消息从视图层次结构中删除子视图时,视图将被释放。
replaceSubview:with:
方法的作用相同,释放被替换的视图并保留插入的视图。
有关Cocoa内存管理约定的完整讨论,请参阅 高级内存管理编程指南 。
5、重新定位和调整视图大小
重新定位或调整视图大小是一个潜在的复杂操作。
当视图移动或调整大小时,它可能会暴露其超级视图中以前不可见的部分,要求超级视图重新显示。
调整大小也会影响视图子视图的布局。
在任何一种情况下,对视图布局的更改可能会引起其他对象的兴趣,这些对象可能需要收到更改通知。
以下部分探讨了这些领域中的每一个。
以编程方式移动和调整视图大小
创建视图实例后,您可以使用任何框架设置方法以编程方式移动它:setFrame:
、setFrameOrigin:
和setFrameSize:
。
如果视图的边界矩形没有使用setBounds...
方法之一显式设置,视图的边界矩形会自动更新以匹配新的框架大小。
当您更改框架矩形时,子视图的框架矩形的位置和大小通常也需要更改。
如果重新定位的视图在autoresizesSubviews
时返回YES
,它的子视图会按照自动调整子视图大小中的说明自动调整大小。
否则,应用程序有责任手动重新定位和调整子视图的大小。
子视图的自动调整
NSView
提供了一种机制来自动移动和调整子视图的大小,以响应它们的超级视图被移动或调整大小。
在许多情况下,只需为视图配置自动调整大小掩码即可为应用程序提供适当的行为。
对于以编程方式创建的视图,默认情况下自动调整大小是打开的,但您可以使用setAutoresizesSubviews:
方法将其关闭。
Interface Builder允许您使用其大小检查器以图形方式设置视图的自动调整大小掩码,并且在测试模式下,您可以立即检查自动调整大小的效果。
自动调整大小掩码也可以以编程方式设置。
视图的自动调整大小掩码是通过使用按位或运算符组合自动调整大小掩码常量并向视图发送setAutoresizingMask:
消息,将掩码作为参数传递来指定的。
表3-1显示了每个掩码常量以及它如何影响视图的调整大小行为。
自动调整蒙版 | 描述 |
---|---|
NSViewHeightSizable | 如果设置,视图的高度将与超级视图的高度成比例地变化。 否则,视图的高度不会相对于超级视图的高度发生变化。 |
NSViewWidthSizable | 如果设置,视图的宽度将与超级视图的宽度成比例地变化。 否则,视图的宽度不会相对于超级视图的宽度发生变化。 |
NSViewMinXMargin | 如果设置,视图的左边缘将根据超级视图宽度的变化按比例重新定位。 否则,视图的左边缘相对于超级视图的左边缘保持在相同的位置。 |
NSViewMaxXMargin | 如果设置,视图的右边缘将根据超级视图宽度的变化按比例重新定位。 否则,视图的右边缘相对于超级视图保持在相同的位置。 |
NSViewMinYMargin | 如果已设置且超级视图未翻转,则视图的上边缘将根据超级视图高度的变化按比例重新定位。 否则,视图的上边缘相对于超级视图保持在相同的位置。 如果设置并翻转超级视图,视图的下边缘将根据超级视图高度的变化按比例重新定位。 否则,视图的下边缘相对于超级视图保持在相同的位置。 |
NSViewMaxYMargin | 如果已设置且超级视图未翻转,则视图的下边缘将与超级视图高度的变化成比例重新定位。 否则,视图的下边缘相对于超级视图保持在相同的位置。 如果设置并翻转超级视图,视图的上边缘将与超级视图高度的变化成比例重新定位。 否则,视图的上边缘相对于超级视图保持在相同的位置。 |
例如,要将视图保持在其Superview的左下角,您可以指定NSViewMaxXMargin
|NSViewMaxYMargin
。
当沿轴的多个方面变得灵活时,调整大小的量在它们之间均匀分布。
图3-3提供了正常和翻转Superview中常量值位置的图形表示。
图3-3查看自动调整大小的掩码常量
当省略这些常量之一时,视图的布局在该方面是固定的;当掩码中包含常量时,视图的布局在该方面是灵活的。
在掩码中包含常量与在Interface Builder中使用弹簧配置自动调整方面相同。
注意: 如果在Interface Builder中创建视图,并且在视图的检查器中没有设置自动调整大小标志,则setAutoresizesSubviews:
会自动设置为NO
。
在以编程方式修改自动调整大小掩码之前,您需要通过向Superview发送setAutoresizesSubviews:
消息并传递YES
作为参数来显式启用Superview的自动调整大小。
当您关闭视图的自动调整大小时,它的所有后代同样不会受到超级视图更改的影响。
然而,子视图的更改仍然可以向下渗透。
同样,如果子视图没有自动调整大小掩码,它的大小不会改变,因此它的子视图都不会自动调整大小。
子类可以重写resizeSubviewsWithOldSize:
或resizeWithOldSuperviewSize:
来自定义视图的自动调整大小行为。
视图的resizeSubviewsWithOldSize:
方法在其框架大小发生变化时由视图自动调用。
然后,此方法只需向每个子视图发送resizeWithOldSuperviewSize:
消息。
每个子视图将旧框架大小与新大小进行比较,并根据其自动调整大小掩码调整其位置和大小。
重要提示:对于自动调整大小有几个注意事项。
要使自动调整大小正常工作,被自动调整大小的子视图必须完全位于其超级视图的框架内。
自动调整大小在已旋转的视图中根本不起作用。
已旋转的子视图可以在未旋转的超级视图中自动调整大小,但它们的后代不会自动调整大小。
通知
除了调整子视图的大小之外,默认情况下,NSView
实例在其边界或框架矩形发生变化时向感兴趣的观察者广播通知。
通知名称分别是NSViewFrameDidChangeNotification
和NSViewBoundsDidChangeNotification
。
基于子视图布局的NSView
实例应该注册为这些子视图的观察者,并在它们移动或调整大小时更新自己。
NSScrollView
和NSClipView
实例以这种方式协作来调整滚动视图的滚动。
默认情况下,框架和边界矩形更改都会为视图实例发送。
您可以使用setPostsFrameChangedNotifications:
和setPostsBoundsChangedNotifications:
阻止NSView
实例提供通知,并将NO
作为参数传递。
如果您的应用程序执行复杂的视图布局,在布局之前关闭更改通知,然后在完成后恢复它们可能会提高性能。
与所有性能调整一样,最好首先对应用程序进行采样,以确定更改通知是否对性能产生负面影响。
6、隐藏视图
您可以使用NSView
方法setHidden:隐藏和取消隐藏(即显示)Cocoa应用程序的视图setHidden:
此方法采用布尔参数:YES
(隐藏接收视图)或NO
(显示接收视图)。
当您使用setHidden:
方法隐藏视图时,它会保留在视图层次结构中,即使它从窗口中消失并且不接收输入事件。
隐藏视图保留在其Superview的子视图列表中并参与自动调整大小。
如果标记为隐藏的视图包含子视图,它们及其视图后代也会被隐藏。
当您隐藏视图时,应用程序工具包还会禁用与视图关联的任何光标矩形、工具提示矩形或跟踪矩形。
隐藏作为窗口当前第一响应者的视图会导致视图的下一个有效键视图(nextValidKeyView
)成为新的第一响应者。
隐藏视图保留在nextKeyView
视图链中,它以前是其中的一部分,但在键盘导航期间被忽略。
您可以通过发送视图的isHidden
或isHiddenOrHasHiddenAncestor
(均由NSView
定义)来查询视图的隐藏状态。
前一种方法在视图被显式标记为隐藏时返回YES
,并使用setHidden:
消息。
后者在视图被显式标记为隐藏时和由于祖先视图被标记为隐藏而隐藏时返回YES
。
注意: 在OS X v10.3之前,要隐藏视图,您必须将其从其Superview中删除,并保留它以供以后重新插入视图层次结构。
因为这种方法将视图与其层次结构分开,所以它有一些限制。
如果调整了Superview的大小,则重新插入时删除的视图不会自动调整为新大小。
此外,如果删除的视图是键视图链的一部分(每个视图都响应nextKeyView
),则必须在重新插入时重新集成到链中。
以编程方式管理这些问题是应用程序的责任。
7、在视图层次结构中转换坐标
在不同的时候,特别是在处理事件时,应用程序需要在同一窗口中将一个NSView
实例的坐标系中的矩形或点转换为另一个(通常是超级视图或子视图)。
NSView
类定义了六个方法,用于在任一方向转换矩形、点和大小:
从指定视图转换为接收器 | 从接收器转换到指定视图 |
---|---|
convertPoint:fromView: | convertPoint:toView: |
convertRect:fromView: | convertRect:toView: |
convertSize:fromView: | convertSize:toView: |
方法将convert...:fromView:
从作为第二个参数传递的视图的坐标系转换到接收方的坐标系。
如果nil
作为视图传递,则假定值在窗口的基(窗口的坐标空间)坐标系中,并将其转换到接收方的坐标系。
convertPoint:fromView:
方法通常用于将鼠标事件坐标(由NSEvent
提供的相对于窗口的坐标)转换为接收视图,如例3-1所示。
例3-1 使用 convertPoint:fromView:
转换事件位置
-(void)mouseDown:(NSEvent *)event
{NSPoint clickLocation;// convert the click location into the view coordsclickLocation = [self convertPoint:[event locationInWindow]fromView:nil];// do something with the click location
}
方法进行convert..:toView:
将接收方坐标系中的值转换为作为参数传递的视图坐标系。
如果视图参数nil
,则将值转换为接收方窗口的基本坐标系。
为了与屏幕坐标系相互转换,NSWindow
定义了convertBaseToScreen:
方法和convertScreenToBase:
方法。
使用NSView
转换方法和这些方法,您只需两条消息即可在视图坐标系和屏幕坐标系之间转换几何结构,如例3-2所示。
示例3-2将视图位置转换为屏幕位置
NSPoint pointInWindowCoordinates;
NSPoint pointInScreenCoords;pointInWindowCoordinates=[self convertPoint:viewLocation toView:nil];
pointInScreenCoords=[[self window] convertBaseToScreen:pointInWindowCoordinates];
当两个视图都没有旋转或只处理点时,转换很简单。
在具有不同旋转的视图之间转换矩形或大小时,必须以合理的方式改变几何结构。
在转换矩形时,NSView
类假设您希望保证原始屏幕区域的覆盖。
为此,转换后的矩形被放大,以便当位于适当的视图中时,它完全覆盖原始矩形。
图3-4显示了rotatedView
对象坐标系中的矩形到其超级视图outerView
的转换。
图3-4在旋转视图中转换值
在转换大小时,NSView
只是将其视为需要从一个视图转换到另一个视图的增量偏移量(0.0,0.0)。
尽管偏移距离保持不变,但沿两个轴的平衡会根据旋转而变化。
值得注意的是,在转换大小时,Cocoa将始终返回由正数组成的大小。
8、转换视图坐标到和从基空间
在Leopard中,NSView
提供了一组新方法,在执行视图内容的像素对齐时应该使用这些方法。
它们提供了在与绘制视图的后备存储像素对齐的“基本”坐标空间之间转换几何的方法:
- (NSRect)convertRectToBase:(NSRect)aRect;
- (NSPoint)convertPointToBase:(NSPoint)aPoint;
- (NSSize)convertSizeToBase:(NSSize)aSize;
- (NSRect)convertRectFromBase:(NSRect)aRect;
- (NSPoint)convertPointFromBase:(NSPoint)aPoint;
- (NSSize)convertSizeFromBase:(NSSize)aSize;
对于传统的视图渲染,其中视图层次结构被绘制成窗口背景存储,这个“基础”空间与窗口的坐标系相同,使用这些新方法的结果与使用表3-2中讨论的现有转换方法将几何图形转换为视图nil
相同。
然而,渲染到核心动画层中的视图具有它们自己的单独背衬存储,这些背衬存储可以对齐,使得窗口空间不一定是执行像素对齐计算的适当坐标系。
这些新的坐标转换方法提供了一种从特定后备存储配置的细节中抽象视图内容绘图代码的方法,并且始终实现正确的像素对齐,而不必针对层支持与传统视图渲染模式进行特殊情况。
无论视图内容如何被缓冲的底层细节如何,转换为基础空间都会将其置于本机设备坐标系中,其中集成坐标会产生几何图形的像素对齐。
当在除1.0以外的用户交互界面比例因子下使用图层支持视图时,请注意视图的尺寸及其相应的支持层的尺寸将根据比例因子而变化,因为CALayer
边界总是以像素表示,而NSView
尺寸仍然以点表示。
大多数图层支持视图的客户端不需要直接在图层空间中执行操作,但对于那些需要直接在图层空间中执行操作的客户端,在适当的时候使用前面的方法在视图空间和图层(“基础”)空间之间转换几何量非常重要。
9、查看标签
该NSView
类定义了一些方法,这些方法允许您使用整数标签标记单个视图对象,并根据这些标签搜索视图层次结构。
subviews
方法返回的第一个子视图开始,接收方的子视图将被深度优先搜索。
而NSView
方法tag
总是返回–1
。
子类可以覆盖此方法以返回不同的值。
子类通常实现setTag:
方法,该方法将标记值存储在实例变量中,从而允许在单个视图的基础上设置标记。
几个Application Kit类,包括NSControl
子类,就是这样做的。
viewWithTag:
方法使用深度优先搜索,在接收者的视图层次结构中从后到前,查找具有给定标记的子视图,如果找到则返回它。
五、创建自定义视图
这个NSView
类主要作为一个抽象超类;通常您创建它的子类的实例,而不是NSView
本身的实例。
NSView
提供了在屏幕上显示内容和处理鼠标和键盘事件的一般机制,但是它的实例缺乏实际绘制任何东西的能力。
如果您的应用程序需要以特定的方式显示内容或处理鼠标和键盘事件,您需要创建NSView
的自定义子类。
为了提供一个具体的例子,本章描述了NSView
的子类DraggableItemView
的实现。
DraggableItemView
类显示一个简单的项目,并允许用户在视图中拖动它。
该视图还支持通过按下箭头键和设置项目的颜色来移动项目。
它为项目的位置、其颜色和视图的背景颜色提供键值编码合规性。
该类说明了以下视图编程任务:
- 分配和解除分配视图。
- 绘制视图内容。
- 标记视图的某些部分以响应值更改进行更新。
- 响应用户启动的鼠标事件。
- 当鼠标悬停在可拖动项目上时更新光标。
- 响应用户发起的按键事件。
- 实现
NSResponder
动作方法。 - 为其可设置属性提供符合键值编码的访问器。
可通过Apple Developer Connection获得 DragItem网址 源代码。
1、分配视图
应用程序使用NSView
类的指定初始化程序initWithFrame:
创建视图对象的新实例。
子类可以指定另一个方法作为其指定的初始化程序,但是initWithFrame:
方法必须提供所需的基本功能。
例如,initWithFrame:``NSTextView
实现创建与NSTextView
实例关联的容器对象的整个集合,而指定的初始化程序initWithFrame:textContainer:
期望显式提供底层容器对象。
initWithFrame:
方法创建集合,然后调用initWithFrame:textContainer:
。
您的自定义类应该采用相同的方法。
DraggableItemViewDraggableItemView
类重写initWithFrame:
并将可拖动项的公开属性设置为默认值,如例4-1所示。
例4-1 DraggableItemView
的DraggableItemView实现initWithFrame:
- (id)initWithFrame:(NSRect)frame {self = [super initWithFrame:frame];if (self) {// setup the initial properties of the// draggable item[self setItemPropertiesToDefault:self];}return self;
}
用于初始化项目颜色、背景颜色和可拖动项目的位置的代码被分解到一个单独的方法中。
这允许将项目的属性重置为它们的默认值,如后面所示。
例4-2中的实现简单地调用属性的访问器方法,提供默认值。
例4-2 DraggableItemView
的实现setItemPropertiesToDefault:
- (void)setItemPropertiesToDefault:sender
{[self setLocation:NSMakePoint(0.0,0.0)];[self setItemColor:[NSColor redColor]];[self setBackgroundColor:[NSColor whiteColor]];
}
初始化在Interface Builder中创建的视图实例
在Interface Builder中创建的视图实例在加载笔尖文件时不会调用initWithFrame:
,这通常会导致混淆。
请记住,Interface Builder在保存笔尖文件时会存档一个对象,因此视图实例已经创建,并且initWithFrame:
已经被调用。
当awakeFromNib
创建视图时,awakeFromNib方法提供了初始化视图的机会。
当加载包含视图对象的nib文件时,当所有对象都未归档时,每个视图实例都会收到一条awakeFromNib
消息。
这为对象提供了初始化任何未与Interface Builder中的对象一起归档的属性的机会。
DraggableItemView
类非常简单,并且没有实现awakeFromNib
。
在Interface Builder中创建视图实例时,initWithFrame:
有两个异常。
了解这些异常以确保视图正确初始化非常重要。
如果您还没有为自定义视图创建Interface Builder调色板,您可以使用两种技术在Interface Builder中创建子类的实例。
第一种是使用Interface Builder容器调色板中的自定义视图代理项。
此视图是自定义视图的替身,允许您相对于其他视图定位和调整视图的大小。
然后,您可以使用检查器指定视图所代表的NSView
的子类。
当应用程序加载nib文件时,自定义视图代理会创建指定视图子类的新实例,并使用initWithFrame:
方法对其进行初始化,并根据需要传递任何自动调整大小的标志。
然后,视图实例会收到一条awakeFromNib
消息。
当您的自定义子类继承自Interface Builder提供直接支持的视图时,第二种技术适用。
要利用Interface Builder的内置支持,请创建Interface Builder直接支持的视图实例,然后使用检查器将类名更改为您的自定义子类。
例如,您可以在Interface Builder中创建一个NSScrollView
实例,并指定应该使用自定义子类(MyScrollView
),再次使用检查器。
在这种情况下,当应用程序加载nib文件时,视图实例已经创建,并且MyScrollView
的initWithFrame:
的实现永远不会被调用。
MyScrollView
实例会收到一条awakeFromNib
消息,并可以相应地配置自己。
2、绘图查看内容
Cocoa不是在确定需要绘图时立即绘图,而是使用延迟绘图机制。
应用程序通常会将视图或视图的一部分标记为需要更新。
在事件循环结束时或响应显式显示请求时,视图机制会锁定视图并调用视图的drawRect:
方法来重绘视图。
通过以这种方式合并更新请求,应用程序可以减少冗余绘图,从而提高性能。
如果您需要强制立即绘制视图,请发送视图之一的display...
消息,由NSView
和NSWindow
声明。
您还可以自己锁定视图的焦点,绘制某些内容,然后解锁焦点。
但是,通过setNeedsDisplay:
或setNeedsDisplayInRect:
方法发布延迟绘图请求是首选方法,因为它更高效。
除了绘制到屏幕之外,视图还负责在打印时提供内容。
与显示到屏幕一样,Application Kit锁专注于视图并调用视图的drawRect:
方法。
当视图绘制时,它可以确定它是绘制到屏幕还是其他设备,并适当地自定义其输出。
视图还可以通过添加页眉和页脚以及自定义分页来自定义其打印输出。
有关Cocoa打印架构和视图的更多信息,请参阅 Mac版打印编程指南 。
实现Drarect:方法
为了让NSView
的具体子类显示任何类型的内容,它只需要实现drawRect:
方法。
在显示过程中调用此方法以生成由窗口服务器呈现为光栅图像的代码。
drawRect:
接受一个参数,一个描述需要在接收者自己的坐标系中绘制的区域的矩形。
DraggableItemView的DraggableItemView``drawRect:
用指定的背景颜色填充视图的边界,然后计算可拖动项的边界(例4-3)并用指定的颜色填充。
例4-3 DraggableItemView
的实现calculatedItemBounds:
- (NSRect)calculatedItemBounds
{NSRect calculatedRect;// calculate the bounds of the draggable item// relative to the locationcalculatedRect.origin=location;// the example assumes that the width and height// are fixed valuescalculatedRect.size.width=60.0;calculatedRect.size.height=20.0;return calculatedRect;
}
完整的drawRect:
实现如例4-4所示。
例4-4 DraggableItemView
的实现drawRect:
- (void)drawRect:(NSRect)rect
{// erase the background by drawing white[[NSColor whiteColor] set];[NSBezierPath fillRect:rect];// set the current color for the draggable item[[self itemColor] set];// draw the draggable item[NSBezierPath fillRect:[self calculatedItemBounds]];
}
向窗口服务器发送绘图指令和数据是有代价的,最好尽可能地将代价降到最低。
您可以通过测试特定图形形状是否与要求绘制的矩形相交drawRect:
请参阅优化视图绘图以获取更多信息,以及其他性能建议。
注: OS X v10.4.3 之前的 NSView
类的实现可能会丢弃在子类的 drawRect:
实现中标记为需要显示的任何矩形。
为获得最大兼容性,当在 drawRect:
方法中标记需要显示的区域时,最好使用 NSObject
实例方法 performSelector:withObject:afterDelay:
调用视图的 setNeedsDisplayInRect:
方法。
将视图标记为需要显示
让视图重新显示的最常见方法是告诉它其图像无效。
每次通过事件循环时,所有需要重新显示的视图都会这样做。
NSView 定义了两种将视图图像标记为无效的方法: setNeedsDisplay:会使视图的整个边界矩形无效,而
setNeedsDisplayInRect:则会使视图的一部分无效。 视图的自动显示由其窗口控制;可以使用
NSWindow``setAutodisplay:` 方法关闭该行为。
不过,您应该很少需要这样做;自动显示机制非常适合大多数类型的更新和重新显示。
自动显示机制调用实际执行显示工作的各种方法。
您还可以使用这些方法强制视图在必要时立即重新显示自身。
display
和displayRect:
是上述方法的对应物;无论是否需要,都可以使接收器重新显示自身。
另外两个方法,displayIfNeeded
和displayIfNeededInRect:
,如果接收器被上述方法标记为无效,则在接收器中重新显示无效的矩形。
实际绘制的矩形保证至少是那些被标记为无效的矩形,但视图可能会将它们合并成更大的矩形,以保存多次调用drawRect:
.
如果要在强制无条件显示时从绘图中排除背景视图,可以使用显式省略备份到不透明祖先的NSView
方法。
这些方法是displayRectIgnoringOpacity:
、displayIfNeededIgnoringOpacity
和displayIfNeededInRectIgnoringOpacity:
.
在DraggableItemView
示例中,setNeedsDisplayInRect:
在显式设置可拖动项目的位置、偏移位置以及更改项目颜色时调用。
设置背景颜色时,整个视图被标记为需要显示。
从设计的角度来看,特别是考虑到Model-View-Controller模式,最好确保对display...
方法的调用是由视图本身、它的超级视图或子视图生成的,而不是控制器或模型对象。
最好通知视图一个模型值即将改变,改变模型值,然后通知视图改变已经发生。
这允许视图在改变前后使适当的矩形无效。
键值观察及其改变通知设计就是为这种用途量身定制的。
有关更多信息,请参阅 键值观察编程指南 。
查看不透明度
该display...
方法必须在需要显示的视图后面找到一个不透明的背景,并从那里开始向前绘制。
display...
方法向上搜索视图层次结构以找到第一个响应isOpaque
消息的YES
视图,并带来无效的矩形。
如果视图实例可以保证它将使用不透明的颜色填充其边界内的所有像素,它应该实现方法isOpaque
,返回YES
。
isOpaque
的NSView
实现返回NO
。
如果视图内容中的所有像素都将不透明地绘制,子类应该覆盖此方法以返回YES
。
在绘图过程中调用isOpaque
方法,并且可能在绘图过程中多次调用给定视图。
子类在实现isOpaque
方法时应避免计算密集型计算。
简单的测试——例如确定背景颜色是否像DraggableItemView
那样不透明——是可以接受的。
DraggableItemView
实现如例4-5所示。
**例4-5 ** isOpaque
的DraggableItemView
- (BOOL)isOpaque
{// If the background color is opaque, return YES// otherwise, return NOreturn [[self backgroundColor] alphaComponent] >= 1.0 ? YES : NO;
}
3、响应用户事件和操作
视图通常是大多数事件和操作消息的接收者。
NSView
子类覆盖NSResponder
类声明的适当事件处理方法。
当自定义视图实例的实例是第一个响应者时,它会在其他对象之前接收发布的事件消息。
同样,通过实现通常由其他用户交互界面对象(如菜单项)发送的操作方法,当自定义视图实例是第一个响应者时,它会接收这些消息。
请参阅 Cocoa事件处理指南 ,了解有关事件处理和响应者链的完整讨论。
事件消息从第一个响应者向上传递到响应者链。
对于所有视图,除了窗口的内容视图之外,视图的下一个响应者是它的超级视图。
当视图实例插入视图层次结构时,下一个响应者会自动设置。
您永远不应该将setNextResponder:
消息直接发送到视图对象。
如果您需要将对象添加到响应者链,您应该将它们添加到窗口响应者链的顶部——如果它没有委托,则通过子类化NSWindow
本身,如果它有委托类,则为委托类。
作为处理显示的类,NSView
通常是鼠标和键盘事件的接收者。
鼠标事件从发生单击的视图开始,向上传递到响应器链。
键盘事件从第一个响应器开始,向上传递到响应器链。
成为第一反应者
作为第一个响应者的视图在其他对象之前接收关键事件和操作消息。
视图可以通过覆盖acceptsFirstResponder
消息并返回YES
来宣布它们可以成为第一个响应者。
默认的NSResponder
实现返回NO
。
如果视图不是第一个响应者,它只接收鼠标按下消息。
因为DraggableItemView
对象响应基本的按键事件,以及响应按下箭头键而生成的NSResponder
操作消息,它返回YES
foracceptsFirstResponder
,如例4-6所示。
例4-6 DraggableItemView
的acceptsFirstResponder
- (BOOL)acceptsFirstResponder
{return YES;
}
当窗口尝试使视图成为第一响应者时,视图会收到一条becomeFirstResponder
消息。
此方法的默认实现始终返回YES
。
类似地,当视图将辞去第一响应者身份时,它会收到一条resignFirstResponder
消息。
要辞去第一响应者身份,resignFirstResponder
返回YES
。
视图拒绝辞去第一响应者身份可能有正当理由,例如如果操作不完整。
如果一个视图成为第一个响应者,专门接受关键事件或NSResponder
操作,它应该通过绘制焦点环来反映这一点。
焦点环通知用户哪个对象是关键事件的当前第一个响应者。
可以成为第一响应者并处理关键事件的视图通常参与窗口的键视图循环。
键视图循环允许用户通过按Tab或Shift-Tab键在窗口中的视图之间切换。
NSView
提供了许多方法来设置和获取键视图循环中的视图。
大多数情况下,键视图循环顺序是在Interface Builder中通过将一个视图连接到另一个视图的nextKeyView
出口来设置的。
处理鼠标单击和拖动事件
自定义视图子类可以以任何适当的方式解释鼠标事件。
按钮类型的视图发送目标操作消息,而单击绘图视图可能会选择图形。
传递给视图的鼠标事件有四种基本类型:鼠标向下、鼠标拖动、鼠标向上和鼠标移动。
默认情况下,如果视图不在最前面的窗口(称为键窗口)中,则不会接收鼠标按下事件。
通过覆盖acceptsFirstMouse:
方法并返回YES
,窗口立即成为键窗口并在鼠标按下时发生作用。
当用户在视图中按下鼠标按钮时,将发送鼠标向下事件。
如果包含视图的窗口不是键窗口,则该窗口将成为键窗口,并丢弃鼠标向下事件。
应用程序可以更改此行为,从而导致初始鼠标向下产生窗口键,并通过覆盖acceptsFirstMouse:
方法并返回YES
来传递给适当的视图。
该窗口使用NSView
方法hitTest:
确定视图层次结构中的哪个视图发送鼠标按下事件。
一旦找到正确的视图,就会发送一个mouseDown:
事件。
对于使用鼠标右键进行的操作,以及使用rightMouseDown:
和otherMouseDown:
方法进行的其他鼠标按钮操作,都会发布相应的鼠标按下事件。
通过发送传递给mouseDown:
方法的事件对象和locationInWindow
消息来返回鼠标事件在接收方窗口坐标系中的位置。
要将点转换为视图坐标系,请使用方法convertPoint:fromView:
将nil
作为视图参数传递。
例4-7说明了DraggableItemView
子类对mouseDown:
方法的实现。
例4-7 mouseDown的 DraggableItemView
实现mouseDown:
-(void)mouseDown:(NSEvent *)event
{NSPoint clickLocation;BOOL itemHit=NO;// convert the mouse-down location into the view coordsclickLocation = [self convertPoint:[event locationInWindow]fromView:nil];// did the mouse-down occur in the item?itemHit = [self isPointInItem:clickLocation];// Yes it did, note that we're starting to dragif (itemHit) {// flag the instance variable that indicates// a drag was actually starteddragging=YES;// store the starting mouse-down location;lastDragLocation=clickLocation;// set the cursor to the closed hand cursor// for the duration of the drag[[NSCursor closedHandCursor] push];}
}
此实现获取鼠标朝下的位置并将其转换为视图的坐标系。
由于拖动项目子类仅允许用户在可拖动矩形中发生鼠标朝下事件时拖动项目,因此该实现调用isPointInItem:
方法,如例4-8所示,以测试鼠标朝下是否在可拖动项目的范围内。
如果是,则拖动实例变量设置为YES
以注意视图不应忽略mouseDragged:
事件。
为了更好地向用户反映拖动正在进行中,光标设置为闭合的手光标。
例4-8 isPointInItem的 DraggableItemView
实现isPointInItem:
- (BOOL)isPointInItem:(NSPoint)testPoint
{BOOL itemHit=NO;// test first if we're in the rough boundsitemHit = NSPointInRect(testPoint,[self calculatedItemBounds]);// yes, lets further refine the testingif (itemHit) {// if this was a non-rectangular shape, you would refine// the hit testing here}return itemHit;
}
请注意,例4-7中的mouseDown:
实现不调用超级实现。
NSView
类处理鼠标事件的默认实现继承自NSResponder
,并将事件向上传递到响应器链进行处理,完全绕过相关视图。
通常,自定义NSView
子类不应调用任何鼠标事件方法的超级实现。
视图经常需要在收到鼠标向下事件后跟踪鼠标的拖动。
当鼠标按钮被按住并且鼠标移动时,视图接收到mouseDragged:
消息。
mouseDragged:
的DraggableItemView
实现如例4-9所示。
例4-9 DraggableItemView
实现的mouseDragged
:
-(void)mouseDragged:(NSEvent *)event
{if (dragging) {NSPoint newDragLocation=[self convertPoint:[event locationInWindow]fromView:nil];// offset the item by the change in mouse movement// in the event[self offsetLocationByX:(newDragLocation.x-lastDragLocation.x)andY:(newDragLocation.y-lastDragLocation.y)];// save the new drag location for the next drag eventlastDragLocation=newDragLocation;// support automatic scrolling during a drag// by calling NSView's autoscroll: method[self autoscroll:event];}
}
视图实例接收到视图的所有鼠标拖动通知,但子类只对可拖动项本身中由鼠标向下事件启动的拖动事件感兴趣。
通过测试实例变量dragging
,视图可以确定是否应该对拖动采取行动。
如果是这样,则可拖动项被自上次鼠标事件以来鼠标位置的变化所抵消,该变化由类的实例变量lastDragLocation
跟踪。
**注意:**如例4-9所示的mouseDragged:
实现调用NSView
方法autoscroll:
,将事件作为参数传递。
如果DraggableItemView
实例嵌入在滚动视图中,当鼠标被拖出视图时,滚动视图会自动滚动。
当视图不包含在滚动视图中时,它什么也不做。
有关详细信息,请参阅滚动视图编程指南。
由mouseDragged:
方法调用的offsetLocationByX:andY:
方法如例4-10所示。
它将可拖动项目的区域标记为需要在请求的数量改变项目位置之前和之后显示。
如果视图在发送isFlipped
消息时返回YES
,则垂直方向的偏移量乘以-1以对应翻转的视图坐标。
在DraggableItemView
实现中,代码被分解为它自己的方法,因为它稍后会被重用。
例4-10 DraggableItemView
的offsetLocationByX:andY:
实现
- (void)offsetLocationByX:(float)x andY:(float)y
{// tell the display to redraw the old rect[self setNeedsDisplayInRect:[self calculatedItemBounds]];// since the offset can be generated by both mouse moves// and moveUp:, moveDown:, etc.. actions, we'll invert// the deltaY amount based on if the view is flipped or// not.int invertDeltaY = [self isFlipped] ? -1: 1;location.x=location.x+x;location.y=location.y+y*invertDeltaY;// invalidate the new rect location so that it'll// be redrawn[self setNeedsDisplayInRect:[self calculatedItemBounds]];}
最后,当释放鼠标按钮时,视图接收到mouseUp:
消息。
例4-11中显示的DraggableItemView
实现更新拖动实例变量以指示拖动操作已经完成并重置光标。
invalidateCursorRectsForView:
消息将在本节末尾讨论。
例4-11 DraggableItemView
实现mouseUp:
-(void)mouseUp:(NSEvent *)event
{dragging=NO;// finished dragging, restore the cursor[NSCursor pop];// the item has moved, we need to reset our cursor// rectangle[[self window] invalidateCursorRectsForView:self];
}
有时会使用第二种处理鼠标拖动的技术,通常称为“短路”事件循环。
应用程序可以连续实现mouseDown:
方法和循环,收集鼠标拖动的事件,直到接收到鼠标向上事件。
与事件掩码不匹配的事件保留在事件队列中,并在循环存在时进行处理。
如果DraggableItemView
类使用这种技术实现相同的行为,它只会实现mouseDown:
方法,消除mouseDragged:
和mouseUp:
方法实现。
例4-12中显示的mouseDown:
实现使用“short circuiting”技术。
例4-12 可替换mouseDown:
实现
-(void)mouseDown:(NSEvent *)event
{BOOL loop = YES;NSPoint clickLocation;// convert the initial mouse-down location into the view coordsclickLocation = [self convertPoint:[event locationInWindow]fromView:nil];// did the mouse-down occur in the draggable item?if ([self isPointInItem:clickLocation]) {// we're dragging, so let's set the cursor// to the closed hand[[NSCursor closedHandCursor] push];NSPoint newDragLocation;// the tight event loop pattern doesn't require the use// of any instance variables, so we'll use a local// variable localLastDragLocation instead.NSPoint localLastDragLocation;// save the starting location as the first relative pointlocalLastDragLocation=clickLocation;while (loop) {// get the next event that is a mouse-up or mouse-dragged eventNSEvent *localEvent;localEvent= [[self window] nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask];switch ([localEvent type]) {case NSLeftMouseDragged:// convert the new drag location into the view coordsnewDragLocation = [self convertPoint:[localEvent locationInWindow]fromView:nil];// offset the item and update the display[self offsetLocationByX:(newDragLocation.x-localLastDragLocation.x)andY:(newDragLocation.y-localLastDragLocation.y)];// update the relative drag location;localLastDragLocation=newDragLocation;// support automatic scrolling during a drag// by calling NSView's autoscroll: method[self autoscroll:localEvent];break;case NSLeftMouseUp:// mouse up has been detected,// we can exit the looploop = NO;// finished dragging, restore the cursor[NSCursor pop];// the rectangle has moved, we need to reset our cursor// rectangle[[self window] invalidateCursorRectsForView:self];break;default:// Ignore any other kind of event.break;}}};return;
}
注意:使用这种模式缩短事件循环既有优点也有缺点。
当拖动进行时,紧密的事件循环可以更好地控制其他事件如何与您的应用程序交互。
这种方法通常需要更少的代码,所有拖动变量都是方法的本地变量。
子类在不重新实现所有拖动代码的情况下覆盖拖动行为更加困难。
此外,在紧密的事件循环期间,计时器不会按预期触发,应用程序的主线程无法处理任何其他应用程序请求。
在编写事件驱动的应用程序时,实现单独的mouseDown:
、mouseDragged:
和mouseUp:
方法通常是更好的设计选择。
每个方法都有明确定义的范围,这通常会导致更清晰的代码。
这种方法还使子类更容易覆盖处理鼠标向下、鼠标拖动和鼠标向上事件的行为。
然而,这种技术可能需要更多的代码和实例变量。
跟踪鼠标移动
除了鼠标向下、鼠标拖动和鼠标向上事件之外,视图还可以接收鼠标移动事件。
鼠标移动事件允许视图在光标位于视图上方时跟踪光标的位置。
默认情况下,视图不会接收鼠标移动事件,因为它们可能经常发生,从而堵塞事件队列。
鼠标移动事件由包含视图的NSWindow
实例发起。
为了让视图接收鼠标移动事件,它必须通过向其窗口发送setAcceptsMouseMovedEvents:
消息来显式请求它们,并传递YES
作为参数。
启用后,只要光标位于视图中,视图就会接收mouseMoved:
事件。
不幸的是,使用此技术无法为单个视图启用鼠标移动事件。
NSViewNSView
允许视图实例注册跟踪矩形。
将对象注册为跟踪矩形的所有者会导致所有者在光标进入并存在该矩形时接收mouseEntered:
和mouseExited:
消息。
应用程序使用NSView
方法addTrackingRect:owner:userData:assumeInside:
注册跟踪矩形。
跟踪矩形在视图的坐标系中提供,所有者是将接收mouseEntered:
和mouseExited:
消息的对象。
userData
参数是任何将作为userData
对象提供给mouseEntered:
和mouseExited:
方法的NSEvent
<–atag–20/>对象中的任意对象。
assumeInside
参数指示光标最初是否应该假设在跟踪矩形内。
该方法返回标识跟踪矩形的跟踪标记,跟踪标记用于取消注册所有者以使用方法removeTrackingRect:
跟踪通知。
应用程序只能为当前显示在窗口中的视图注册跟踪矩形。
尽管跟踪矩形是由视图创建和使用的,但它们实际上是由视图的窗口维护的。
因此,当视图移动或调整大小时,跟踪矩形不会自动移动或调整大小。
当视图的框架发生变化或作为子视图插入时,子类有责任删除和重新注册跟踪矩形。
这通常是通过覆盖NSView
方法resetCursorRects
来完成的。
NSView
还提供了一些方法来支持跟踪矩形的常见用法;在鼠标进入矩形时更改光标。
addCursorRect:cursor:
方法允许您使用视图的坐标系注册矩形,并指定当鼠标在该矩形上时应显示的光标。
光标矩形是不稳定的。
当视图的窗口调整大小、视图的框架或边界发生变化、视图在层次结构中移动或视图滚动时,视图会收到resetCursorRects
消息。
子类应该覆盖resetCursorRects
并在该方法中注册任何所需的光标矩形和跟踪矩形。
removeCursorRect:cursor:
方法允许您显式删除与提供的参数完全匹配的光标矩形。
discardCursorRects
方法删除视图的所有光标矩形。
通过DraggableItemView
,DraggableItemView提供了光标位于可拖动项目上方的视觉反馈。
resetCursorRects
的实现,如例4-13所示,丢弃了所有当前的光标矩形,并为可拖动项目的边界添加了一个新的光标矩形。
例4-13 DraggableItemView
的resetCursorRects
实现
-(void)resetCursorRects
{// remove the existing cursor rects[self discardCursorRects];// add the draggable item's bounds as a cursor rect// clip the draggable item's bounds to the view's visible rectNSRect clippedItemBounds = NSIntersectionRect([self visibleRect], [self calculatedItemBounds]);// if the clipped item bounds isn't empty then the item is at least partially// in the visible rect. Register the clipped item boundsif (!NSIsEmptyRect(clippedItemBounds)) {[self addCursorRect:clippedItemBounds cursor:[NSCursor openHandCursor]];}
}
为视图添加光标矩形不会自动将光标矩形限制在视图的可见区域。
您必须自己找到建议的光标矩形与视图可见矩形的交集。
如果生成的矩形不为空,则应将其作为第一个参数传递给addCursorRect:cursor:
方法。
您永远不应该直接调用resetCursorRects
;而是向视图的窗口发送一个invalidateCursorRectsForView:
消息,传递适当的视图。
每次可拖动项移动时,DraggableItemView
对象都需要重置其光标矩形。
mouseUp:
实现如例4-11所示,向视图的窗口发送一个invalidateCursorRectsForView:
消息,将视图本身作为参数传递。
同样,在mouseDown:
版本中,这会缩短事件循环,如例4-12所示,当检测到鼠标向上事件时发送invalidateCursorRectsForView:
消息。
在视图中处理关键事件
如成为第一响应者中所述,视图仅在覆盖acceptsFirstResponder
并返回YES
时才接收按键事件。
因为DraggableItemView
对象响应用户按键,所以类会覆盖此方法并返回YES
。
有两个与NSResponder
相关的方法由keyDown:
和performKeyEquivalent:
方法提供。
NSResponder
还声明了一些由key-down事件触发的响应器操作。
这些操作将特定的击键映射到常见的操作。
通过实现适当的操作方法,您可以绕过覆盖更复杂的keyDown:
方法。
如果视图对简单的键等效项做出反应,则自定义视图应该覆盖performKeyEquivalent:
方法。
键等效项的一个示例用法是将返回键设置为按钮等效项。
当用户按下返回时,按钮就像被单击了一样。
子类performKeyEquivalent:
的实现应该返回YES
,如果它已经处理了键事件,则返回NO
。
如果视图实现了performKeyEquivalent:
,它通常不会同时实现keyDown:
。
DraggableItemViewDraggableItemView
类重写keyDown:
方法,如例4-14所示,它允许用户按下R键将可拖动矩形的位置重置到视图的原点。
例4-14 DraggableItemView
实现keyDown:
- (void)keyDown:(NSEvent *)event
{BOOL handled = NO;NSString *characters;// get the pressed keycharacters = [event charactersIgnoringModifiers];// is the "r" key pressed?if ([characters isEqual:@"r"]) {// Yes, it ishandled = YES;// reset the rectangle[self setItemPropertiesToDefault:self];}if (!handled)[super keyDown:event];}
**注意:**如果您的子类覆盖了keyDown:
方法,您必须为视图不处理的键事件调用超级实现;否则操作方法将被忽略。
视图通过简单地实现适当的方法来处理NSResponder
操作方法。
DraggableItemView
类实现了其中的四个方法,对应于向上、向下、向左和向右移动操作。
实现如例4-15所示。
例4-15 DraggableItemView
实现moveUp:
、moveDown:
、moveLeft:
和moveRight:
-(void)moveUp:(id)sender
{[self offsetLocationByX:0 andY: 10.0];[[self window] invalidateCursorRectsForView:self];
}-(void)moveDown:(id)sender
{[self offsetLocationByX:0 andY:-10.0];[[self window] invalidateCursorRectsForView:self];
}-(void)moveLeft:(id)sender
{[self offsetLocationByX:-10.0 andY:0.0];[[self window] invalidateCursorRectsForView:self];
}-(void)moveRight:(id)sender
{[self offsetLocationByX:10.0 andY:0.0];[[self window] invalidateCursorRectsForView:self];
}
在例4-15中,每个方法都使用offsetLocationByX: andY:offsetLocationByX:andY:``offsetLocationByX:andY:
实现适当调整。
移动矩形后,每个方法都使光标矩形无效。
这个功能也可以在keyDown:
中实现,直接检查按下键的Unicode字符,检测箭头键,并相应地执行。
但是,使用响应器操作方法允许用户重新映射命令。
通过响应链处理操作方法
NSResponder
不是唯一可以在响应器链上生成事件的类。
任何实现目标操作方法的控件都可以通过响应器链发送这些操作,而不是通过接口生成器中的第一个响应器代理并指定操作来发送特定对象。
可可事件处理指南 中的“响应器链”中提供了通过响应器链发送操作消息的详细讨论。
DraggableItemViewDraggableItemView
类实现了changeColor:
方法,当Color面板中的颜色发生变化时,该方法通过响应链发送。
例4-16显示了DraggableItemView
的changeColor:
实现。
例4-16 DraggableItemView
实现changeColor:
- (void)changeColor:(id)sender
{// Set the color in response// to the color changing in the Color panel.// get the new color by asking the sender, the Color panel[self setItemColor:[sender color]];
}
当颜色面板可见且 DraggableItemView
类的实例是第一个响应者时,改变颜色面板中的颜色会导致矩形改变颜色。
4、Property Accessor Methods
类应为其所有公共属性提供符合键值编码标准的访问方法。
这就为其他需要设置视图各种显示方面的对象提供了一个发布接口。
访问器方法还能实现良好的设计并封装内存管理问题,从而大大降低内存泄漏和崩溃的几率。
DraggableItemView
类为以下属性实现getter和setter访问器方法:itemColor
、backgroundColor
和location
。
每个setter访问器方法测试新值是否与当前值不同,如果不同,则保存新值并将视图标记为需要重新显示适当的部分。
此外,当位置发生变化时,setLocation:
方法也会使光标跟踪矩形无效。
例4-17 DraggableItemView
访问器方法
- (void)setItemColor:(NSColor *)aColor
{if (![itemColor isEqual:aColor]) {[itemColor release];itemColor = [aColor retain];// if the colors are not equal, mark the// draggable rect as needing display[self setNeedsDisplayInRect:[self calculatedItemBounds]];}
}- (NSColor *)itemColor
{return [[itemColor retain] autorelease];
}- (void)setBackgroundColor:(NSColor *)aColor
{if (![backgroundColor isEqual:aColor]) {[backgroundColor release];backgroundColor = [aColor retain];// if the colors are not equal, mark the// draggable rect as needing display[self setNeedsDisplayInRect:[self calculatedItemBounds]];}
}- (NSColor *)backgroundColor
{return [[backgroundColor retain] autorelease];
}- (void)setLocation:(NSPoint)point
{// test to see if the point actually changedif (!NSEqualPoints(point,location)) {// tell the display to redraw the old rect[self setNeedsDisplayInRect:[self calculatedItemBounds]];// reassign the rectlocation=point;// display the new rect[self setNeedsDisplayInRect:[self calculatedItemBounds]];// invalidate the cursor rects[[self window] invalidateCursorRectsForView:self];}
}- (NSPoint)location {return location;
}
6、取消分配视图
当视图的保留计数为零时调用dealloc
方法。
您的应用程序永远不应该显式调用dealloc
。
自动释放机制会在适当的时候调用它。
Deloc的DraggableItemView
实现dealloc
显示颜色对象并调用dealloc
的超级实现。
- (void)dealloc
{[color release];color=nil;[super dealloc];
}
六、高级自定义视图任务
本章创建一个自定义视图描述了自定义视图子类的常见实现细节。
本章描述了高级视图子类化问题,虽然并不罕见,但许多视图子类并不需要这些问题。
1、确定输出设备
大多数视图显示的图像是其状态的稳定表示。
然而,视图对象也与用户动态交互,这种交互通常涉及到与图像本身无关的临时绘图——例如选择和其他突出显示。
这些内容应该只显示在屏幕上,而不是打印机、传真设备或粘贴板上。
您可以通过向当前图形上下文发送isDrawingToScreen
消息来确定视图是否正在绘制到屏幕,如例5-1所示。
例5-1测试输出设备
- (void)drawRect:(NSRect)rect
{[[NSColor whiteColor] set];NSRectFill(rect);// draw a background grid only if we’re drawing to the screenif ([[NSGraphicsContext currentContext] isDrawingToScreen]) {[self drawGrid];}// insert view drawing code here
}
2、绘图外部绘图:
如果定义的方法需要在视图中绘图,而不需要通过drawRect:
方法,则必须在开始任何绘图之前将lockFocus
发送到目标视图,并在完成绘图后立即发送unlockFocus
。
当另一个视图已经拥有焦点时,将焦点锁定在一个视图上是完全合理的。
事实上,这正是子视图在其超级视图中绘制时发生的情况。
聚焦机制保留一个堆栈,其中包含已聚焦的视图,因此当一个视图被发送unlockFocus
消息时,焦点将恢复到之前立即聚焦的视图。
例5-2说明了使用lockFocus
和unlockFocus
方法来确定光标位置像素的颜色。
它将从视图的mouseDown:
、mouseUp:
和mouseMoved:
方法中调用,以响应视图中的鼠标向下事件。
例5-2 显式使用lockFocus
和unlockFocus
- (void) examinePixelColor:(NSEvent *) theEvent
{NSPoint where;NSColor *pixelColor;float red, green, blue;where = [self convertPoint:[theEvent locationInWindow] fromView:nil];// NSReadPixel pulls data out of the current focused graphics context, so -lockFocus is necessary here.[self lockFocus];pixelColor = NSReadPixel(where);// always balance -lockFocus with an -unlockFocus.[self unlockFocus];red = [pixelColor redComponent];green = [pixelColor greenComponent];blue = [pixelColor blueComponent];// we have the color, code that does something with it// would reside here}
注意: 如果另一个线程在同一视图上调用了lockFocus
,lockFocus
可能会阻塞。
当另一个线程在视图上调用unlockFocus
时,排队的lockFocus
会执行。
七、优化视图绘图
绘图通常是处理器密集型操作。
当应用程序将某些东西绘制到屏幕上时,CPU、图形系统、窗口服务器、内核和物理内存都必须贡献资源。
绘图的高成本使其成为有吸引力的优化候选者。
本章描述了您可以在自定义视图中应用的设计选择和技术,以消除冗余或不必要的绘图并提高绘图性能。
注意: 鼓励您使用Developer Tools包中提供的分析实用程序,特别是Sample和Quartz Debug,以确定视图子类可能如何影响应用程序的性能。
有关可用性能分析工具的详细讨论,请参阅 性能概述 。
1、避免过度使用视图
NSView
在管理窗口内容方面提供了极大的灵活性,并提供了绘制应用程序内容的基本画布。
但是,当您考虑窗口的设计时,请仔细考虑如何使用视图。
尽管视图是在窗口内组织内容的便捷方式,但如果您创建复杂、深度嵌套的视图层次结构,您可能会遇到性能问题。
尽管Cocoa窗口可以管理相对大量的视图(大约一百个)而不会出现明显的性能问题,但这个数字包括您的自定义视图以及您使用的标准系统控件和子视图。
如果您的窗口有数百个自定义视觉元素,您可能不想将它们全部实现为NSView
的子类。
相反,您应该考虑编写自己的自定义类,这些类可以由更高级别的NSView
子类管理。
然后可以优化NSView
子类的绘图代码以处理您的自定义对象。
何时使用自定义对象的一个很好的例子是照片浏览器,它显示数百甚至数千张照片的缩略图。
将每张照片包装在NSView
实例中既昂贵又低效。
相反,您最好创建一个轻量级类来管理一张或多张照片,并创建一个自定义视图来管理该轻量级类。
2、指定视图不透明度
如果您实现了NSView
的自定义子类,您可以通过将视图对象声明为不透明来加速绘图性能。
不透明视图是使用不透明颜色填充其内容中的所有像素的视图。
Cocoa绘图系统不需要为一个或多个不透明子视图覆盖的区域向超级视图发送更新消息。
默认情况下,NSView
的isOpaque
方法返回NO
。
要将自定义视图对象声明为不透明,请覆盖此方法并返回YES
。
如果创建不透明视图,请记住视图对象负责使用不透明颜色填充其边界矩形内的所有像素。
有关isOpaque
的示例实现,请参阅View Opacity。
3、使视图的某些部分无效
Cocoa提供了两种重新绘制视图内容的技术。
第一种技术是使用display
、displayRect:
或相关方法立即绘制内容。
第二种技术是稍后通过将视图的某些部分标记为脏的和需要更新来绘制内容。
第二种技术提供了更好的性能,适用于大多数情况。
NSView
定义了方法setNeedsDisplay:
和setNeedsDisplayInRect:
用于将视图的某些部分标记为脏的。
Cocoa收集脏的矩形并保存它们,直到到达运行循环的顶部,此时您的视图被告知要重新绘制自己。
传递到drawRect:
例程中的矩形是脏矩形的联合,但是运行OS X 10.3及更高版本的应用程序可以获得单个矩形的列表,如约束绘图以提高性能中所述。
一般来说,您应该避免调用display
系列方法来重绘视图。
如果您必须调用它们,请不要频繁调用。
因为它们会导致立即调用您的drawRect:
例程,它们会通过抢占其他挂起操作来导致性能显着降低。
它们还排除了合并其他更改然后一次重绘这些更改的能力。
4、约束绘图以提高性能
方法的唯一参数是drawRect:``NSRect
结构),它包围了视图被要求绘制的区域。
这个矩形是自视图实例上次收到display
消息以来被标记为需要更新的矩形的并集。
视图仍然可以在自己的边界内的任何地方绘制,因为应用程序工具包会自动剪掉传入绘图的矩形之外的任何绘图drawRect:
然而,视图可以通过尝试只绘制完全或部分落在剪裁矩形内的内容部分来提高其绘图性能。
在OS X 10.3及更高版本中,视图可以通过使用NSView
方法getRectsBeingDrawn:count:
和needsToDrawRect:
来进一步限制它们的绘图。
这些方法分别提供了对视图无效区域的详细表示的直接和间接访问,即它的不重叠矩形列表,应用程序工具包为每个NSView
实例维护这些区域。
应用程序工具包自动强制裁剪到这个矩形列表,您可以通过让视图将绘图限制在与此列表中的任何矩形相交的对象来进一步提高进行复杂或昂贵绘图的视图的性能。
视图可以调用方法getRectsBeingDrawn:count:
在其drawRect:
实现中检索定义视图被要求绘制区域的非重叠矩形列表。
然后它可以遍历这个矩形列表,对其内容执行交集测试,以确定实际需要绘制什么。
通过消除这些对象,视图可以避免不必要的绘图工作,并提高应用程序的绘图效率。
例6-1显示了getRectsBeingDrawn:count:
的基本用法。
它和下面的代码示例(例6-2))说明了针对视图中可绘制对象intersection-testing矩形列表的技术。
对于交集测试,您可以使用Foundation框架的NSGeometry.h
头文件中声明的函数。
NSIntersectsRect
函数特别有用。
例6-1 针对脏矩形对已知区域的显式交集测试
- (void) drawRect:(NSRect)aRect {const NSRect *rects;int count, i;id thing;NSEnumerator *thingEnumerator = [[self arrayOfAllThingsIDraw] objectEnumerator];[self getRectsBeingDrawn:&rects count:&count];while (thing = [thingEnumerator nextObject]) {// First test against coalesced rect.if (NSIntersectsRect([thing bounds], aRect)) {// Then test per dirty rectfor (i = 0; i < count; i++) {if (NSIntersectsRect([thing bounds], rects[i])) {[self drawThing:thing];break;}}}}
}
对于视图可能绘制的每个对象,此drawRect:
实现首先根据drawRect:
方法的参数(aRect)测试对象的边界矩形。
如果两者相交,视图将确定对象的边界是否与 getRectsBeingDrawn:count:
所获取列表中的任何矩形相交。
如果确实相交,视图将绘制该对象(或要求其绘制自身)。
因为视图通过绘制一组单独定位的项目来呈现其内容是很常见的,所以NSView
类提供了一个方便的方法,基本上可以为您完成例6-1中的大部分工作。
这个方法,needsToDrawRect:
,不需要您使用getRectsBeingDrawn:count:
获取脏矩形列表或执行交集测试的内部循环。
如例6-2所示,生成的代码更加简洁。
例6-2 简化交集测试使用needsToDrawRect:
- (void) drawRect:(NSRect)aRect {id thing;NSEnumerator *thingEnumerator = [[self arrayOfAllThingsIDraw] objectEnumerator];while (thing = [thingEnumerator nextObject]) {if ([self needsToDrawRect:[thing bounds]]) {[self drawThing:thing];}}
}
通过使用与例6-1中使用的相同的“平凡拒绝”测试,对needsToDrawRect:
方法进行了优化,以有效地拒绝完全位于所绘制区域边界之外的对象。
5、抑制默认裁剪
默认情况下,Cocoa会自动将drawRect:
方法中的绘图剪辑到要求视图绘制的区域。
如果视图绘制的区域不属于剪辑的边界,则该绘图都不会到达屏幕。
对于大多数类型的视图,这是适当的行为,因为它可以防止在其他视图拥有的窗口区域中绘图,并且不需要视图仔细限制其绘图。
但在某些情况下,这可能不是您想要的。
剪辑会产生设置、强制和清理成本,如果可以的话,您可能希望避免这些成本。
在这些情况下,自定义视图可以覆盖NSView
方法wantsDefaultClipping
并返回NO
:
- (BOOL)wantsDefaultClipping {return NO;
}
显然,没有强制剪裁带来了危险和机会。
您不能在getRectsBeingDrawn: count:getRectsBeingDrawn:count:
因为这可能会破坏其他视图中的绘图。
您可以采取两种(负责任的)方法之一:
- 你可以非常仔细地画画。
- 您可以提供自己的剪辑。
一种可能的实现策略drawRect:
在这种情况下,迭代正在绘制的矩形列表。
剪辑到每个矩形并绘制内容,一次一个矩形。
这种策略是提高还是降低视图中的绘图性能在很大程度上取决于视图的内容和典型的绘图行为。
6、在动态窗口调整大小期间绘图
实时窗口大小调整是一个优化不佳的绘图代码变得特别明显的领域。
当用户调整窗口大小时,窗口的移动应该是平滑的。
如果您的代码在此期间试图做太多工作,窗口移动可能看起来波涛汹涌,对用户没有反应。
以下部分向您介绍了改进实时调整代码大小的几个选项。
根据您针对的OS X版本,您可以在实现中使用这些选项中的一个或多个。
最小绘制
当实时调整大小操作正在进行时,速度是必不可少的。
提高速度的最简单方法是做更少的工作。
因为在实时调整大小操作中质量通常不太重要,所以您可以采取一些快捷方式来加快绘图速度。
例如,如果您的绘图代码通常执行高精度计算来确定项目的位置,您可以在实时调整大小操作中用快速近似值替换这些计算。
NSView
提供了inLiveResize
方法,让您知道何时正在进行实时调整大小操作。
您可以在drawRect:
例程中使用此方法进行条件绘制,如下例所示:
- (void) drawRect:(NSRect)rect
{if ([self inLiveResize]){// Draw a quick approximation}else{// Draw with full detail}
}
另一种最小化工作的方法是仅重绘在调整大小操作期间暴露的视图区域。
如果您的应用程序针对OS X 10.3版,您可以使用getRectsBeingDrawn:count:
方法来检索暴露的矩形。
如果您的目标是OS X 10.4或更高版本,则提供getRectsExposedDuringLiveResize:count:
方法以仅返回通过调整大小暴露的矩形。
Cocoa Live调整大小通知
您可以使用NSView
的viewWillStartLiveResize
和viewDidEndLiveResize
方法来帮助优化实时调整大小代码。
Cocoa在实时调整大小操作发生之前和之后立即调用这些方法。
您可以使用viewWillStartLiveResize
方法缓存数据或进行任何其他有助于加快实时调整大小代码速度的初始化。
您可以使用viewDidEndLiveResize
方法清理缓存并将视图恢复到正常状态。
Cocoa为窗口层次结构中的每个视图调用viewWillStartLiveResize
和viewDidEndLiveResize
。
此消息只发送给每个视图一次。
在实时调整大小操作期间添加的视图不会收到消息。
同样,如果您在调整大小操作结束前删除视图,这些视图不会收到viewDidEndLiveResize
消息。
如果您使用这些方法创建内容的低分辨率近似,您可能希望使viewDidEndLiveResize
方法中的视图内容无效。
无效视图会导致在实时调整大小循环之外以全分辨率重新绘制视图。
如果您覆盖了viewWillStartLiveResize
或viewDidEndLiveResize
,请确保将消息发送给super
,以允许子视图也为调整大小操作做准备。
如果您需要在调整大小操作开始之前添加视图,如果您希望该视图接收viewWillStartLiveResize
消息,请确保在调用super
之前这样做。
保留窗口内容
在OS X v10.4及更高版本中,Cocoa为您提供了一种在实时调整大小操作期间更智能地更新内容的方法。
NSWindow
和NSView
都支持在操作期间保留内容。
这种技术可以让你决定哪些内容是真正无效的,需要重新绘制。
要支持内容的保存,您必须执行以下操作:
- 覆盖自定义视图中的
preservesContentDuringLiveResize
方法。
您的实现应返回YES
以指示视图支持内容保存。 - 覆盖视图的
setFrameSize:
方法。
您的实现应该使视图中需要重绘的任何部分无效。
通常,这仅包括视图大小增加时暴露的矩形区域。
要查找视图中在调整大小期间暴露的区域,NSView
提供了两种方法。
rectPreservedDuringLiveResize
方法返回视图中未更改的矩形区域。
getRectsExposedDuringLiveResize:count:
方法返回表示任何新暴露区域的矩形列表。
对于大多数视图,您只需将第二种方法返回的矩形传递给setNeedsDisplayInRect:
。
如果您仍然需要使视图的其余部分无效,则提供第一种方法。
以下示例提供了可用于setFrameSize:
方法的默认实现。
在下面的示例中,该实现检查视图是否正在调整大小。
如果是,并且如果调整大小操作暴露了任何矩形,它将获取新暴露的矩形并使其无效。
如果视图大小缩小,此方法什么也不做。
- (void) setFrameSize:(NSSize)newSize
{[super setFrameSize:newSize];// A change in size has required the view to be invalidated.if ([self inLiveResize]){NSRect rects[4];int count;[self getRectsExposedDuringLiveResize:rects count:&count];while (count-- > 0){[self setNeedsDisplayInRect:rects[count]];}}else{[self setNeedsDisplay:YES];}
}
2024-06-16(日)