视图是所有控件的基础,可以做为控件,也可以做为放置控件的容器,所以其功能也会相对复杂一些,它涉及到包括坐标管理、层级管理、绘制管理、事件响应等。
自定义视图
在开始正式内容之前,先创建一个简单的示例,步骤如下:
- 创建一个新项目,采用swift语言和xib界面;
- 在xib上添加一个名为 custom view 的视图控件;
- 新建一个名为 CustomView.swift类,继承NSView;
- 把添加的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)
}
}
}


750

被折叠的 条评论
为什么被折叠?



