02. NSView 视图对象

视图是所有控件的基础,可以做为控件,也可以做为放置控件的容器,所以其功能也会相对复杂一些,它涉及到包括坐标管理、层级管理、绘制管理、事件响应等。

自定义视图

在开始正式内容之前,先创建一个简单的示例,步骤如下:

  1. 创建一个新项目,采用swift语言和xib界面;
  2. 在xib上添加一个名为 custom view 的视图控件;
  3. 新建一个名为 CustomView.swift类,继承NSView;
  4. 把添加的custom view视图控件的Class指向 CustomView.swift;
    在这里插入图片描述
// CustomView.swift
import Cocoa

final class CustomView: NSView{
    
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
}

外观样式

绘制
draw()方法主要是设置视图的外观样式,即重绘,Mac OS出于性能考虑默认是采用延时绘制的,但是如果调用其display或displayRect方法则会立即重绘。

    // CustomView.swift    
    // MARK :  Draw, 
    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
        NSColor.blue.setFill()
        let frame = self.bounds
        let path = NSBezierPath()
        path .appendRoundedRect(frame, xRadius: 20, yRadius: 20)
        path.fill()
    }

在这里插入图片描述
设置层样式
相当于边框和背景色等

// CustomView.swift
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.configLayer()
    }
    func configLayer(){
        self.wantsLayer = true
        self.layer?.backgroundColor = NSColor.red.cgColor
        self.layer?.borderWidth = 2
        self.layer?.cornerRadius = 10
    }

个性化绘制
如果在drawRect方法之外还需要绘制视图,则需要实现以下功能,但需要注意先锁定,再绘制。

    func drawViewShape(){
        self.lockFocus()
        //NSRectFill(self.bounds)
        let text: NSString = "RoundedRect"
        let font = NSFont(name: "Palatino-Roman", size: 12)
        let attrs = [NSAttributedStringKey.font :font! ,
                        NSAttributedStringKey.foregroundColor :NSColor.blue,NSAttributedStringKey.backgroundColor :NSColor.red]
        let loaction = NSPoint(x: 50, y: 50)
        text.draw(at: loaction, withAttributes: attrs)
        self.unlockFocus()
    }

坐标系管理

在Mac OS系统中坐标系的顶点可为左下或左上两种,默认为左下,可通过以下方法变为左上角。

    // CustomView.swift
    //复写NSView中属性,改变坐标系为左上角
    override var isFlipped: Bool{
        get{
            return true
        }
    }

Frame和Bounds

坐标的计算有两个非常关键的属性,这两个属性涉及到滚动计算和自动布局等功能,详细解释如下:

  • frame: 表示当前视图添加到父视图中时,坐标位置和大小,相对父视图;
  • bounds:表示当前视图的内部坐标系,值的变化会引起它之中所有子视图位置的变化,相对子视图;
    在这里插入图片描述
    上述两个属性都可由CGRect(x, y, width, height)来定义,先看左图:
  • 如果子视图(蓝色)设置了frame属性(x=5, y=5),则其位置(5,5)是相对于父视图原点位置为参照物的;
  • 如果设置父视图的bounds属性(-5, -5),则表示其原点坐标由(0,0)变成为(-5,-5),此时子视图的frame实际变成了(10, 10);

上述坐标的变化就是NSScrollView的实现原理。

import Cocoa

@main
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet var window: NSWindow!
    //子控件
    @IBOutlet weak var customView: CustomView!
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        //this
        let frame = NSRect(x: 2, y: 2, width: 80, height: 18)
        self.customView.frame = frame
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }

    func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
        return true
    }
}

坐标系转换

每个视图都有自己的内部坐标系统,所以屏幕上同一个点在不同视图中的坐标系统是不一样的,很多时候是需要进行转换坐标系的,比如在**鼠标事件处理(主要用于鼠标事件处理)**中,原始坐标点是基于window的,所以需要通过某种方法转换为当前操作对象的坐标系统,在MacOS下支持以下几种:

  • conventPoint():从源视图到目标视图或从目标视图到源视图;
  • conventPointFromBacking():从视图绘图缓冲区到目标视图;
  • conventPointToBacking():从目标视图到视图绘图缓冲区;
  • conventPointToLayout():从目标视图到层;
  • conventPointFromLayout():从层到目标视图;
//CustomView.swift
    // MARK: Event
    override func mouseDown(with theEvent: NSEvent) {
        // nil 默认为window
        let point = self.convert(theEvent.locationInWindow, to: nil)
        NSLog("window point: \(theEvent.locationInWindow)")
        NSLog("view point: \(point)")
    }
//~window point: (145.78448486328125, 245.07119750976562)
//~view point:   (194.78448486328125, 78.92880249023438)

视图管理

主要是增、删和事件通知等,本小节先只介绍基本内容。

子视图管理

  • addSubview():添加子视图;
  • removeFromSuperView():从父视图中删除;
  • setHidden():显示/隐藏视图;

以下代码是把一个按钮添加到当前窗口中:

    // AppDelegate.swift
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let frame = NSRect(x: 2, y: 2, width: 80, height: 18)
        
        addNewView(frame)
    }
    
    func addNewView(_ frame: NSRect){
        let btn = NSButton()
        btn.frame = frame
        btn.title = "button click"
        btn.bezelStyle = .rounded
        
        self.customView.addSubview(btn)
        self.customView.postsFrameChangedNotifications = true
    }

视图事件

视图的事件比较特殊,它是采用消息通知实现的,主要是定义一些操作的回调方法

    // AppDelegate.swift
    // 自定义事件,添加到 func applicationDidFinishLaunching()方法中    
    func registerNotification() {
         NotificationCenter.default.addObserver(self, selector:#selector(self.recieveFrameChangeNotification(_:)),  name:NSView.frameDidChangeNotification, object: nil)
    }

    //事件执行函数
    @objc func recieveFrameChangeNotification(_ notification: Notification){
        print("dddddd")
    }

视图查找

每个视图控件都可设置一个名为tag的属性,这样就可以在父视图中通过tag来查找子视图,然后做一些其它操作了,也可以设置identifier标识。

    // AppDelegate.swift 
    func addNewView(_ frame: NSRect){
        let btn = NSButton()
        btn.frame = frame
        btn.title = "button click"
        btn.bezelStyle = .rounded
        self.customView.addSubview(btn)
        self.customView.postsFrameChangedNotifications = true
        
        btn.tag = 1;
        let btnView: NSView? = self.customView.viewWithTag(1)
    }

自动大小

可通过设置self.customView.autoresizingMask = NSAutoresizingMaskOptions,但现在基本被autoLayout布局代替的了,所以不推荐使用这种方法;
在这里插入图片描述
视图的详细位置设置原理:
在这里插入图片描述

视图截图

可以为自定义视图中定义一个截图方法,供外部按钮调用,需要注意沙箱问题,保存目录需要开启相应的权限。

     //CustomView.swift
    func saveSelfAsImage() {
        self.lockFocus()
        let image = NSImage(data:self.dataWithPDF(inside: self.bounds))
        self.unlockFocus()
        let imageData = image!.tiffRepresentation
        
        let fileManager = FileManager.default
        //写死的文件路径
        let path = "/Users/liudong/Desktop/myCapture.png"
        fileManager.createFile(atPath: path, contents: imageData, attributes: nil)
        
        //定位文件路径
        let fileURL = URL(fileURLWithPath: path)
        NSWorkspace.shared.activateFileViewerSelecting([fileURL])
    }

带滚动条的视图截图

    func saveScrollViewAsImage() {
        let pdfData = self.dataWithPDF(inside: self.bounds)
        let imageRep = NSPDFImageRep(data: pdfData)!
        let count = imageRep.pageCount
        for i in 0..<count {
            imageRep.currentPage = i
            let tempImage = NSImage()
            tempImage.addRepresentation(imageRep)
            let rep  = NSBitmapImageRep(data:tempImage.tiffRepresentation!)
            let imageData = rep?.representation(using:.png, properties: [:])
            let fileManager = FileManager.default
            //写死的文件路径
            let path = "/Users/liudong/Desktop/myCapture.png"
            fileManager.createFile(atPath: path, contents: imageData, attributes: nil)
            
            //定位文件路径
            let fileURL = URL(fileURLWithPath: path)
            NSWorkspace.shared.activateFileViewerSelecting([fileURL])
        }
    }

滚动视图

当父视图小于子视图的尺寸时就会产生滚动。NSScrollView主要包含NSClipview和NSScroller滚动条。
在这里插入图片描述
滚动的原理就是NSScrollerView通过NSClipView视图的bounds属性的x,y值的变化实现的滚动效果。

编码滚动视图

代码如下:

    func addScrollView(){
        
        let frame = CGRect(x: 10, y: 10, width: 50, height:50)
        
        //创建滚动视图,创建图像视图,添加图片
        let myScrollView = NSScrollView(frame: frame)
        let image = NSImage(named: "BtnIcon")
//        let image = NSImage(named: NSImage.Name(rawValue: "screenshot.png"))
        let imageViewFrame = CGRect(x: 0, y: 0, width: (image?.size.width)!, height:  (image?.size.height)!)
        let imageView = NSImageView(frame: imageViewFrame)
        imageView.image = image
        
        //下面两行控制是否显示滚动条(但不影响滚动)
        myScrollView.hasVerticalScroller = true
        myScrollView.hasHorizontalScroller = true
        myScrollView.documentView = imageView //显示图像
    }

子视图滚动

滚动到顶部,在上节代码中继续添加下列代码

        let contentView2: NSClipView = myScrollView.contentView

        //滚动到顶部位置
        var newScrollOrigin : NSPoint
        let contentView: NSClipView = myScrollView.contentView

        if self.window.contentView!.isFlipped {
            newScrollOrigin = NSPoint(x: 0.0,y: 0.0);
        }
        else{
            newScrollOrigin = NSPoint(x: 0.0,y: imageView.frame.size.height-contentView.frame.size.height);
        }
        contentView.scroll(to: newScrollOrigin)

添加事件,事件的滚动应该是设置clipView,而不是最外层的ScrollerView。

     @IBOutlet weak var clipView: NSClipView!
    @IBAction func scrollTextView(_ sender: NSButton) {
        var cgRect = self.clipView.frame
        cgRect.origin.y = cgRect.origin.y + 10
        self.clipView.bounds = cgRect
    }

自定义滚动条

上述通过myScrollView.hasVerticalScroller = true只能设置是否显示,但不禁止其功能,如果需要禁止滚动需要重新定义,重写scrollWheel方法,给y轴设置一个偏移量即可:

class DisableVerticalScrollView: NSScrollView {
    override func scrollWheel(with event: NSEvent) {
        let f = abs(event.deltaY)
        if event.deltaX == 0.0 && f >= 0.01  {
            return
        }
        if event.deltaX == 0.0 && f == 0.0  {
            return
        }
        else {
            super.scrollWheel(with: event)
        }
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

korgs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值