NSView - ShenYj/ShenYj.github.io GitHub Wiki
视图 (View, 对应类 NSView) 是 AppKit 中控件的基础, 提供一下几个主要功能
- 作为容器防止各种控件
- 提供子类化的视图控件,方便快速开发
- 作为文本视图接收键盘输入
macOS 系统中的视图坐标系统原点(0, 0)在视角坐标系的左下角
如果想要让坐标系原点从左上角开始, 可以通过覆盖视图的
isFlipped
方法返回true
, 参考: AppKit Coordinates
与 iOS
基本保持一致,某些值类型要注意下类型:
CGSize -> NSSize
CGPoint -> NSPoint
NSView.frameDidChangeNotification
和 NSView.boundsDidChange
分别代表视图 frame
和 bounds
变化时的消息通知。
要接收通知,除了需要注册上述两个通知事件,还需要设置视图下面的两个属性为
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 }
NSView
的tag
属性是只读的,NSControl
子类实现了可读写的tag
属性
在经历过 iOS 开发出示 macOS 时的第一感觉就是常规 iOS 下哪些可读写属性都是只读的
与 iOS
平台无区别
视图本身没有提供背景、边框和圆角等属性,可以利用 layer
属性来控制这些效果。
使用
layer
属性前必须先设置wantsLayer
为true
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.red.cgColor
self.layer?.borderWidth = 2
self.layer?.cornerRadius = 10
第一次想给视图设置颜色的时候也是把我搞的头大了,或许这也是在推出 iOS 后改进了,习惯了 iOS 开发,对这种表示不方便
NSView的视图绘制也是调用 drawRect
方法实现的。对于 AppKit
中的各种界面控件,系统默认实现了不同控件的界面绘制和事件响应控制,如果要自定义控件的外观样式,可以在 drawRect
方法中实现界面绘制。
从性能方面考虑,系统对界面绘采用了延时绘制机制进行的。调用 setNeedsDisplay:
方法使当前视图或 Rect 定义的区域变为 invalidate
状态,并不是立即绘制,系统会在下一个绘制周期重绘。调用 display
、 displayRect
方法会强制视图立即重绘
-
下面的代码使用
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
之外绘制视图时,需要使用 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
) 可以给视图增加透明化和毛玻璃视觉效果,通过下面两种方式设计实现增效视图
从控件工具箱中可以找到 Visual Effect View
控件,其属性面板中的属性包含
-
Blending Mode
: 混合渲染模式,分为Behind Window
和In Window
两种-
Behind Window
: 与当前窗口下面的内容产生混合渲染效果 -
In Window
: 至于当前窗口中的内容产生混合渲染- 在这种情况下,增效视图所在的父视图必须使用层,即父视图的
wantsLayer
属性为true
- 在这种情况下,增效视图所在的父视图必须使用层,即父视图的
-
-
Material
: 不同材质产生不同的视觉效果,有titlebar 、 selection 、menu 、 popover 、sidebar 、 light 、 dark 、 mediumLight 和 ultraDark 多种材质选择 -
State
: 窗口的不同状态对应的风格,分为Active
和Inactive
, 默认为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;
NSView
的 enclosingScrollView
属性可以获得视图的滚动条, 如果视图没有滚动条则 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) }
滚动条的 hasVerticalscroller
和 hasHorizontalRuler
分别用来控制是否显示纵向和横向的滚动条。如果设置他们为 false
,只是不显示出来,并不是禁止滚动的行为。但是大多数情况下上述两个方法并不能真正实现滚动条的完全不显示,要做到完全不显示滚动条,需要重写滚动条类的 tile
方法,通过设置水平和垂直方向滚动条的 size
中的 width
和 height
为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)
}
}
}