MacOS学习(二) 常用组件

Demo地址

在了解这些控件之前,我们需要了解一下AppKit的坐标系。和UIKit中有所不同的是,AppKit的原点位于右下角。向上/右延伸。


Snip20171225_8.png
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

Snip20171228_28.png

来看看常见属性

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的样式


Snip20171228_30.png

下面看看常见属性

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
}
Snip20171228_20.png

我们在AppDelegate中返回Menu就是上面的红色框的位置的菜单,其他项目都是系统默认的,我暂时不知道如何隐藏,或者说能不能隐藏,不过想隐藏上面的`√Window可以通过这几个方法:

方法一

NSApp.mainWindow?.title = ""    // 去掉window.title

方法二


Snip20171228_21.png

方法三


Snip20171228_24.png

顶部左侧菜单(菜单栏)

NSApp.mainMenu?.items.first?.submenu?.addItem(withTitle: "666", action: #selector(menuItem_01Action(item:)), keyEquivalent: "")
Snip20171228_19.png

NSSlider

NSSlider的样式


Snip20171228_31.png

先看看常见属性

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

Snip20171229_34.png

这个就是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

Snip20171229_35.png

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)
}
Snip20171229_37.png

注意,如果你使用的是黑丝的菜单栏,有是使用黑色的icon,你可能会看不到你的Icon


Snip20171229_38.png

像这样,在不点击时时不会显示的,这时候需要设置一下图片的属性

let icon = NSImage.init(named: NSImage.Name(rawValue: "lightning"))
icon?.isTemplate = true // 设置这个属性后,系统会自动为图片取反,以适应菜单栏
statusItem.image = icon
Snip20171229_39.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351