Demo地址
在了解这些控件之前,我们需要了解一下AppKit的坐标系。和UIKit中有所不同的是,AppKit的原点位于右下角。向上/右延伸。
NSView
先了解一下NSView中的常用的属性方法:
frame
:返回控件相对于父控件的位置(以上图为例:frame=(10, 10, 15, 10))
bounds
:返回控件相对于自身的位置(以上图为例:frame=(0, 0, 15, 10))
needsDisplay
:在当前控件需要重绘时,重新绘制当前控件
window
:返回当前控件所在的window对象
draw
(_:):绘制当前控件(这个方法一般极少被手动调用,我们一般使用needsDisplay)
NSView和UIView中最大的不同就是系统不会默认为其创建图层(layer),可能更对的是Apple对于性能的考虑,毕竟没有就不用绘制了┑( ̄Д  ̄)┍,减轻了C/GPU的压力。但是这就意味着我们办法像UIView中一样,肆意把玩layer,做各种动画什么的。不过苹果提供了一个属性--wantsLayer,当我们需要layer做一些事情的时候,只需要将其修改为true即可(默认是false)。
下面就是一段最常用的修改View背景颜色的代码:
let v = NSView.init()
v.frame = CGRect.init(x: 10, y: 10, width: 100, height: 100)
v.wantsLayer = true
v.layer?.backgroundColor = NSColor.yellow.cgColor
view.addSubview(v)
NSButton
NSButton和UIButton使用区别还是很大的,NSButton有很多的系统自带的样式,通过ButtonType和BezelStyle来设置。但是需要配合着使用,有的搭配是无效的。其实没什么好讲的,看一下Demo中的组合列表就可以了解了。
以下列出一些常用属性:
btn.title = title // 按钮文字
btn.image = image // 按钮图片
btn.action = selector // 按钮触发的方法
btn.alternateTitle = "" // 开启状态文字
btn.alternateImage = image // 开启状态图片
btn.state = .on // 按钮的状态
/**
noImage 不显示图片
imageOnly 仅显示图片
imageLeft 图片在文字左侧
imageRight 图片在文字右侧
imageBelow 图片在文字下方
imageAbove 图片在文字上方
imageOverlaps 图片和文字重叠
*/
btn.imagePosition = .imageBelow // 图文位置
btn.imageScaling = .scaleProportionallyDown // 设置图片缩放
btn.isBordered = true // 按钮是否有边框
btn.isTransparent = true // 按钮是否透明
// 以下设置的快捷键为: Shift + Command + I (如果设置的和系统的冲突,则不会触发)
btn.keyEquivalent = "I" // 快捷键
btn.keyEquivalentModifierMask = [.shift, .command] // 快捷键掩码
btn.highlight(true) // 按钮是否为高亮
NSImageView
在MacOS中,推荐ImageView只做展示,如果你要做用户交互,官方推荐使用NSButton。
/**
scaleProportionallyDown 原有尺寸
scaleAxesIndependently 图片按ImageView尺寸等比拉伸
scaleProportionallyUpOrDown 图片拉伸到ImageView尺寸
scaleNone 默认尺寸(原有)
*/
imageView.imageScaling = .scaleProportionallyDown // 图片缩放类型
/**
图片位置(imageScaling=scaleProportionallyUpOrDown时无效)
alignCenter 居中
alignTop 居中置顶
alignTopLeft 靠左置顶
alignTopRight 靠右置顶
alignLeft 居左
alignBottom 底部
alignBottomLeft 底部靠左
alignBottomRight 底部靠右
alignRight 居右
*/
imageView.imageAlignment = .alignBottom // 图片对齐方式
/**
none 无样式
photo 照片样式
grayBezel
groove
button
具体的样式可以修改值看看 - - 语言不好形容
*/
imageView.imageFrameStyle = .button // 边框样式
imageView.isEditable = true // 是否支持编辑(编辑,复制,剪切,拖拽等)
imageView.allowsCutCopyPaste = true // 图片支持剪切复制
imageView.animates = true // 支持动图
imageView.focusRingType = .none // 获取焦点时状态
// NSImageView编辑时(修改,拖拽等)触发的方法
imageView.target = self
imageView.action = #selector(imageViewAction(sender:))
imageFrameStyle
和背景色会产生冲突,优先级高于背景色
imageView.imageFrameStyle = .button
// 下面的代码不会生效
imageView.imageView.wantsLayer = true
imageView.layer?.backgroundColor = NSColor.yellow.cgColor
系统为我们提供了一些默认的图片可供使用:
imageView.image = NSImage.init(named: .touchBarMailTemplate) // NSImage.Name.
imageView.imageView.wantsLayer = true
imageView.layer?.backgroundColor = NSColor.yellow.cgColor
系统为我们提供了一些默认的图片可供使用:
```swift
imageView.image = NSImage.init(named: .touchBarMailTemplate) // NSImage.Name.
NSTextField
在MacOS中没有类似于iOS的UILable控件,而是使用NSTextField实现。
接下来我们先模拟出一个MacOS中的`UILabel`:
lbl.stringValue = "I`m value" // 设置文本
lbl.isEditable = false // 是否支持编辑
lbl.isBordered = false // 是否有边框
lbl.backgroundColor = NSColor.clear // 背景色
lbl.textColor = NSColor.black // 文字颜色
lbl.maximumNumberOfLines = 0 // 是否支持多行 0为不限制行数
其他的常用属性
lbl.placeholderString = "占位文字"
// 属性文字 这个会在后面系统研究 这里仅做了解
let attr = NSMutableAttributedString.init(string: "噜噜噜")
attr.addAttributes([NSAttributedStringKey.foregroundColor:NSColor.red], range: NSRange.init(location: 0, length: 2))
lbl.attributedStringValue = attr
// 限制文本格式 如果输入的文本与定义的格式不符,焦点会始终停留在该TextField上
let formatter = NumberFormatter.init()
formatter.numberStyle = .decimal
lbl.formatter = formatter
lbl.delegate = self
这里介绍一下NSTextFieldDelegate
的代理方法
// TextField 获取到焦点并开始编辑
override func controlTextDidBeginEditing(_ obj: Notification) {}
// TextField 文本发生变化
override func controlTextDidChange(_ obj: Notification) {}
// TextField 失去焦点结束编辑
override func controlTextDidEndEditing(_ obj: Notification) {}
// 验证内容 会在TextField失去焦点的时候触发
func control(_ control: NSControl, isValidObject obj: Any?) -> Bool {
// 监听 回车,删除,ESC 等 的输入
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {}
// 文本不符合规则时 是否允许失去焦点 true 允许 false 不允许
func control(_ control: NSControl, didFailToFormatString string: String, errorDescription error: String?) -> Bool
在iOS开发中,我们需要密码输入框只需要设置一个属性即可,但是在MacOS中,密码输入框是NSTextField的子类:NSSecureTextField
let slbl = NSSecureTextField.init(string: "123456")
slbl.frame = CGRect.init(x: 10, y: 100, width: 120, height: 40)
view.addSubview(slbl)
NSTextView
和NSTextField有很多类似的地方,我们先来看看他和NSTextField的区别
-
父类不同
NSTextField继承自NSControl
NSTextView继承自NSText
-
对特定键盘符号响应不同
-
Enter键
NSTextField 结束编辑
NSTextView 换行
-
Tab键
NSTextField 焦点进入下一个控件
NSTextView 退格
-
-
对特定符号显示不同
-
"
符号(以下的正常显示为中英文情况都可正常显示)NSTextField 可正常显示
NSTextView 英文的
"
会转换为中文的”
或“
-
总结来说,NSTextField提供的是简单的文本输入,而NSTextView提供更复杂的文本输入。
下面介绍一下NSTextView的常用属性
txtV.isAutomaticQuoteSubstitutionEnabled = false // 关闭自动转换引号
txtV.font = NSFont.systemFont(ofSize: 14) // 文字样式
txtV.textColor = NSColor.red // 文字颜色
txtV.backgroundColor = NSColor.yellow // 背景色
txtV.textContainerInset = NSSize.init(width: 10, height: 10) // 设置上下左右边距(width:左右 height:上下)
// 属性文字
let attr = NSMutableAttributedString.init(string: "噜噜噜")
attr.addAttributes([NSAttributedStringKey.foregroundColor:NSColor.red], range: NSRange.init(location: 0, length: 2))
txtV.textStorage?.setAttributedString(attr)
txtV.delegate = self // 代理
下面介绍一下NSTextViewDelegate代理的方法
// 监听文本改变
func textDidChange(_ notification: Notification) {}
// 监听 回车,删除,ESC 等 的输入
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {}
NSAlert
这个弹窗就是NSAlert
来看看常见属性
alertV.icon = NSImage.init(named: NSImage.Name(rawValue: "1")) // 弹出图片
alertV.messageText = "messageText" // 弹窗标题
alertV.informativeText = "informativeText" // 弹窗信息
/**
critical 警告
informational 描述
warning 严重
*/
alertV.alertStyle = .informational // 弹窗类型
alertV.showsHelp = true // 左下角显示帮助按钮
alertV.showsSuppressionButton = true // 显示默认的勾选按钮
// alertV.suppressionButton?.state 这个可以获取到勾选按钮状态
alertV.beginSheetModal(for: NSApp.mainWindow!) { (reutrnCode) in } // 用户点击弹窗按钮
// 添加按钮 注:当按钮过多,AlertView会自动增加宽度,按钮点击回调`reutrnCode`从1000开始计
alertV.addButton(withTitle: "巴扎黑")
// 我们可以自定义AlertView: alertV.window.contentView? 这个可以获取到弹框面板,但最好不要移除自带控件,否则会报约束错误,建议只做隐藏
NSAlertDelegate没有实现什么方法,只有一个监听点击帮助按钮的
func alertShowHelp(_ alert: NSAlert) -> Bool {}
NSPopover
这个弹窗就是Popover的样式
下面看看常见属性
popover.appearance = NSAppearance.init(named: .vibrantLight) // 弹窗样式
popover.contentViewController = popovc
/**
behavior在样式上没什么区别,只是对不同的用户操作有不同的响应
applicationDefined popover怎么拖拽点击都不会消失
semitransient 点击除contentViewController之外的区域,popover会消失,拖动窗口不会消失
transient 点击除contentViewController之外的区域,popover会消失,拖动窗口会消失
*/
popover.behavior = .transient
/**
计算方法
sender.(minX,minY,maxX,maxY) + sender.bounds.(minX,minY,maxX,maxY)
*/
popover.show(relativeTo: view.bounds, of: view, preferredEdge: .maxX)
介绍一下NSPopoverDelegate
// popover是否能被关闭
func popoverShouldClose(_ popover: NSPopover) -> Bool {}
// popover即将显示
func popoverWillShow(_ notification: Notification) {}
// popover已经显示
func popoverDidShow(_ notification: Notification) {}
// popover即将关闭
func popoverWillClose(_ notification: Notification) {}
// popover已经关闭
func popoverDidClose(_ notification: Notification) {}
// 拖拽popover能出现单独窗口 (一个又透明度的View)
func popoverShouldDetach(_ popover: NSPopover) -> Bool {}
// 拖拽popover能出现单独窗口 (拖拽后会展示返回的Window)
func detachableWindow(for popover: NSPopover) -> NSWindow? {}
NSMenu
先来实现一个按钮右键菜单
let rbtn = NSButton.init(title: "rBtn", target: self, action: #selector(rbtnAction(sender:)))
// 创建菜单
let menu = NSMenu.init(title: "menu_01")
// 创建菜单项
let menuItem_01 = NSMenuItem.init(title: "menu_01_01", action: #selector(menuItem_01Action(item:)), keyEquivalent: "")
let menuItem_02 = NSMenuItem.init(title: "menu_01_02", action: #selector(menuItem_02Action(item:)), keyEquivalent: "")
// 在菜单中添加菜单项
menu.addItem(menuItem_01)
menu.addItem(menuItem_02)
// 按钮的菜单指向我们创建的菜单
rbtn.menu = menu
接着是一个左键菜单
// 在btn上弹出cusMenu,NSApp.currentEvent表示用户当前触发的事件
NSMenu.popUpContextMenu(cusMenu, with: NSApp.currentEvent!, for: btn)
我们常见的Dock上的右键菜单
// 我们需要在AppDelegate中 添加
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
return dockMenu() // 这里返回的就是我们自定义的Menu
}
// dockMenu
func dockMenu() -> NSMenu {
// 一级主菜单
let m_1 = NSMenu.init(title: "m_1")
// 一级主菜单下的菜单项
m_1.addItem(withTitle: "m_1_I_1", action: #selector(m_1_I_1Action), keyEquivalent: "")
let m_1_I_2 = m_1.addItem(withTitle: "m_1_I_2", action: #selector(m_1_I_2Action), keyEquivalent: "")
// 二级子菜单
let m_1_I_1_m = NSMenu.init(title: "m_1_I_1_m")
m_1_I_2.submenu = m_1_I_1_m
// 二级子菜单下的菜单项
let m_1_I_2_M_I_1 = NSMenuItem.init(title: "m_1_I_1_m_I_1", action: #selector(m_1_I_2_M_I_1Action), keyEquivalent: "")
let m_1_I_2_M_I_2 = NSMenuItem.init(title: "m_1_I_1_m_I_2", action: #selector(m_1_I_2_M_I_2Action), keyEquivalent: "")
m_1_I_1_m.addItem(m_1_I_2_M_I_1)
m_1_I_1_m.addItem(m_1_I_2_M_I_2)
return m_1
}
我们在AppDelegate中返回Menu就是上面的红色框的位置的菜单,其他项目都是系统默认的,我暂时不知道如何隐藏,或者说能不能隐藏,不过想隐藏上面的`√Window可以通过这几个方法:
方法一
NSApp.mainWindow?.title = "" // 去掉window.title
方法二
方法三
顶部左侧菜单(菜单栏)
NSApp.mainMenu?.items.first?.submenu?.addItem(withTitle: "666", action: #selector(menuItem_01Action(item:)), keyEquivalent: "")
NSSlider
NSSlider的样式
先看看常见属性
slider.sliderType = .circular // 进度条样式(圆、线)
slider.isContinuous = true // 实时监听变化(原本是鼠标停止才触发action事件
slider.numberOfTickMarks = 5 // 分割为多少份
slider.tickMarkPosition = .above // 分割线位置
slider.allowsTickMarkValuesOnly = true // 只停留在标尺上
slider.minValue = 0 // 最小值
slider.maxValue = 100 // 最大值
slider.floatValue = 25.0 // 当前的值
slider.cell = CusSliderCell.init() // 当前Slider的视图
自定义NSSlider
class CusSliderCell: NSSliderCell {
// 1、自定义指示标识
override func drawKnob(_ knobRect: NSRect) {
// // 使用图片
// let image = NSImage.init(named: NSImage.Name(rawValue: "hand"))
// image?.draw(in: knobRect)
// 使用绘图方法绘制
NSColor.cyan.set()
let knobPath = NSBezierPath.init(ovalIn: knobRect)
knobPath.fill()
}
// 2、自定义标志器左右两边颜色
override func drawBar(inside rect: NSRect, flipped: Bool) {
NSColor.red.set()
let allPath = NSBezierPath.init(roundedRect: rect, xRadius: 2, yRadius: 2)
allPath.fill()
// 获取左边的区域
let w = CGFloat((doubleValue - minValue) / (maxValue - minValue)) * rect.width
var myRect = rect
myRect.size.width = w
let leftPath = NSBezierPath.init(rect: myRect)
NSColor.yellow.set()
leftPath.fill()
}
}
结合一下NSSlider和NSPopover
// 创建popover
let popo = NSPopover.init()
let popvc = CusPopoverVC.init()
popo.contentViewController = popvc
popo.behavior = .semitransient
// 创建slider
let slider = NSSlider.init(frame: .init(x: 10, y: 10, width: 200, height: 40))
slider.isContinuous = true
slider.minValue = 0
slider.maxValue = 100
slider.target = self
slider.action = #selector(sliderAction(slider:))
// 监听slider变化
@objc func sliderAction(slider: NSSlider) -> Void {
let strV = String.init(format: "%0.2lf", slider.floatValue)
if let txtF = ((popo?.contentViewController as? CusPopoverVC)?.valueTxtF) {
txtF.stringValue = strV
}
let radio = CGFloat.init(slider.floatValue) / CGFloat.init(slider.maxValue)
// Knob(那个点)的宽高都为21
let w = radio * (slider.bounds.width - slider.bounds.height)
let sliderRect = CGRect.init(x: w, y: -10, width: slider.bounds.height, height: slider.bounds.height)
popo?.show(relativeTo: sliderRect, of: slider, preferredEdge: .minY)
}
NSStatusBar & NSStatusItem
这个就是NSStatusBar,来看看怎么实现
// 在AppDelegate中添加
var iconItem: NSStatusItem? // 我们创建的Item必须被强引用,否则不会显示
func applicationDidFinishLaunching(_ aNotification: Notification) {
iconItem = statusIconItem()
}
// 构建一个StatusItem
func statusIconItem() -> NSStatusItem {
// NSStatusBar.system 获取系统的Bar 设置NSStatusItem的宽度
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
statusItem.highlightMode = true
// 设置StatusItem 图片
statusItem.image = NSImage.init(named: NSImage.Name(rawValue: "sunny_night"))
let statusMenu = NSMenu.init(title: "menu")
let statusMenu_I_01 = NSMenuItem.init(title: "statusMenu_I_01", action: #selector(statusMenu_I_01Action), keyEquivalent: "R")
let statusMenu_I_02 = NSMenuItem.init(title: "statusMenu_I_02", action: #selector(statusMenu_I_02Action), keyEquivalent: "T")
statusMenu.addItem(statusMenu_I_01)
statusMenu.addItem(statusMenu_I_02)
statusItem.menu = statusMenu
statusItem.toolTip = "I'm toolTip" // 鼠标悬停在NSStatusItem上会显示
statusItem.target = self
statusItem.action = #selector(statusItemAction(sender:)) // 设置点击方法 这里传入的是NSStatusBarButton对象
return statusItem
}
toolTip
NSStatusBar & NSStatusItem + NSPopover的练习
@objc func statusItemAction(sender: NSStatusBarButton) -> Void {
let popo = NSPopover.init()
popo.behavior = .semitransient
let popVC = NSStatusBarVC.init()
popo.contentViewController = popVC
popo.show(relativeTo: (sender.bounds), of: sender, preferredEdge: .minY)
}
注意,如果你使用的是黑丝的菜单栏,有是使用黑色的icon,你可能会看不到你的Icon
像这样,在不点击时时不会显示的,这时候需要设置一下图片的属性
let icon = NSImage.init(named: NSImage.Name(rawValue: "lightning"))
icon?.isTemplate = true // 设置这个属性后,系统会自动为图片取反,以适应菜单栏
statusItem.image = icon