+
+
Posts List
  1. 本章目标
  2. 新建工程及配置
  3. 创建菜单栏应用
    1. 选择程序图标
    2. 在菜单栏创建图标
    3. 隐藏打开时的Dock栏图标和窗口
    4. 设置点击打开Popover
    5. 配置Popover失去焦点后隐藏
    6. 左键打开Popover,右键打开菜单
  4. 参考资料

macOS新手开发教程(二)构建菜单栏应用

本章目标

  1. 一个呆在菜单栏的应用
  2. Dock中没有图标、打开时没有窗口
  3. 左键点击菜单栏图标时打开一个popover
  4. 右键点击菜单栏图标时打开一个菜单

新建工程及配置

在Xcode新建一个macOS APP工程,选择语言为Swift,使用Storyboard构建UI。

这样,我们就得到一个初始的项目,如果运行这个项目,将打开一个没有内容的窗口。

在导航区可以看到项目中包括两个文件夹,一个是与项目名同名的文件夹,其下是构建程序的源代码,另一个Products存放编译后的应用,如果将编译后的应用拖到Applications文件夹就算安装好了。

  • AppDelegate.swift 负责应用程序的生命周期
  • ViewController.swift 负责设置storyboard中的视图
  • Main.storyboard 视图
  • Assets.xcassets 保存媒体文件
  • Info.plist 信息属性配置文件
  • …entitlements 权限配置文件

创建菜单栏应用

选择程序图标

Iconfont-阿里巴巴矢量图标库里找一个中意的图标,下载18x18、36x36、54x54三种尺寸。然后Assets.xcassets中新建一个Image Set,更名为StatusBarButtonImage,再将下载好的三种尺寸图标分别拖入1x、2x、3x。在Attributes Inspector中选择Render As Template Image以适应系统的黑暗模式。

CleanShot 2020-01-25 at 13.56.11@2x

在菜单栏创建图标

先修改AppDelegate.swift,在class内创建一个菜单栏图标:

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

然后为这个菜单栏图标指定图标文件,在applicationDidFinishLaunching中写入:

1
2
3
if let button = statusItem.button {
button.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
}

运行后菜单栏就会显示一个黑色幽灵图标:

暗黑模式下则是白色幽灵:

隐藏打开时的Dock栏图标和窗口

Info.plist中增加一个键Application is agent (UIElement),设置其类型为Boolean,值为YES。目的是告诉Xcode这是一个只在菜单栏显示,而不在Dock栏显示的代理应用。

剩下的就是干掉打开时显示的窗口了。凡是显示的东西,肯定和UI有关,凡是和UI有关,大半和Main.storyboard有关。打开Main.storyboard,可以看到里面有一个Window Controller Scene,将其删除。

这时候编译运行,就能够得到一个只在菜单栏显示的图标了。

设置点击打开Popover

新建一个Cocoa Class,命名为PopoverViewController,它将用来控制Popover的逻辑。

接下来将视图和逻辑绑定。Storyboard.swift中已经有一个视图了,不过绑定的是ViewController.swift,只需要设置Custom Class和Storyboard ID为PopoverViewController,然后就会发现之前所有的View Controller都变成了Popover View Controller,现在ViewController.swift也可以删除了。

CleanShot 2020-01-25 at 15.10.05@2x

回到PopoverViewController.swift,在末尾增加:

1
2
3
4
5
6
7
8
9
10
11
12
13
extension PopoverViewController {
static func freshController() -> PopoverViewController {
//获取对Main.storyboard的引用
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
// 为PopoverViewController创建一个标识符
let identifier = NSStoryboard.SceneIdentifier("PopoverViewController")
// 实例化PopoverViewController并返回
guard let viewcontroller = storyboard.instantiateController(withIdentifier: identifier) as? PopoverViewController else {
fatalError("Something Wrong with Main.storyboard")
}
return viewcontroller
}
}

最后打开AppDelegate.swift,在class内声明一个Popover:

1
let popover = NSPopover()

再创建一个打开/关闭Popover的Toggle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@objc func togglePopover(_ sender: Any?) {
if popover.isShown {
closePopover(sender: sender)
} else {
showPopover(sender: sender)
}
}

funcshowPopover(sender: Any?) {
if let button = statusItem.button {
​popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
​}
}

funcclosePopover(sender: Any?) {
​popover.performClose(sender)
}

然后在applicationDidFinishLaunching内设置点击图标时调用togglePopover,现在的applicationDidFinishLaunching应该是这样:

1
2
3
4
5
6
7
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let button = statusItem.button {
button.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
button.action = #selector(togglePopover(_:))
}
popover.contentViewController = PopoverViewController.freshController()
}

编译运行后,点击菜单栏图标将出现一个Popover,再次点击关闭。

配置Popover失去焦点后隐藏

上面的Popover还有个问题,不再次点击图标就不关闭。加上Popover又永远显示在屏幕的最顶层,可以说是留着碍眼关闭又麻烦。我们希望在Popover失去焦点的时候能自动关闭。

我们需要创建一个事件监视器,监视是否有鼠标按下等用户操作,如果这些操作不是对Popover做的,就关闭Popover。

所以新建一个Swift文件,内容写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
}
}
}

然后打开AppDelegate.swift,将监视器应用到程序中,在class中声明:

1
var eventMonitor: EventMonitor?

然后在applicationDidFinishLaunching末尾添加:

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

这样还不够,因为监视器并没有打开,我们修改showPopoverclosePopover,让每次显示Popover时开启监视器,关闭Popover时关闭监视器。

1
2
3
4
5
6
7
8
9
10
11
funcshowPopover(sender: Any?) {
if let button = statusItem.button {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
eventMonitor?.start()
}

funcclosePopover(sender: Any?) {
popover.performClose(sender)
eventMonitor?.stop()
}

至此,一个简易的Popover程序就完成了,想在Popover中显示自定义内容,可以在Storyboard中拖入控件自行DIY。

左键打开Popover,右键打开菜单

trans作为一个翻译软件,将Popover设置为主界面,用来进行翻译。不过有了主界面还不够,一般的菜单栏应用,都是可以通过右键打开一个小菜单,进而选择设置、退出等。

我们先在Storyboard中创建一个Menu,将其拖入Application Scene。

CleanShot 2020-01-25 at 15.30.34@2x

然后将Menu与AppDelegate.swift建立联系,先打开Assistant:

然后按住control键,将Menu拖入AppDelegate中插入一个outlet,命名为menu。

之前我们为statusItem.button赋以togglePopover的动作。现在我们在class内新建一个Handler来接管togglePopover

1
2
3
4
5
6
7
8
9
10
11
@objc func mouseClickHandler() {
if let event = NSApp.currentEvent {
switch event.type {
case .leftMouseUp:
togglePopover(popover)
default:
statusItem.menu = menu
statusItem.button?.performClick(nil)
}
}
}

然后将之前对statusItem.button的设置更改为:

1
2
3
4
5
if let button = statusItem.button {
button.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
button.action = #selector(mouseClickHandler)
button.sendAction(on: [.leftMouseUp, .rightMouseUp])
}

这时候编译运行会发现,先点左键出现Popover,再点右键出现Menu,但是再点击左键还是Menu。这是因为一旦statusItem.menu = menu,图标的点击事件就被绑定锁死了。所以我们要在每次菜单关闭后statusItem.menu = nil,取消绑定。

AppDelegate.swift的末尾增加:

1
2
3
4
5
6
extension AppDelegate: NSMenuDelegate {
// 为了保证按钮的单击事件设置有效,menu要去除
func menuDidClose(_ menu: NSMenu) {
self.statusItem.menu = nil
}
}

applicationDidFinishLaunching内增加:

1
menu.delegate = self

再重新编译运行,就OK了。

参考资料

  1. Menus and Popovers in Menu Bar Apps for macOS | raywenderlich.com
  2. macOS 开发之状态栏小工具分别响应鼠标左右键单击 - 知乎
  3. macOS 开发之菜单栏形式的状态栏小工具 - 知乎

本文作者: rhinoc

本文链接: https://www.rhinoc.top/macos_2/

版权声明: 本博客所有文章除特别声明外,均采用BY-NC-SA 4.0国际许可协议,转载请注明。

打赏
Love U 3000
  • Through WeChat
  • Through Alipay