昨天打着打着电话,突然玲就问我,macOS 能不能同时显示两个数字时钟,于是我就找了一圈,DashBoard 上虽然可以显示两个,但是都是模拟时钟,而且玲也没买触摸板,在桌面和 DashBoard 之间切换不那么方便。
就计划帮她写一个,于是“产品经理玲”就发给我了一个 prototype (゚o゚;;
![](/wp-content/uploads/2019/11/kimino.webp)
写,都可以写(x
于是结合产品经理玲和我的想法的话,就要自己来画 NSWindow 和它下面NSViewController 对应的 NSView
NSWindow 就自然是全部透明,无边框,这个很好实现~
importRyza class KiminoWindow: NSWindow { override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) setupWindow() } func setupWindow() { isOpaque = false backgroundColor = NSColor.clear isMovableByWindowBackground = true styleMask = .borderless } }
接下来就是 NSView 了,这里就先用 PaintCode 画一个接近玲想的 prototype
![](/wp-content/uploads/2019/11/kimino-paintcode.webp)
接下来就是把这个 NSView 显示的内容从模版里静态的文字、颜色,变成可以通过代码调整、自动更新的
importRyza class KiminoView: NSView { /// timezone var timeZone: String = "Asia/Chongqing" { didSet { self.update() } } /// font colour var clockColor: NSColor = NSColor(red: 0.946, green: 0.829, blue: 0.243, alpha: 1) { didSet { self.update() } } /// redraw func update() { self.setNeedsDisplay(self.frame) } /// draw clock interface /// /// - Parameter dirtyRect: rect override func draw(_ dirtyRect: NSRect) { // rectangle drawing NSGraphicsContext.saveGraphicsState() let rectanglePath = NSBezierPath(roundedRect: NSRect(x: 2, y: 0, width: 150, height: 55), xRadius: 10, yRadius: 10) // clock background NSColor(red: 0, green: 0, blue: 0, alpha: 0.916).setFill() rectanglePath.fill() NSGraphicsContext.restoreGraphicsState() // digits on the clock let textRect = NSRect(x: 2, y: 2, width: 150, height: 55) let formatter = DateFormatter() // 24 hrs formatter.dateFormat = "HH:mm" // specific timezone formatter.timeZone = TimeZone.init(identifier: timeZone) let textTextContent = formatter.string(from: Date()) let textStyle = NSMutableParagraphStyle() textStyle.alignment = .center let textFontAttributes = [ .font: NSFont(name: "digital-7", size: 63)!, .foregroundColor: clockColor, .paragraphStyle: textStyle, ] as [NSAttributedString.Key: Any] let textTextHeight: CGFloat = textTextContent.boundingRect(with: NSSize(width: textRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes).height let textTextRect: NSRect = NSRect(x: textRect.minX, y: textRect.minY + (textRect.height - textTextHeight) / 2, width: textRect.width, height: textTextHeight) NSGraphicsContext.saveGraphicsState() textRect.clip() textTextContent.draw(in: textTextRect.offsetBy(dx: 0, dy: 0), withAttributes: textFontAttributes) NSGraphicsContext.restoreGraphicsState() } }
接下来就在应用启动时添加第二个 NSWindow 就可以啦~
顺便想到也许会有需要将两个时钟放在最前的需要,于是就再做了个Status Bar的坑~
// // AppDelegate.swift // kimino // // Created byRyza on 8/11/2019. // Copyright © 2019Ryza. All rights reserved. // importRyza @NSApplicationMain class AppDelegate: NSWindowController, NSApplicationDelegate { var keepFrontMenuItem: NSMenuItem! var statusBarItem: NSStatusItem! var clockWindow: Array<NSWindow>! var isFloating: Bool! /// Button Action @objc func keepFrontAction() { // toggle floating isFloating = !isFloating // save user preference UserDefaults.standard.set(isFloating, forKey: "keepfront") // do the actual functionality keepFront() } /// Actual keep front functionality func keepFront() { // update diplayed status keepFrontMenuItem.title = "Keep Front: \(isFloating ? "On" : "Off")" // change window level accordingly if isFloating { self.clockWindow.forEach { window in window.level = .floating } } else { self.clockWindow.forEach { window in window.level = .normal } } } /// Quit this application @objc func quit() { NSApplication.shared.terminate(self) } /// Setup after application finishing launching /// /// - Parameter aNotification: unused func applicationDidFinishLaunching(_ aNotification: Notification) { // put the first clock window into `clockWindow` array clockWindow = [NSApplication.shared.windows[0]] // prepare and display the second clock window, London let storyboard = NSStoryboard(name: "Main", bundle: nil) let kiminoSecondaryWindowController = storyboard.instantiateInitialController() as! NSWindowController if let kiminoSecondaryWindow = kiminoSecondaryWindowController.window { let kiminoSecondaryView = kiminoSecondaryWindow.contentViewController?.view as! KiminoView // timezone, Europe/London kiminoSecondaryView.timeZone = "Europe/London" kiminoSecondaryView.clockColor = NSColor(red: 0, green: 0.628, blue: 1, alpha: 1) // bring front kiminoSecondaryWindow.orderFront(self) // append the second window to `clockWindow` array clockWindow.append(kiminoSecondaryWindow) } // place clock windows to top right placeWindow(clockWindow[0], 100, 100) placeWindow(clockWindow[1], 120, 40) // update every second Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in self.clockWindow.forEach({ window in let view = window.contentViewController?.view as! KiminoView view.update() }) } // Status Bar let statusMenu = NSMenu(title: "kimino") keepFrontMenuItem = NSMenuItem(title: "Keep Front", action: #selector(AppDelegate.keepFrontAction), keyEquivalent: "") keepFrontMenuItem.keyEquivalentModifierMask = .command keepFrontMenuItem.keyEquivalent = "L" let quiteMenuItem = NSMenuItem(title: "Quit", action: #selector(AppDelegate.quit), keyEquivalent: "") quiteMenuItem.keyEquivalentModifierMask = .command quiteMenuItem.keyEquivalent = "Q" statusMenu.addItem(keepFrontMenuItem) statusMenu.addItem(quiteMenuItem) statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) statusBarItem.menu = statusMenu statusBarItem.button?.image = NSImage(named: "kimino-status-bar") // restore user defaults isFloating = UserDefaults.standard.bool(forKey: "keepfront") keepFront() print("every day, every night, every dream") } /// Place NSWindow to specific postion with respect to the screen /// /// - Parameters: /// - window: window to be replaced /// - right: pixels to the right edge /// - top: pixels to the top edge func placeWindow(_ window: NSWindow, _ right: CGFloat, _ top: CGFloat) { if let screen = window.screen { let screenRect = screen.visibleFrame let newOriginX = screenRect.maxX - window.frame.width - right let newOriginY = screenRect.maxY - window.frame.height - top window.setFrameOrigin(NSPoint(x: newOriginX, y: newOriginY)) } } }
其实最后效果还是不错的(*^3^)
![](/wp-content/uploads/2019/11/kimino-time.webp)