NSView - ShenYj/ShenYj.github.io GitHub Wiki

视图: NSView

基本视图

视图 (View, 对应类 NSView) 是 AppKit 中控件的基础, 提供一下几个主要功能

  • 作为容器防止各种控件
  • 提供子类化的视图控件,方便快速开发
  • 作为文本视图接收键盘输入

坐标系统

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

如果想要让坐标系原点从左上角开始, 可以通过覆盖视图的 isFlipped 方法返回 true, 参考: AppKit Coordinates

frame和bounds

iOS 基本保持一致,某些值类型要注意下类型:

  • CGSize -> NSSize
  • CGPoint -> NSPoint

变化通知

NSView.frameDidChangeNotificationNSView.boundsDidChange 分别代表视图 framebounds 变化时的消息通知。

要接收通知,除了需要注册上述两个通知事件,还需要设置视图下面的两个属性为 true

@property BOOL postsFrameChangedNotifications;
@property BOOL postsBoundsChangedNotifications;
  • e.g.

    func registerNotification() {
        NotificationCenter.default.addObserver(self,
                                        selector: #selector(recieveFrameChangeNotification(_:)),
                                        name: NSView.frameDidChangeNotification,
                                        object: scrollView)
        
    }
    @objc
    func recieveFrameChangeNotification(_ notification: Notification) {
        
    }

坐标转换

类似于 iOS 手指点击一样, 在 macOS 同样有这通过鼠标点击获得某个点的坐标然后进行转换的需求

视图管理

视图作为容器可以添加子视图,子视图中又可以继续添加下一级视图,形成多层级嵌套关系

  • 通过以下属性分别代表视图的窗口、视图的父视图和视图的所有子视图

    public var window: NSWindow? { get }
    public var superview: NSView? { get }
    public var subviews: [NSView]

需要注意的是视图的 window 属性,需要在视图显示完成后才能正常获得,因此一般是在视图所在的 ViewController 对象的 viewDidAppear 方法而不是 viewDidLoaded 方法中获得

视图查找

与 UIView 一样,每个视图都绑定一个唯一的 tag 属性。 视图通过 viewWithTag 方法去查找子视图

public func viewWithTag(aTag: Int) -> NSView?
public var tag: Int { get }

NSViewtag 属性是只读的, NSControl 子类实现了可读写的 tag 属性

在经历过 iOS 开发出示 macOS 时的第一感觉就是常规 iOS 下哪些可读写属性都是只读的

autoSize

iOS 平台无区别

layer

视图本身没有提供背景、边框和圆角等属性,可以利用 layer 属性来控制这些效果。

使用 layer 属性前必须先设置 wantsLayertrue

self.wantsLayer = true
self.layer?.backgroundColor = NSColor.red.cgColor
self.layer?.borderWidth = 2
self.layer?.cornerRadius = 10

第一次想给视图设置颜色的时候也是把我搞的头大了,或许这也是在推出 iOS 后改进了,习惯了 iOS 开发,对这种表示不方便

视图绘制

NSView的视图绘制也是调用 drawRect 方法实现的。对于 AppKit 中的各种界面控件,系统默认实现了不同控件的界面绘制和事件响应控制,如果要自定义控件的外观样式,可以在 drawRect 方法中实现界面绘制。

在drawRect方法中实现绘制

从性能方面考虑,系统对界面绘采用了延时绘制机制进行的。调用 setNeedsDisplay: 方法使当前视图或 Rect 定义的区域变为 invalidate 状态,并不是立即绘制,系统会在下一个绘制周期重绘。调用 displaydisplayRect 方法会强制视图立即重绘

  • 下面的代码使用 Quartz 2D 的绘图函数实现了在视图上绘制圆角矩形:

    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()
    }

在drawRect方法之外实现绘制

drawRect 之外绘制视图时,需要使用 lockFocus 方法锁定视图,完成绘制后再执行 unlockFocus 解锁。

如果在执行 lockFocus 时已经有其他流程执行了 lockFocus,则会将当前操作保存到队列中,等待其他流程执行 unlockFocus 来恢复后来的 lockFocus 中的绘制操作。

drawRect 方法与 lockFocus 锁定方式不能同时使用

  • 不过这种方式现在已经被标记为过期

    @available(macOS, introduced: 10.0, deprecated: 10.14, message: "To draw, subclass NSView and implement -drawRect:; AppKit's automatic deferred display mechanism will call -drawRect: as necessary to display the view.")
    open func lockFocus()

实测也没有看到效果

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

视图截屏

使用 lockFocus 方法锁定后获得 PDF 数据转换成 NSData 写入文件。

由于 lockFocus 方法已经过期,因此在没有锁定/解锁的情况下,实测还是可以截屏的

  • 示例代码

    func saveSelfAsImage() {
        // lockFocus()
        let image = NSImage(data: dataWithPDF(inside: bounds))
        // unlockFocus()
        let imageData = image!.tiffRepresentation
        let fileManager = FileManager.default
        let path = "/Users/shenyj/Downloads/myCapture.png"
        fileManager.createFile(atPath: path, contents: imageData, attributes: nil)
        
        // 保存结束后 Finder 中自动定位到文件路径
        let fileURL = URL(fileURLWithPath: path)
        NSWorkspace.shared.activateFileViewerSelecting([fileURL])
    }

macOS 中保存图片,需要注意下沙盒权限的配置, 参考资料: Sandbox extension creation failed

  • 如果视图比较大,是带滚动条的大视图,则按照下面的方法处理,可以保证获得整个滚动页面的截图

    func saveScrollViewAsImage() {
        let pdfData = dataWithPDF(inside: 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 path = "/Users/shenyj/Downloads/scrollViewCapture.png"
            FileManager.default.createFile(atPath: path, contents: imageData, attributes: nil)
            
            // 保存结束后 Finder 中自动定位到文件路径
            let fileURL = URL(fileURLWithPath: path)
            NSWorkspace.shared.activateFileViewerSelecting([fileURL])
        }
    }

事件响应

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

增效视图

增效视图 (Visual Effect View, 对应类 NSVisualEffectView) 可以给视图增加透明化和毛玻璃视觉效果,通过下面两种方式设计实现增效视图

XIB

从控件工具箱中可以找到 Visual Effect View 控件,其属性面板中的属性包含

  • Blending Mode: 混合渲染模式,分为 Behind WindowIn Window 两种
    • Behind Window: 与当前窗口下面的内容产生混合渲染效果
    • In Window: 至于当前窗口中的内容产生混合渲染
      • 在这种情况下,增效视图所在的父视图必须使用层,即父视图的 wantsLayer 属性为 true
  • Material: 不同材质产生不同的视觉效果,有titlebar 、 selection 、menu 、 popover 、sidebar 、 light 、 dark 、 mediumLight 和 ultraDark 多种材质选择
  • State: 窗口的不同状态对应的风格,分为 ActiveInactive, 默认为 Active
  • Mask Image: 如果设置了 Mask Image ,则只在图像所在区域产生效果。

代码

代码设置属性与xib一致

  • e.g.

    import Cocoa
    
    class ViewController: NSViewController {
        
        lazy var effectVIew: NSVisualEffectView = {
            let effectView = NSVisualEffectView()
            effectView.wantsLayer = true
            effectView.material = .light
            effectView.state = .active
            effectView.blendingMode = .withinWindow
            return effectView
        }()
    
        override func viewDidAppear() {
            super.viewDidAppear()
            
            view.wantsLayer = true
            view.layer?.backgroundColor = NSColor.red.cgColor
            effectVIew.frame = view.bounds
            view.addSubview(effectVIew, positioned: .below, relativeTo: view.subviews.first)
        }
    }

滚动视图

在 macOS 中滚动视图主要包括3个视图互相写作的部分

  • 裁剪视图(Clip View,对应类是 NSClipView)
  • 滚动视图(Scroller,对应类是 NSScroller)
  • 需要滚动的文档视图(Document View,对应类 DocumentView)

用代码创建滚动条视图

  • e.g.

    func setScrollView() {
        
        let frame = CGRect(x: 0, y: 0, width: 200, height: 200)
        let scrollView = NSScrollView(frame: frame)
        scrollView.contentView.backgroundColor = NSColor.green
        
        guard let image = NSImage(named: NSImage.Name.init("rfwDB3L")) else { return }
        let imageViewFrame = CGRect(x: 0, y: 0, width: (image.size.width), height: (image.size.height))
        print("imageViewFrame: \(imageViewFrame)")
        let imageView = NSImageView(frame: imageViewFrame)
        
        imageView.image = image
        
        /// 我说之前怎么 .show 带不出来呢, 是has开头
        scrollView.hasVerticalScroller = true
        scrollView.hasHorizontalRuler = true
        scrollView.documentView = imageView
        
        view.addSubview(scrollView)
    }

滚动到指定的位置

NSClipView 提供了下面两个方法来实现视图滚动到指定的位置或一个矩形区域。

- (void)scrollPoint:(NSPoint)aPoint;
- (BOOL)scrollRectToVisible:(NSRect)aRect;

NSViewenclosingScrollView 属性可以获得视图的滚动条, 如果视图没有滚动条则 enclosingScrollView 为 nil

  • e.g.

    func scroll() {
        let contentView: NSClipView = scrollView.contentView
        var newScrollOrigin: NSPoint
        if view.isFlipped {
            newScrollOrigin = NSPoint(x: 0, y: 0)
        }
        else {
            newScrollOrigin = NSPoint(x: 0, y: 200)
        }
        contentView.scroll(to: newScrollOrigin)
    }

滚动条的显示控制

滚动条的 hasVerticalscrollerhasHorizontalRuler 分别用来控制是否显示纵向和横向的滚动条。如果设置他们为 false,只是不显示出来,并不是禁止滚动的行为。但是大多数情况下上述两个方法并不能真正实现滚动条的完全不显示,要做到完全不显示滚动条,需要重写滚动条类的 tile 方法,通过设置水平和垂直方向滚动条的 size 中的 widthheight 为0 来实现。

  • e.g.

    class NoScrollerScrollView: NSScrollView {
        
        override func tile() {
            super.tile()
            
            var hFrame = horizontalScroller?.frame
            hFrame?.size.height = 0
            if let hFrame = hFrame { horizontalScroller?.frame = hFrame }
            
            var vFrame = verticalScroller?.frame
            vFrame?.size.width = 0
            if let vFrame = vFrame { verticalScroller?.frame = vFrame }
        }
    }

如果要禁止一个方向的滚动,需要子类化 NSScrollView, 重载它的 scrollWheel 方法, 比如判断 y 轴方向的偏移量满足一定条件返回即可

import AppKit

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