视图是AppKit中控件的基础类,提供了几个主要功能:
1.做为容器放置各种控件
2.各种子类化的视图控件,方便快速开发
3.做为文本视图接收键盘输入
OSX系统中视图坐标系统原点(0,0)在XY坐标系的左下角。
有时候处理方便需要原点(0,0)在XY坐标系的左上角,可以通过覆盖视图的isFlipped方法,强制视图坐标系原点为左上角位置。
- (BOOL)isFlipped {
return YES;
}
视图的frame定义为CGRect (x,y,width,height),表示视图在父视图中的坐标位置Position为(x,y)和大小Size为(width,height),frame在父视图中有意义。
视图的bounds定义为CGRect (0,0,width,height),是视图本身的内部坐标系统,bounds的坐标原点的变化会影响子视图位置。
视图的子视图矩形框的位置坐标(5,5),父视图view的bounds原点从(0,0)变为(-5,-5)后, 矩形在父视图坐标系中坐标值没有变化,但相对位置发生了变化。如果父视图的size不够大的话,矩形一部分区域甚至会超出父视图被裁剪掉。
理解视图bounds的变化对子视图的影响是后续学习滚动视图NSScrollView的基础。
修改视图Frame相关的原点Origin,大小Size,矩形Frame,使用下面方法或属性
-(void)setFrameOrigin:(NSPoint)newOrigin;
-(void)setFrameSize:(NSSize)newSize;
@property NSRect frame;
修改视图Bounds原点Origin,大小Size,bounds,使用下面方法或属性
-(void)setBoundsOrigin:(NSPoint)newOrigin;
-(void)setBoundsSize:(NSSize)newSize;
@property NSRect bounds;
每个视图都有自己的坐标系统,因此同一个屏幕点的坐标在不同的视图中是不一样的。视图类中提供了丰富的坐标转换方法,从视图到视图,视图到窗口,视图到屏幕绘图缓存区,视图到layer层之间坐标转换。包括坐标point,大小size,矩形rect 3类的互相转换函数。
下面仅列出坐标点的相关转换方法,其他大小size,矩形rect请参考NSView.h 的头文件。
1.从源视图坐标系转化到视图自己的坐标系(aView参数为nil是默认为window)
-(NSPoint)convertPoint:(NSPoint)aPoint fromView:(NSView *)aView;
2.从视图自己的坐标系转化到目标视图的坐标系
-(NSPoint)convertPoint:(NSPoint)aPoint toView:(NSView *)aView;
3.视图缓存区坐标系转化到视图自己的坐标系
-(NSPoint)convertPointFromBacking:(NSPoint)aPoint;
4.从视图自己的坐标系转化到视图缓存区坐标系
-(NSPoint)convertPointToBacking:(NSPoint)aPoint;
5.视图跟layer中间的坐标转换
-(NSPoint)convertPointToLayer:(NSPoint)aPoint
-(NSPoint)convertPointFromLayer:(NSPoint)aPoint ;
坐标转换最基本的使用是在视图的鼠标事件处理中,原始的坐标点是基于window视图的,需要转换为视图坐标处理。
- (void)mouseDown:(NSEvent *)event {
//从window坐标转换为视图坐标
NSPoint clickLocation = [self convertPoint:[event locationInWindow]
fromView:nil];
}
视图做为容器可以添加子视图,子视图中又可以继续添加下一级视图,形成多层级嵌套关系。
下面3个属性分别代表视图的window,视图的父视图,视图的所有子视图。
@property (readonly, assign) NSWindow *window;
@property (readonly, assign) NSView *superview;
@property (copy) NSArray<NSView *> *subviews;
每个视图可以提供一个唯一的tag属性。视图通过viewWithTag方法按深度遍历算法去查找子视图。
-(NSView *)viewWithTag:(NSInteger)aTag;
NSView视图类的tag属性是只读的,NSControl子类实现可读写的tag属性。
@property (readonly) NSInteger tag;
调用addSubview方法将一个视图增加到父视图
-(void)addSubview:(NSView *)aView;
需要得到视图增加到父视图通知时,可以在下面2个方法增加逻辑处理。
-(void)viewWillMoveToSuperview:(nullable NSView *)newSuperview;
-(void)viewDidMoveToSuperview;
执行setHidden方法,传入YES隐藏视图,NO显示视图。
-(void)setHidden:(BOOL)hidden;
调用removeFromSuperview方法将视图从父视图中删除。
-(void)removeFromSuperview;
使用视图的setAutoresizingMask方法控制视图的自动size属性,当父视图大小变化时,子视图在父视图中上下左右边距,宽度和高度按比例扩展大小。
xib中可以在属性面板中直接点击设置(如下图示)
AutoresizingMask配置是按位定义的,代码中使用逻辑或来设置不同的控制。
typedef NS_OPTIONS(NSUInteger, NSAutoresizingMaskOptions) {
NSViewNotSizable = 0,
NSViewMinXMargin = 1,
NSViewWidthSizable = 2,
NSViewMaxXMargin = 4,
NSViewMinYMargin = 8,
NSViewHeightSizable = 16,
NSViewMaxYMargin = 32
};
备注:autoSize视图控制方法已经逐渐被autoLayout自动布局代替了,这种方式了解即可,不推荐使用。
视图本身没有提供背景,边框,圆角等属性,可以利用layer属性来控制这些效果,使用层属性之前必须调用设置wantsLayer为YES。
self.wantsLayer = YES;
self.layer.backgroundColor = [NSColor redColor].CGColor;
self.layer.borderColor = [NSColor greenColor].CGColor;
self.layer.borderWidth = 2;
self.layer.cornerRadius = 20;
视图绘制是调用drawRect:方法来实现的。对于AppKit中的各种界面控件,系统默认实现了不同控件的界面绘制和事件响应控制,对于自定义的控件可以在drawRect方法中实现界面的个性化绘制。
从性能方面考虑系统对界面绘制采用了延时绘制机制进行的。调用setNeedsDisplay:或setNeedsDisplayInRect:方法,使当前视图或Rect定义的区域变为invalidate状态,并不是立即绘制,系统会在下一个绘图周期重绘。
调用display,displayRect: 方法会强制视图立即重绘。
下面的代码使用Quartz 2D的绘图函数实现了在视图上绘制圆角矩形。
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
[[NSColor blueColor] setFill];
NSRect bounds = [self bounds];
NSBezierPath *roundedShape = [NSBezierPath bezierPath];
[roundedShape appendBezierPathWithRoundedRect:bounds xRadius:20 yRadius:20];
[roundedShape fill];
}
在drawRect方法之外需要绘制视图时,需要使用lockFocus方法锁定视图,完成绘制后在执行unlockFocus解锁。如果在执行lockFocus时已经有其他流程执行了lockFocus,则会将当前操作保存到队列中,等待其他流程执行unlockFocus来恢复后来的lockFocus中的绘图操作。
- (void)updateViewShape {
[self lockFocus];
//绘图相关代码
[self unlockFocus];
}
NSView视图继承自NSResponder,可以响应鼠标,键盘以及Action消息,消息可以沿着响应链一直追溯到事件方法的响应者为止。
后面#鼠标和键盘事件#章节会对事件响应做更为详细的说明。
NSViewBoundsDidChangeNotification,NSViewFrameDidChangeNotification分别代表视图frame,bounds变化时的消息通知。要接收通知需要注册通知事件,并且设置下面的属性为YES。
@property BOOL postsFrameChangedNotifications;
@property BOOL postsBoundsChangedNotifications;
当视图size大于父视图的size时,可以使用NSScrollView滚动条视图来控制显示范围。
滚动条视图NSScrollView主要包括内容视图NSClipView,滚动条NSScroller,需要滚动控制的文档视图3个互相协作的部分。
在xib界面上从控件面板拖放创建一个Scroll View,在xib导航区看到的scrollView的结构如下
我们在前面视图章节中提到过视图的bounds的变化可以影响到它的子视图的位置变化。NSScrollView就是通过NSClipView视图bounds的x或y坐标变化来实现文档视图在水平或者垂直方向滚动。
下面的代码首先创建了NSScrollView对象scrollView,接着创建了NSImageView图像视图,设置了图像视图的image和frameSize;然后设置了scrollView允许有垂直和水平2个方向的滚动条。最后是配置了NSScrollView的documentView。
NSScrollView *scrollView = [[NSScrollView alloc]initWithFrame:[self.window.contentView bounds]];
NSImage *image = [NSImage imageNamed:@"screen.png"];
NSImageView *imageView = [[NSImageView alloc]initWithFrame:scrollView.bounds];
[imageView setFrameSize:image.size];
imageView.image = image;
scrollView.hasVerticalScroller = YES;
scrollView.hasHorizontalScroller = YES;
scrollView.documentView = imageView;
[self.view addSubview:scrollView];
NSView提供了下面2个方法实现了视图滚动到指定的位置或一个矩形区域。
-(void)scrollPoint:(NSPoint)aPoint;
-(BOOL)scrollRectToVisible:(NSRect)aRect;
NSView的enclosingScrollView属性可以获取到视图的滚动条,如果视图没有滚动条则enclosingScrollView为nil.
滚动到视图顶部
NSScrollView *scrollView = self.enclosingScrollView;
NSScrollView *contentView = scrollView.contentView;
NSPoint newScrollOrigin;
if(self.isFlipped){
newScrollOrigin = NSMakePoint(0.0,0.0);
}
else{
newScrollOrigin = NSMakePoint(0.0,self.frame.size.height-contentView..frame.size.height);
}
[self scrollPoint:newScrollOrigin];
滚动到视图顶部
if(self.isFlipped){
newScrollOrigin = NSMakePoint(0.0,self.frame.size.height-contentView..frame.size.height);
}
else{
newScrollOrigin = NSMakePoint(0.0,0.0);
}
[self scrollPoint:newScrollOrigin];
Window xib界面拖一个NSTextView和NSButton控件,绑定ScrollView到Outlet 变量 scrollView,在属性面板中给TextView中输入一大段文字如下,同时给Scroll按钮绑定Action事件scrollClick。
- (IBAction)scrollClick:(id)sender {
CGRect frame = self.scrollView.bounds;
frame.origin.x = frame.origin.x -10;
self.scrollView.bounds = frame;
}
运行后 点击Scroll按钮,发现每点击一次TextView中的内容向右滚动10个像素距离。