第十七章: 自定义View
程序中所有的可视对象要么是window,要么是view.在这一章中,你将创建一个NSView的子类. 随着时间的推移,你一般会需要创建自定义的view来完成自定义画图和事件响应.即使你没有打算这样做,你也应该通过学习创建view类来了解cocoa的内部工作机制window是NSWindow的对象.每个window都会有多个views,每个view描述window中的一个矩形区域. view负责该区域的画图动作以及鼠标事件响应. view也可以响应键盘消息. 你以及和多个view的子类打过交道了: NSButton, NSTextField,NSTableView,和NSColorWell 都是view (注意,window不是NSView的子类)
View的层次
view是按一定层次关系组织(如图17.1) .window包含了一个叫做content view的view.该view填满了整个window内部区域[除去title bar. 你可以在NSWindow类中找到contentView 和 setContentView方法] 通常,content view会有自己的子view.而这些子view有会有自己的子view.一个view知道自己的父view和子view,并知道自己所属的窗口 [到NSView类声明中,找找和它们相关的方法]找到了么?
- (NSView *)superview; - (NSArray *)subviews; - (NSWindow *)window;任何类型的view [这么说是指 view的子类,如NSButton,NSTableView...]都可以包含多个子view.不过对于大部分类型的view,我们不会给它添加子view. 下面5中类型view通常有子view
1. window的content view2. NSBox. box中的内容就是它的子view3. NSScrollView. scroll view中包含的view就是它的子view. scroll bar也是它的子view4. NSSplitView. SplitView中的view就是它的子view 如图 17.2-- 让一个View画自己 --
在本节中,将创建一个简单的view. 它将自己刷成绿色.就像17.4 [灰色的..呵呵]新建一个Cocoa Application工程, 命名为ImageFun. 点选File->New file菜单,创建一个Objective-C NSView子类.命名为StretchView.
--创建一个View 子类的对象--
打开MainMenu.nib. 从Library中拖一个CustomView(view&cell->Layout View)放置在window上.如图17.5-- drawRect--
当一个view需要刷新显示时,它将会收到drawRect:的消息,参数为一个需要重画的矩形区域, 这个方法会被自动调用-你不需要直接在代码中调用. 如果你需要让一个view重画,可以调用方法setNeedsDisplays[myView setNeedsDisplay:YES];该方法将myView设置成"脏"的. 在事件处理中,myView将被重画
[cocoa , 系统]在调用drawView:前, 会对这个view lock focus. 每一个view都有自己的graphics context-包含了view的坐标系统,当前颜色,当前字体等. 当view被lock focus,它的graphics context将激活,而当unlock focus后,它的graphics context将不是激活状态. 任何时候的draw命令都是在当前激活的graphics context进行 [对于Mac的画图draw, 可以看看Quarz 2D . graphics context也是它里面的概念咯. 其实cocoa view 画图也是通过Quarz 2D来实现, 要对屏幕显卡进行绘制,那么就要有一个绘制环境,这个环境也就是graphics context , cocoa 中在每个view中保存了一个各自的graphics context . 绘制到那个view时就将它的graphics context设置为当前Quarz用来绘制的graphics context - 通过lock focus]
你可以使用NSBezierPath来绘制线条,圆形,曲线和矩形. 你可以使用NSImage来在view上绘制合成图像. 在本节例子中, 绘制一个绿色的矩形
打开StretchView.m. 添加如下代码
- (void)drawRect:(NSRect)rect{ NSRect bounds = [self bounds]; [[NSColor greenColor] set]; [NSBezierPath fillRect:bounds];}如图17.10, NSRect结构由两个成员组成:origin - NSPoint类型, 和size - NSSize类型
因为性能的原因,Objective-C类中很少使用到结构. 你有可能用到的一些cocoa结构: NSPoint,NSRect,NSRange,NSDecimal 和NSAffineTransformStruct等等. NSRange描述区间. NSDecimal描述数字精度, NSAffineTransformStruct描述图形线性变换
注意,view通过bounds知道自己的位置. 在drawRect中得到bounds区域,将当前color设置为绿色,再使用当前色来填充整个bounds区域
通过参数传递的NSRect描述了这个view需要重画的"脏"的区域.它有可能会小于整个view的大小.如果绘制比较费时间的东西,可以只对该脏的区域进行重新绘制
setNeedsDisplay:将激发view整个可见区域重画. 如果需要激发view某个指定区域进行重话可以使用setNeedsDisplayInRect:
NSRect dirtyRect;dirtyRect = NSMakeRect(0, 0, 50, 50);[myView setNeedsDisplayInRect:dirtyRect];编译运行程序.试着改变window的大小看看
-- 使用NSBezierPath绘制--
如果想绘制线条,曲线或多边形,可以使用NSBezierPath. 前面,你使用了NSBezierPath的fillRect 类方法来给view上色.在这节中你将使用NSBezierPath绘制随机点间的线条如图17.11@interface StretchView : NSView
{ NSBezierPath *path;}- (NSPoint)randomPoint;@end
在StretchView.m中,重载initWithFrame方法-这是NSView的designated initializer[还记得它吧] . 它会在view对象创建时自动调用[在这个例子中,是在nib文件加载是cocoa调用]. 修改StretchView.m 在initWithFrame中,创建了一个path对象#import "StretchView.h"@implementation StretchView
- (id)initWithFrame:(NSRect)rect
{ if (![super initWithFrame:rect]) return nil;// Seed the random number generator
srandom(time(NULL));// Create a path object
path = [[NSBezierPath alloc] init]; [path setLineWidth:3.0]; NSPoint p = [self randomPoint]; [path moveToPoint:p]; int i; for (i = 0; i < 15; i++) { p = [self randomPoint]; [path lineToPoint:p]; } [path closePath]; return self;}- (void)dealloc{ [path release]; [super dealloc];}// randomPoint returns a random point inside the view- (NSPoint)randomPoint{ NSPoint result; NSRect r = [self bounds]; result.x = r.origin.x + random() % (int)r.size.width; result.y = r.origin.y + random() % (int)r.size.height; return result;}- (void)drawRect:(NSRect)rect
{ NSRect bounds = [self bounds];// Fill the view with green
[[NSColor greenColor] set]; [NSBezierPath fillRect: bounds];// Draw the path in white
[[NSColor whiteColor] set]; [path stroke];}@end
编译运行程序,怎么样?酷吧! 好了,现在用[path fill] 代替[path stroke]看看,有什么不一样?-- NSScrollView--
在美术世界里,在同样的质量下,绘制的越大就越美观啊. 你的view很漂亮了.不过能不能让它更大一点呢., 你需要将它放置在scroll view中如图17.12scroll view由3个部分组成: document view, content view,和scroll bar. 在本例中.你的view将成为document view,并显示在content view中-它是NSClipView的对象
虽然这个看上去复杂,其实很容易办到. 实际上都不需要添加代码. 打开mainmenu.nib文件,选中view. 从LayOut 菜单中选择Embed Objects in Scroll View如图17.13
-- 通过程序创建View--
你可以在Interface Builder中实例化多个view. 有时候,你会需要通过程序来创建view.例如,假定你希望在window上创建一个buttonNSView *superview = [window contentView];NSRect frame = NSMakeRect(10, 10, 200, 100);NSButton *button = [[NSButton alloc] initWithFrame:frame];[button setTitle:@"Click me!"];[superview addSubview:button];[button release];--思考:cells--
NSControl从NSView继承得到. 因为view有自己的graphics context. 这让view成为一个大,高价的类. 当初,在提供NSButton类时, 有人要编写一个计算器程序, 他第一件事就是创建10行10列的NSButton. 这样就有100个view别创建,效率是相当低啊. 后来,有人想到了一个聪明的主意: 将NSButton的大脑移到另外一个类[大脑? 就是button的主要功能了](不再是view类).并创建一个大的view(叫做NSMatrix). 用来装那100个button大脑. 我们把这个button 大脑内叫 NSButtonCell [这个就如设计原则所说,内的聚合咯,少用继承,多用聚合,看到好处了] 如图17.16到最后,NSButton就是一个简单的view再加上它的大脑 NSButtonCell. button cell做了所有事情,而NSButton只是window上的一块绘制区域如图17.17
你拖一个control到window上,然后选择Embed Objects In -> Martix. 这样就创建了一个NSMatrix. 可以按住Option拖动martix 来设定它的行列数.如图17.18
在处理matirx时,你常常要面对这样的问天,哪个cell激活?cell也可以设置它的tag
- (IBAction)myAction:(id)sender { id theCell = [sender selectedCell]; int theTag = [theCell tag]; ...}cell的tag可以通过Interface Builder来设定
-- 思考: isFlipped --
PDF和PostScript使用的是标准的迪卡尔坐标系统.当向上移动页面时,y值增加. Quartz使用了同样的模型. view的原点在左下点.对于有些绘制,如果让原点在左上方会更方便. 也就是当向下移动页面是y增加,这时我们叫这个view是filpped的
你通过重载方法ifFlipped来filp一个view
- (BOOL)isFlipped{ return YES;}当我们讨论坐标系统时, x和y是使用点来计数的. 一般72点=1英寸. 默认的1.0point及时屏幕上的一个像素.不过,你可以通过改变坐标系统来改变point的大小
// Make everything in the view twice as largeNSSize newScale;newScale.width = 2.0;newScale.height = 2.0;[myView scaleUnitSquareToSize:newScale];[myView setNeedsDisplay:YES];-- 挑战 --
NSBezierPath可以会在Bezier曲线. 绘制看看咯.(请查看NSBezierPath帮助文档)