适用于macOS的菜单栏应用

菜单栏上的应用程序早已为macOS用户所熟知。 其中一些应用程序具有“正常”部分,其他应用程序仅位于菜单栏中。
在本指南中,您将编写一个应用程序,该应用程序在弹出窗口中显示名人的一些报价。 在创建此应用程序的过程中,您将学到:

  • 在菜单栏中分配应用程序图标
  • 使应用程序仅托管在菜单栏上
  • 添加自定义菜单
  • 根据用户的要求显示一个弹出窗口,并在必要时使用事件监视将其隐藏

注意:本指南假定您熟悉Swift和macOS。

开始使用


启动Xcode。 接下来,在File / New / Project ...菜单上,选择macOS / Application / Cocoa App模板,然后单击Next

在下一个屏幕上,输入Quotes作为产品名称 ,选择您的组织名称组织标识符 。 然后,确保已选择Swift作为应用程序语言,并选中了使用Storyboards复选框。 取消选中“ 创建基于文档的应用程序” ,“ 使用核心数据” ,“ 包含单元测试”和“ 包括UI测试”复选框



最后,再次单击下一步 ,指定保存项目的位置,然后单击创建
创建新项目后,打开AppDelegate.swift并将以下属性添加到类中:

let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength) 

在这里,我们在菜单栏中创建状态项(应用程序图标),固定长度对用户可见。

然后,我们需要将图片分配给菜单栏中的该新项目,以便区分新应用程序。

在项目导航器中,转到Assets.xcassets, 上传图片并将其拖动到资产目录中。

选择图片并打开属性检查器。 将“ 渲染为”选项更改为“ 模板图像”



如果您使用自己的图像,请确保该图像是黑白图像,并将其配置为“ 模板”图像,以使该图标在深色和浅色菜单栏上看起来都很好。

返回AppDelegate.swift ,并将以下代码添加到applicationDidFinishLaunching(_ :)

 if let button = statusItem.button { button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) button.action = #selector(printQuote(_:)) } 

在这里,我们将刚刚添加的应用程序图标分配给应用程序图标,并在单击它时分配操作

将以下方法添加到该类:

 @objc func printQuote(_ sender: Any?) { let quoteText = "Never put off until tomorrow what you can do the day after tomorrow." let quoteAuthor = "Mark Twain" print("\(quoteText) — \(quoteAuthor)") } 

此方法只是将报价单打印到控制台。

注意objc方法指令 。 这使您可以使用此方法作为对按钮单击的响应。

生成并运行该应用程序,您将在菜单栏中看到新的应用程序。 万岁!
每次单击菜单栏中的图标时,Xcode控制台中都会显示著名的Mark Twain谚语。

我们将主窗口和图标隐藏在扩展坞中


在直接处理功能之前,我们需要做一些小事情:

  • 删除停靠图标
  • 删除不必要的应用程序主窗口

要删除停靠图标,请打开Info.plist 。 添加一个新的Application is agent(UIElement)键,并将其值设置为YES



现在是时候处理主应用程序窗口了。

  • 打开Main.storyboard
  • 选择“ 窗口控制器”场景并将其删除
  • 查看Controller场景假,我们将尽快使用它




生成并运行该应用程序。 现在,应用程序在扩展坞中既没有主窗口又没有不必要的图标。 太好了!

将菜单添加到状态项


对于认真的应用程序,单击响应显然是不够的。 添加功能的最简单方法是添加菜单。 在AppDelegate的末尾添加此功能。

 func constructMenu() { let menu = NSMenu() menu.addItem(NSMenuItem(title: "Print Quote", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "P")) menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "Quit Quotes", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) statusItem.menu = menu } 

然后在applicationDidFinishLaunching(_ :)的末尾添加此调用

 constructMenu() 

我们创建NSMenu ,向其中添加3个NSMenuItem实例,并将此菜单设置为应用程序图标菜单。

一些要点:

  • 菜单项的标题是出现在菜单中的文本。 一个本地化应用程序的好地方(如有必要)。
  • 动作 (如按钮或其他控件的动作)是用户单击菜单项时调用的方法
  • keyEquivalent是键盘快捷键,可用于选择菜单项。 小写字符使用Cmd作为修饰符,小写字符使用Cmd + Shift 。 仅在应用程序位于最顶部且处于活动状态时,此方法才有效。 在我们的情况下,必须显示菜单或其他窗口,因为我们的应用程序在扩展坞中没有图标
  • spacerItem是处于非活动状态的菜单项,其形式为其他元素之间的灰线。 用它来分组
  • printQuote是您已经在AppDelegate中定义的方法,而终止NSApplication定义的方法。

启动应用程序,然后单击应用程序图标将看到一个菜单。



尝试单击菜单-选择“ 打印报价”会在Xcode控制台中显示报价,而“ 退出报价”将终止应用程序。

添加一个弹出窗口


您已经看到从代码中添加菜单是多么容易,但是在Xcode控制台中显示引号显然不是用户对应用程序的期望。 现在,我们将添加一个简单的视图控制器,以正确的方式显示引号。

转到File / New / File ...菜单,选择macOS / Source / Cocoa Class模板,然后单击Next



  • 将类命名为QuotesViewController
  • 成为NSViewController的继承者
  • 确保未选中“ 同时为用户界面创建XIB文件”复选框
  • 将语言设置为Swift

最后,再次单击“ 下一步” ,选择一个保存文件的位置,然后单击“ 创建”
现在打开Main.storyboard 。 展开View Controller Scene,然后选择View Controller实例



首先选择Identity Inspector并将类更改为QuotesViewController ,然后将Storyboard ID设置为QuotesViewController

现在,将以下代码添加到QuotesViewController.swift文件的末尾:

 extension QuotesViewController { // MARK: Storyboard instantiation static func freshController() -> QuotesViewController { //1. let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) //2. let identifier = NSStoryboard.SceneIdentifier("QuotesViewController") //3. guard let viewcontroller = storyboard.instantiateController(withIdentifier: identifier) as? QuotesViewController else { fatalError("Why cant i find QuotesViewController? - Check Main.storyboard") } return viewcontroller } } 

这是怎么回事:

  1. 我们有一个指向Main.storyboard的链接。
  2. 创建一个与我们刚在上方安装的场景标识符匹配的场景标识符
  3. 创建QuotesViewController的实例并返回它。

您创建此方法后,现在使用QuotesViewController的每个人都不需要知道如何创建它。 它只是工作。

注意guard语句中的fatalError 。 最好使用它或assert assertFailure,以便在开发中出现问题时,您自己和开发团队的其他成员都可以知道。

现在回到AppDelegate.swift 。 添加一个新的属性。

 let popover = NSPopover() 

然后,将pplicationDidFinishLaunching(_ :)替换为以下代码:

 func applicationDidFinishLaunching(_ aNotification: Notification) { if let button = statusItem.button { button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) button.action = #selector(togglePopover(_:)) } popover.contentViewController = QuotesViewController.freshController() } 

您已更改click操作,以调用togglePopover(_ :)方法,稍后我们将进行编写。 另外,我们没有配置和添加菜单,而是配置了一个弹出窗口,该弹出窗口将显示QuotesViewController中的内容

将以下三种方法添加到AppDelegate

 @objc func togglePopover(_ sender: Any?) { if popover.isShown { closePopover(sender: sender) } else { showPopover(sender: sender) } } func showPopover(sender: Any?) { if let button = statusItem.button { popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) } } func closePopover(sender: Any?) { popover.performClose(sender) } 

showPopover()显示一个弹出窗口。 您只需指出它的来源,macOS便将其定位并绘制一个箭头,就像它从菜单栏中出现一样。

closePopover()仅关闭弹出窗口,而togglePopover()是根据其状态显示或隐藏弹出窗口的方法。

启动应用程序,然后单击其图标。



一切都很好,但是内容在哪里?

我们实现Quote View Controller


首先,您需要一个用于存储引号和属性的模型。 转到File / New / File ...菜单,然后选择macOS / Source / Swift File模板 ,然后选择Next 。 将文件命名为Quote ,然后单击Create

打开Quote.swift文件,并向其中添加以下代码:

 struct Quote { let text: String let author: String static let all: [Quote] = [ Quote(text: "Never put off until tomorrow what you can do the day after tomorrow.", author: "Mark Twain"), Quote(text: "Efficiency is doing better what is already being done.", author: "Peter Drucker"), Quote(text: "To infinity and beyond!", author: "Buzz Lightyear"), Quote(text: "May the Force be with you.", author: "Han Solo"), Quote(text: "Simplicity is the ultimate sophistication", author: "Leonardo da Vinci"), Quote(text: "It's not just what it looks like and feels like. Design is how it works.", author: "Steve Jobs") ] } extension Quote: CustomStringConvertible { var description: String { return "\"\(text)\" — \(author)" } } 

在这里,我们定义了一个简单的报价结构和一个返回所有报价的静态属性。 由于我们使Quote符合CustomStringConvertible协议,因此我们可以轻松地获取格式方便的文本。

有进步,但是我们仍然需要控件来显示所有这一切。

添加界面元素


打开Main.storyboard,然后拉出视图控制器上的3个按钮( Push Button )和标签( Multiline Label)

放置按钮和标签,使它们看起来像这样:



将左按钮以20的间距连接到左边缘,并垂直居中。
将右按钮以20的间距连接到右边缘,并垂直居中。
将底部按钮以20的间距连接到底部边缘,并水平居中。
将标记的左右边缘与按钮之间的间距为20,垂直居中。



您将看到几个布局错误,因为没有足够的信息可以自动布局解决它。

将“ 水平内容拥抱优先级”设置为249,以允许调整标签的大小。



现在执行以下操作:

  • 将左按钮图像设置为NSGoLeftTemplate并清除标题
  • 将右按钮图像设置为NSGoRightTemplate并清除标题
  • 将下面按钮的标题设置为“ 退出报价”
  • 将标签的文本对齐方式设置为居中。
  • 检查标签上的“换行符”是否设置为“自动换行”


现在打开QuotesViewController.swift并将以下代码添加到QuotesViewController类的实现中:

 @IBOutlet var textLabel: NSTextField! 


将此扩展添加到类实现中。 现在在QuotesViewController.swift中有两个类扩展。

 // MARK: Actions extension QuotesViewController { @IBAction func previous(_ sender: NSButton) { } @IBAction func next(_ sender: NSButton) { } @IBAction func quit(_ sender: NSButton) { } } 

我们仅添加了用于显示引号的标签的插座 ,以及将与按钮连接的3个存根方法。

将代码与Interface Builder连接


注意:Xcode在您的代码左侧-IBActionIBOutlet关键字旁边放置了圆圈。



我们将使用它们将代码连接到UI。

按住Alt键的同时,在项目导航器中单击Main.storyboard 。 因此, 情节提要板在右侧的助手编辑器中打开,在左侧的代码中打开。

textLabel左侧的圆圈拖到界面构建器中的标签上。 以相同的方式,分别将上一个下一个退出方法与左,右和下按钮组合。



启动您的应用程序。



我们使用了默认的弹出窗口大小。 如果您想要更大或更小的弹出窗口,只需在情节提要中调整其大小即可。

为按钮编写代码


如果您尚未隐藏助手编辑器 ,请单击Cmd-Return或V iew>标准编辑器>显示标准编辑器

打开QuotesViewController.swift并将以下属性添加到类实现中:

 let quotes = Quote.all var currentQuoteIndex: Int = 0 { didSet { updateQuote() } } 

quotes属性包含所有引号, currentQuoteIndex是当前显示的引号的索引。 CurrentQuoteIndex还具有一个属性观察器,以在索引更改时用新的引号更新标签的内容。

现在添加以下方法:

 override func viewDidLoad() { super.viewDidLoad() currentQuoteIndex = 0 } func updateQuote() { textLabel.stringValue = String(describing: quotes[currentQuoteIndex]) } 

当视图加载时,我们将引号索引设置为0,这又导致对接口的更新。 updateQuote()只是更新文本标签以显示报价。 对应的currentQuoteIndex

最后,使用以下代码更新这些方法:

 @IBAction func previous(_ sender: NSButton) { currentQuoteIndex = (currentQuoteIndex - 1 + quotes.count) % quotes.count } @IBAction func next(_ sender: NSButton) { currentQuoteIndex = (currentQuoteIndex + 1) % quotes.count } @IBAction func quit(_ sender: NSButton) { NSApplication.shared.terminate(sender) } 

next()previous()方法在所有引用中循环。 退出关闭应用程序。

启动应用程序:



事件监控


用户还希望我们的应用程序做一件事-当用户单击窗口外的某个地方时,隐藏弹出窗口。 为此,我们需要一种名为macOS全局事件监视器的机制。

创建一个新的Swift文件,将其命名为EventMonitor ,并将其内容替换为以下代码:

 import Cocoa public class EventMonitor { private var monitor: Any? private let mask: NSEvent.EventTypeMask private let handler: (NSEvent?) -> Void public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) { self.mask = mask self.handler = handler } deinit { stop() } public func start() { monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) } public func stop() { if monitor != nil { NSEvent.removeMonitor(monitor!) monitor = nil } } } 

初始化此类的实例时,我们将为它传递一个将要监听的事件掩码(例如击键,鼠标滚轮等)和一个事件处理程序。
当我们准备开始监听时, start()调用addGlobalMonitorForEventsMatchingMask(_:handler :) ,它返回我们正在保存的对象。 一旦掩码中包含的事件发生,系统就会调用您的处理程序。

要停止监视事件,请在stop()中调用removeMonitor () ,然后通过设置为nil来删除该对象。

我们剩下的就是在正确的时间调用start()stop() 。 该类还会在反初始化程序上调用stop()进行清理。

连接事件监视器


上一次打开AppDelegate.swift并添加一个新属性:

 var eventMonitor: EventMonitor? 

然后在applicationDidFinishLaunching(_ :)的末尾添加此代码以配置事件监视器

 eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in if let strongSelf = self, strongSelf.popover.isShown { strongSelf.closePopover(sender: event) } } 

当您单击向左或向右按​​钮时,这将通知您的应用程序。 请注意:处理程序将不会响应您在应用程序内部的鼠标单击而被调用。 这就是为什么当您在弹出窗口中单击时弹出窗口不会关闭的原因。

我们对自身使用引用,以避免在AppDelegateEventMonitor之间形成强链接循环的危险。

showPopover(_ :)方法的末尾添加以下代码:

 eventMonitor?.start() 

当弹出窗口出现时,我们开始监视事件。

现在,将代码添加到closePopover(_ :)方法的末尾:

 eventMonitor?.stop() 

当弹出窗口关闭时,我们在这里结束监视。

该应用程序已准备就绪!

结论


在这里,您将找到该项目的完整代码。

您已经了解了如何在菜单栏上的应用程序中设置菜单和弹出窗口。 为什么不尝试使用多个标签或带格式的文本来更好地查看引号? 还是连接后端以接收来自Internet的报价? 还是要使用键盘在引号之间导航?

研究的好地方是官方文档: NSMenuNSPopoverNSStatusItem

Source: https://habr.com/ru/post/zh-CN447754/


All Articles