视图和滚动条

视图NSView

视图是AppKit中控件的基础类,提供了几个主要功能:

1.做为容器放置各种控件

2.各种子类化的视图控件,方便快速开发

3.做为文本视图接收键盘输入

坐标系统

OSX系统中视图坐标系统原点(0,0)在XY坐标系的左下角。

ViewFlipped

有时候处理方便需要原点(0,0)在XY坐标系的左上角,可以通过覆盖视图的isFlipped方法,强制视图坐标系原点为左上角位置。

- (BOOL)isFlipped {
    return YES;
}

Frame & Bounds

视图的frame定义为CGRect (x,y,width,height),表示视图在父视图中的坐标位置Position为(x,y)和大小Size为(width,height),frame在父视图中有意义。

frame

视图的bounds定义为CGRect (0,0,width,height),是视图本身的内部坐标系统,bounds的坐标原点的变化会影响子视图位置。

视图的子视图矩形框的位置坐标(5,5),父视图view的bounds原点从(0,0)变为(-5,-5)后, 矩形在父视图坐标系中坐标值没有变化,但相对位置发生了变化。如果父视图的size不够大的话,矩形一部分区域甚至会超出父视图被裁剪掉。

理解视图bounds的变化对子视图的影响是后续学习滚动视图NSScrollView的基础。

ViewBounds

修改视图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;

坐标转换

ViewCoordinate

每个视图都有自己的坐标系统,因此同一个屏幕点的坐标在不同的视图中是不一样的。视图类中提供了丰富的坐标转换方法,从视图到视图,视图到窗口,视图到屏幕绘图缓存区,视图到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;

ViewHierarchay

增加视图

调用addSubview方法将一个视图增加到父视图

-(void)addSubview:(NSView *)aView;

视图添加到父视图的回调方法

需要得到视图增加到父视图通知时,可以在下面2个方法增加逻辑处理。

-(void)viewWillMoveToSuperview:(nullable NSView *)newSuperview;
-(void)viewDidMoveToSuperview;

隐藏视图

执行setHidden方法,传入YES隐藏视图,NO显示视图。

-(void)setHidden:(BOOL)hidden;

删除视图

调用removeFromSuperview方法将视图从父视图中删除。

-(void)removeFromSuperview;

视图的autoSize控制

使用视图的setAutoresizingMask方法控制视图的自动size属性,当父视图大小变化时,子视图在父视图中上下左右边距,宽度和高度按比例扩展大小。

xib中可以在属性面板中直接点击设置(如下图示)
ViewAutoSize

AutoSizeMask

AutoresizingMask配置是按位定义的,代码中使用逻辑或来设置不同的控制。

typedef NS_OPTIONS(NSUInteger, NSAutoresizingMaskOptions) {
    NSViewNotSizable            =  0,
    NSViewMinXMargin            =  1,
    NSViewWidthSizable         =  2,
    NSViewMaxXMargin            =  4,
    NSViewMinYMargin            =  8,
    NSViewHeightSizable        = 16,
    NSViewMaxYMargin            = 32
};

备注:autoSize视图控制方法已经逐渐被autoLayout自动布局代替了,这种方式了解即可,不推荐使用。

视图layer属性

视图本身没有提供背景,边框,圆角等属性,可以利用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方法中实现界面的个性化绘制。

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方法之外实现绘制

在drawRect方法之外需要绘制视图时,需要使用lockFocus方法锁定视图,完成绘制后在执行unlockFocus解锁。如果在执行lockFocus时已经有其他流程执行了lockFocus,则会将当前操作保存到队列中,等待其他流程执行unlockFocus来恢复后来的lockFocus中的绘图操作。

- (void)updateViewShape {
    [self lockFocus];
    //绘图相关代码
    [self unlockFocus];
}

事件响应

NSView视图继承自NSResponder,可以响应鼠标,键盘以及Action消息,消息可以沿着响应链一直追溯到事件方法的响应者为止。

后面#鼠标和键盘事件#章节会对事件响应做更为详细的说明。

视图的frame/bounds变化通知

NSViewBoundsDidChangeNotification,NSViewFrameDidChangeNotification分别代表视图frame,bounds变化时的消息通知。要接收通知需要注册通知事件,并且设置下面的属性为YES。

@property BOOL postsFrameChangedNotifications;
@property BOOL postsBoundsChangedNotifications;

滚动条视图

当视图size大于父视图的size时,可以使用NSScrollView滚动条视图来控制显示范围。

滚动条视图工作原理

滚动条视图NSScrollView主要包括内容视图NSClipView,滚动条NSScroller,需要滚动控制的文档视图3个互相协作的部分。

ScrollViewComponents

在xib界面上从控件面板拖放创建一个Scroll View,在xib导航区看到的scrollView的结构如下

ScrollViewComponentsXib

我们在前面视图章节中提到过视图的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];

ScrollToPosition

代码滚动TextView的例子

Window xib界面拖一个NSTextView和NSButton控件,绑定ScrollView到Outlet 变量 scrollView,在属性面板中给TextView中输入一大段文字如下,同时给Scroll按钮绑定Action事件scrollClick。

NSTextViewScrol

- (IBAction)scrollClick:(id)sender {
    CGRect frame = self.scrollView.bounds;
    frame.origin.x = frame.origin.x -10;
    self.scrollView.bounds = frame;
}

运行后 点击Scroll按钮,发现每点击一次TextView中的内容向右滚动10个像素距离。