Skip to content

NSWindow

ShenYj edited this page Dec 29, 2021 · 4 revisions

NSWindow

.

创建好macOS的App后,默认产生的Main.storyboard含有了三个层次:

Application SceneWindow Controller SceneView Controller Scene

Mac App标准结层次构:Application(WindowController→Window)(ViewController→View)

窗口 NSWindow 简介

应用UI视图的容器, 负责接收用户的鼠标、键盘等系统事件,转发消息到相关的接收者对象。

.

AppKit提供的一些子类化的窗口还可以实现一些辅助的交互功能,比如文件打开/保存的对话框、字体颜色选择器等

活动窗口与非活动窗口

.

每个应用程序启动后至少会打开一个窗口。运行多个应用时,屏幕上会打开多个窗口。我们把当前用户正在工作的应用的窗口成为活动窗口,其他应用的窗口相应的成为非活动窗口

相对于iOS应用,我们更多的情况下屏幕上只显示一个应用(不考虑复杂场景)

可以接收输入事件(键盘、鼠标、触控板等外部设备)的窗口对象成为键盘窗口(Key Window),当前的活动窗口也成为主窗口(Main Window)。一个时刻只能有一个键盘窗口(响应键盘鼠标等外设的窗口)和一个主窗口,又可以是不同的窗口。

比如当主窗口可以接收输入事件时,它同时是键盘窗口

更多介绍可以阅读苹果文档Window Layering and Types of Windows

面板 NSPanel

面板是一种特殊的窗口,执行一些辅助功能,常用来作为一些警告确认框、用户输入信息等对话框。

NSPanel 类型的窗口不能作为主窗口,只能作为键盘窗口。

一些常用的子类有 NSColorPanel(颜色选择)、NSFontPanel(字体选择)、NSSavePanel(保存打开文件),这些子类化的窗口都只能作为键盘窗口。

窗口界面的组成

窗口界面包括: 标题栏、内容视图和底部边框区组成

.

属性面板区域部分参数说明

  • Window

    • Show Title Bar: 窗口顶部标题栏的显示与隐藏,默认显示
    • Hide Title Text: 勾选后隐藏窗口的标题
    • Transparent Title Bar: 将标题栏设置为透明样式
  • Controls

    • Close、Minimize、Resize: 这就是macOS应用最常见的窗口左侧那三个按钮
  • Behavior

    • Restorable: 表示是否允许保存窗口的当前状态,下次运行时可以恢复之前的状态。比如记住窗口的上一次的位置。
    • Visible At Launch: 通过 xib 创建的窗口这个属性默认是勾选的,标识窗口在加载 xib文件时自动显示出来。如果当前 xib 文件上有多个窗口需要程序控制显示, 需要将这个属性勾选去掉。

模态窗口 Modal Window

当有多个窗口在屏幕上时,用户可以点击切换到任意一个窗口上。有些特殊场景需要限制用户必须处理完当前窗口的任务,完成任务后关闭它才能继续操作其他的窗口,这种窗口成为模态窗口(Modal Window)

e.g. 在视图上有一个按钮, 点击弹出一个模态窗口的示例代码

import Cocoa

class ViewController: NSViewController {
    
    private lazy var modalWindow: NSWindow = {
        let rect = NSRect.init(origin: .zero, size: CGSize(width: 200, height: 120))
        let styleMask: NSWindow.StyleMask = [.titled, .closable, .resizable]
        let window = NSWindow(contentRect: rect,
                              styleMask: styleMask,
                              backing: NSWindow.BackingStoreType.buffered,
                              defer: false)
        window.title = "modal window"
        window.center()
        window.isReleasedWhenClosed = false
        return window
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        NotificationCenter.default.addObserver(self, selector: #selector(windowClose), name: NSWindow.willCloseNotification, object: nil)
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    @IBAction func tap(_ sender: NSButton) {
        NSApplication.shared.runModal(for: modalWindow)
    }
    
    
    @objc func windowClose() {
        NSApplication.shared.stopModal()
    }
}

NSApplication.shared.stopModal()方法是用来结束模态状态的。
如果用户直接点击窗口左上角的关闭按钮,此刻虽然窗口关闭了,但是整个应用仍然处于模态状态,任何操作都无法得到相应。因此需要监听窗口关闭事件,增加结束模态的方法的调用。

此处为了便于代码的阅读, 写在了ViewController下, 监听模态的结束,可以放在AppDelegate

默认window被关闭后会自动被释放, 当你关闭模态状态再次展示这个懒加载的window时会crash, 关闭子自动释放内存isReleasedWhenClosed后就可以重新的模态展示了

func applicationDidFinishLaunching(_ aNotification: Notification) {
    // Insert code here to initialize your application
    NotificationCenter.default.addObserver(self, selector: #selector(windowClose), name: NSWindow.willCloseNotification, object: nil)
}

@objc func windowClose() {
    NSApplication.shared.stopModal()
}

在其他资料中,在NSApplication.shared.runModal(for: window)后直接执行了关闭window.close(),进行模态展示后直接调用close()并不会将其关闭,事实上,当一个窗口以 runModel(for:) 的方式显示出来之后,应用会进入一个模态过程。这个动作相当于启动了一个阻塞的线程,启动方法调用之后的所有代码都会被阻塞住,只有在调用了 stopModel 停止 模态过程 后,代码才会继续执行。

模态会话窗口 Modal Session

比起模态窗口,模态会话窗口方式创建的窗口对系统事件响应的限制稍微小一些, 允许响应快捷键和系统菜单,比如字体颜色选择面板。

结束模态会话窗口根模态窗口一样需要注册通知来处理关闭事件,保证结束会话状态

窗口的使用

创建窗口

使用 NSWindow类创建窗口对象,除了frame参数,还需要指定styleMask来确定窗口的样式风格, 如上面那段示例代码中

let styleMask: NSWindow.StyleMask = [.titled, .closable, .resizable]
func createWindow() {
    let frame = CGRect(x: 0, y: 0, width: 400, height: 280)
    let style: NSWindow.StyleMask = [.titled, .closable, .resizable]
    let window = NSWindow(contentRect: frame, styleMask: style, backing: .buffered, defer: false)
    // 设置标题
    window.title = "New Create Window"
    // 显示窗口
    window.makeKeyAndOrderFront(self)
    // 居中
    window.center()
}

参数说明

  • styleMask控制窗口风格的参数
    • borderless: 没有顶部标题栏和控制按钮
    • titled: 有顶部标题栏边框
    • closable: 带有关闭按钮
    • miniaturizable: 带有最小化按钮
    • resizable: 带有恢复按钮
    • texturedBackground: 带纹理背景窗口
    • unifiedTitleAndToolbar: 窗口的标题栏按钮区和窗口顶部的标题区融合为一体
    • fullScreen: 全屏显示
    • fullSizeContentView: 内容视图占据整个窗口大小
    • utilityWindow: NSPanel类型的窗口
    • docModalWindow: 模态文档,NSPanel类型窗口
    • nonactivatingPanel: 一种非活动主应用NSPanel类型窗口,点击这种面板不会导致主应用窗口从活动状态变为费活动状态
    • hudWindow: HUD黑色风格窗口,只有NSPanel类型窗口支持
  • backing 窗口绘制的缓存模式
    • retained: 兼容老系统参数,基本很少使用
    • nonretained: 不缓存直接绘制
    • buffered: 缓存绘制
    • defer: 表示延迟创建还是立即创建

窗口通知

  • didBecomeKeyNotification: 窗口成为键盘窗口
  • didBecomeMainNotification: 窗口成为主窗口
  • didMoveNotification: 窗口移动
  • didResignKeyNotification: 窗口不再是键盘窗口
  • didResignMainNotification: 窗口不再是主窗口
  • didResizeNotification: 改变窗口大小
  • willCloseNotification: 关闭窗口
  • willMiniaturizeNotification: 窗口最小化

其他通知事件可在NSWindowNotifications下查阅

contentView

通过xib 设计窗口,直接从控件库拖上去就可以了。在运行过程中要动态增加view 元素到窗口的话,可以借助window 的 contentView,它代表了窗口的根视图。

这与iOS 开发中Cell 的设计有些类似

通常有三种方法可以改变window 的contentView

  • 直接增加View 控件到contentView

    let frame = CGRect(x: 10, y: 10, width: 80, height: 18)
    let textField = NSTextField(frame: frame)
    textField.stringValue = "动态插入元素"
    NSApplication.shared.keyWindow?.contentView?.addSubview(textField)
  • 使用自定义的NSView 或NSViewController 的view 替换contentView

    let vc = NSViewController(nibName: nil, bundle: nil)
    self.window.contentView = vc.view
    let view = NSView()
    self.window.contentView = view
  • 对于macOS 10.10及以后的系统,创建一个NSViewController 或子类,可以实例化后赋值给窗口的contentViewController

    let vc = NSViewController()
    self.window.contentViewController = vc

设置窗口的image和title

  • 窗口的标题由Title 属性直接设置即可

  • image 需要先设置窗口的representedURL后在设置standardWindowButton来设置image

    window.representedURL = URL(string: "WindowTitle")
    let image = NSImage(named: NSImage.Name.init("logo"))
    window.standardWindowButton(.documentIconButton)?.image = image

    效果图

    .

设置窗口的背景颜色

刚开始接触NSWindow时, 我还是按照iOS习惯的方式去设置contentView或view的颜色,发现行不通

其实NSWindow如果需要设定背景色,直接设置backgroundColor属性就行了

window.backgroundColor = NSColor.green

效果图

.

关闭窗口时终止应用

默认情况下当你关闭最后的窗口后发现dock上应用进程还在, 如果需要保证关闭最后一个窗口或者关闭应用唯一的窗口时应用自动退出, 有两种方式

  • 在AppDelegate 中增加如下代码

    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
  • 监听通知,判断关闭的窗口为当前主程序window 时执行关闭应用

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application
        NotificationCenter.default.addObserver(self, selector: #selector(windowClose(notification:)), name: NSWindow.willCloseNotification, object: nil)
    }
    
    @objc func windowClose(notification: Notification) {
        if let window = notification.object {
            if window as? NSObject == NSApplication.shared.keyWindow {
                NSApp.terminate(self)
            }
        }
    }
    

在窗口标题区域增加视图

通过获取contentView 的父视图,可以将提示信息或操作按钮(如注册按钮)增加到窗口顶部区域,提醒用户操作

func addButtonToTitleBar() {
    let titleView = modalWindow.standardWindowButton(.closeButton)?.superview
    let button = NSButton()
    let x = (modalWindow.contentView?.frame.width)! - 100
    let frame = CGRect(x: x, y: 0, width: 80, height: 24)
    button.frame = frame
    button.title = "Register"
    button.bezelStyle = .roundRect
    titleView?.addSubview(button)
}

效果图

.

居中显示

  • 调用窗口的center 方法

    由于窗口有历史记忆功能,会记住上次应用运行时退出前的frame位置, 因此需要现在界面中通过代码设置它的isRestoreable属性为false

    self.window?.isRestorable = false
    self.window?.center()
  • 在视图控制器的viewDidAppear方法中获取当前view的窗口属性对象后进行处理

    override func viewDidAppear() {
        super.viewDidAppear()
        self.view.window?.isResizable = false
        self.view.window?.center()
    }

控制窗口的显示位置

调用窗口的setFrame方法可以控制窗口的位置,如果希望通过代码控制窗口的位置,同样也必须关闭isRestorable属性

self.window?.isRestorable = false
self.window?.setFrame(CGRect(x: 0, y: 0, width: 100, height: 100), display: true)

应用关闭后点击Dock菜单再次打开应用

AppDelegate中实现协议方法,在防范重重新打开窗口

    func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
    self.window.makeKeyAndOrderFront(self)
    return true
}

总结

窗口的注意点

  • 任何窗口的关闭要么通过点击左上角的系统关闭按钮,要么通过代码执行窗口的close方法来关闭
  • 对于任何一种模态窗口,关闭后还必须额外调用结束模态的方法去结束状态。如果点击了窗口的左上角关闭按钮,而没有执行结束模态的方法,整个应用仍然处于模态状态,应用的其他窗口无法正常工作

一般情况下很少需要单独创建和管理窗口对象。

窗口的创建都是基于项目场景模板创建的,或者通过WindowController 创建和管理的。

  • 新建一个项目,工程自动生成MainMenu.xib中会包含一个窗口对象,这个窗口是由AppDelegate 管理的
  • 新建一个项目,勾选Create Document-Based Application,自动生成的Document.xib 会包含一个窗口对象,它是由NSDocument 类来管理的
  • 新建一个NSWindowController 的子类WindowController, 勾选使用xib, 自动生成的WindowController.xib 中包含一个窗口对象

实际项目开发中,推荐创建一个NSWindowController类,然后调用它的showWindow方式来显示

外链

Getting Started

Social

Clone this wiki locally