macOS利用NSWindowController创建窗口实现打开应用的实际案例
macOS利用NSWindowController创建窗口实现打开应用的实际案例

macOS利用NSWindowController创建窗口实现打开应用的实际案例

场景分析

在macOS中创建菜单栏时,我想要实现关闭(不退出)应用窗口后,点击状态栏的菜单按钮,实现打开应用窗口的效果。

import AppKit
import SwiftUI

class StatusBarController:ObservableObject {
    private var statusItem: NSStatusItem!
    
    init() {
        // 创建系统菜单栏图标
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        if let button = statusItem.button {
            button.image = NSImage(named: "templateIcon")
            button.toolTip = "轻压图片"
        }
        
        // 创建菜单
        let menu = NSMenu()
        
        let openItem = NSMenuItem(title: "打开 App", action: #selector(openApp), keyEquivalent: "o")
        openItem.target = self
        menu.addItem(openItem)
        
        menu.addItem(NSMenuItem.separator())
        
        let quitItem = NSMenuItem(title: "退出", action: #selector(NSApp.terminate(_:)), keyEquivalent: "q")
        menu.addItem(quitItem)
        
        statusItem.menu = menu
        
    }
    
    @objc func openApp() {
        print("打开 App")
        NSApp.activate(ignoringOtherApps: true)
    }
}

当关闭主窗口后,重新点击“打开App”菜单按钮时,并不会打开应用窗口。

问题分析

App启动后,点击“打开App”菜单按钮时,调用openApp方法,能够将App设置为活动应用。

当主窗口被关闭,再点击菜单按钮,调用openApp,实际上没有效果。

NSApp.activate(ignoringOtherApps: true) 确实激活了 App,但没有让主窗口重新出现。

在macOS中,当用户点击窗口左上角红叉关闭窗口时,应用仍在后台运行,但是窗口被销毁了。所以,调用NSApp.activate(…) 虽然激活了 App,但没有任何可见窗口出现。

因此,当窗口被关闭冲,如果想要通过状态栏的菜单按钮打开窗口,就需要主动创建窗口。

使用NSWindowController创建窗口

主动创建窗口:首先需要创建一个窗口控制器,通过NSApplication判断当前是否含有主窗口。

WindowManager 类:

import AppKit
import SwiftUI

class WindowManager {
    
    static let shared = WindowManager()
    
    var mainWindowController: NSWindowController?
    
    func showMainWindow() {
        if NSApp.windows.contains(where: {
            let className = NSStringFromClass(type(of: $0))
            return className.contains("AppKitWindow") || className.contains("NSWindow")
        }){
            print("找到 SwiftUI 主窗口")
            if let window = mainWindowController {
                window.showWindow(nil)
            } else {
                NSApp.activate(ignoringOtherApps: true)
            }
        } else {
            print("创建新的 Window 窗口")
            let window = NSWindow(
                contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
                styleMask: [.titled, .closable, .resizable],
                backing: .buffered,
                defer: false
            )
            
            window.title = "主窗口"
            window.center()
            window.isReleasedWhenClosed = false
            window.contentViewController = NSHostingController(rootView: ContentView())
            
            let controller = NSWindowController(window: window)
            controller.shouldCascadeWindows = true
            controller.showWindow(nil)
            self.mainWindowController = controller
        }
    }
}

在状态栏菜单代码中,调用WindowManager类的showMainWindow方法,从而实现创建新的窗口。

class StatusBarController:ObservableObject {
    ...
    
    @objc func openApp() {
        WindowManager.shared.showMainWindow()   // 调用方法
    }
}

当点击状态栏菜单并调用openApp方法后,会弹出自建的窗口。

代码解析

1、窗口控制器

class WindowManager {
    
    static let shared = WindowManager()
    
    var mainWindowController: NSWindowController?
    
    func showMainWindow() {

在这个代码中,创建了一个WindowManager的窗口管理类,并使用static创建单例模式。

创建一个mainWindowController变量(可选的NSWindowController类型)和showMainWindow方法。

mainWindowController变量用于存储一个 NSWindowController 类型的对象引用。

showMainWindow方法则用于判断并显示主窗口。

2、判断窗口是否存在

if NSApp.windows.contains(where: {
    let className = NSStringFromClass(type(of: $0))
    return className.contains("AppKitWindow") || className.contains("NSWindow")
}){
    print("找到 SwiftUI 主窗口")
    if let window = mainWindowController {
        window.showWindow(nil)
    } else {
        NSApp.activate(ignoringOtherApps: true)
    }
} else {
    print("创建新的 Window 窗口")

这是一段判断代码,从NSApp.windows获取全部的窗口。如果窗口类型中含有“AppKitWindow”或“NSWindow”,则表示当前的窗口没有关闭,可以展示出来。

这里可以在视图代码中遍历NSApp.windows了解到窗口类型:

for window in NSApp.windows {
    print("window:\(window)")
}

当首次打开应用时,NSApp.windows遍历的窗口信息为:

window:<NSStatusBarWindow: 0x127a2d2b0>
window:<SwiftUI.AppKitWindow: 0x12800a000>

这表示当前应用存在两个窗口,分别为 NSStatusBarWindow(状态栏)和SwiftUI.AppKitWindow(主窗口)。

当关闭主窗口后,重新使用NSWindowController创建主窗口,NSApp.windows遍历窗口信息:

window:<NSStatusBarWindow: 0x127a2d2b0>
window:<NSWindow: 0x12792f160>

可以看到,通过NSWindowController创建的主窗口类型是NSWindow,而不是SwiftUI.AppKitWindow。

所以,就需要根据NSApp.windows显示的各窗口类型来判断,如果类型包含“AppKitWindow”或“NSWindow”,就表示现在有打开的主窗口。然后,就可以将主窗口设置为活动窗口,应用窗口显示在最前面。

if NSApp.windows.contains(where: {
    let className = NSStringFromClass(type(of: $0))
    return className.contains("AppKitWindow") || className.contains("NSWindow")
})

如果判断有主窗口,再判断主窗口是不是通过NSWindowController创建的窗口,因为我们通过mainWindowController这个变量存储着 NSWindowController 类型的对象引用。

print("找到 SwiftUI 主窗口")
if let window = mainWindowController {
    window.showWindow(nil)
} else {
    NSApp.activate(ignoringOtherApps: true)
}

如果mainWindowController变量不为nil,则表示当前的主窗口是我们通过NSWindowController创建的窗口,那么我们就调用showWindow(nil)方法,显示窗口。

如果mainWindowController变量为nil,那么就说明当前的主窗口不是我们创建的NSWindowController窗口,我们就调用NSApp.activate()方法,激活主窗口。

3、创建NSWindowController窗口

当主窗口被关闭后,想要重新显示窗口。就需要通过NSWindowController创建新的窗口。

print("创建新的 Window 窗口")
let window = NSWindow(
    contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
    styleMask: [.titled, .closable, .resizable],
    backing: .buffered,
    defer: false
)

window.title = "主窗口"
window.center()
window.contentViewController = NSHostingController(rootView: ContentView())

let controller = NSWindowController(window: window)
controller.shouldCascadeWindows = true
controller.showWindow(nil)
self.mainWindowController = controller

使用NSWindow创建窗口,设置NSWindow的标题、居中,内容视图为ContentView视图。

初始化NSWindowController窗口控制器,设置级联为true,显示窗口。

将创建的NSWindowController窗口控制器的引用保存到mainWindowController变量中。

4、调用NSWindowController

最后,设置oepnApp方法,每次点击状态栏菜单项,就会调用WindowManager的showMainWindow方法。

class StatusBarController:ObservableObject {
    ...

    @objc func openApp() {
        WindowManager.shared.showMainWindow()   // 显示主窗口
    }
}

5、实现效果

当首次打开应用时,应用的窗口类型为SwiftUI.AppKitWindow,在状态栏中点击“打开App”,就会调用NSApp.activate()方法,将激活App。

如果把主窗口关闭,在状态栏中点击“打开App”,就会利用NSWindowController创建一个窗口。

总结

当点击Dock栏中的应用或从应用程序中打开应用时,窗口的生命周期是由 SwiftUI 的 @main App 机制管理。

当用户关闭窗口后,想要通过状态栏重新打开应用,就需要通过NSWindowController创建新的窗口。

此时窗口的类型从SwiftUI.AppKitWindow变成了NSWindow。

生命周期也从SwiftUI 的 @main App 机制变成由WindowManager类中的NSWindowController窗口控制器进行管理。

延伸问题(重复窗口问题)

虽然,现在可以实现窗口的创建,但实际上还是分成了NSWindowController创建和SwiftUI的@main App两种创建形式。

当通过NSWindowController创建窗口后,点击Dock栏的应用程序,就会创建新的窗口,这样就会存在重复打开窗口的问题。这两个窗口分别由NSWindowController和SwiftUI 的 @main App进行管理。

这个问题目前只有两种解决办法:

1、修改 App 主入口,把 WindowGroup 去掉或换成空场景:

@main
struct ImageSlimApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        // 空 Scene,窗口由 AppDelegate 管理
        Settings {} // 占位,不弹出任何窗口
    }
}

使用 AppDelegate 来唯一控制窗口创建。

class AppDelegate: NSObject, NSApplicationDelegate {
    var window: NSWindow!
    var statusBarController: StatusBarController?
    
    func applicationDidFinishLaunching(_ notification: Notification) {
        statusBarController = StatusBarController()
        
        let contentView = ContentView()
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 500, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable], // 可以调大小
            backing: .buffered,
            defer: false)
        
        window.center()
        window.isReleasedWhenClosed = false
        window.contentView = NSHostingView(rootView: contentView)
        
        window.makeKeyAndOrderFront(nil)
    }
    
    func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
        if !flag {
            window.makeKeyAndOrderFront(nil)
        }
        return true
    }
}

当点击底部Dock菜单栏的应用时,会调用AooDelegate的生命周期回调方法applicationShouldHandleReopen方法,,用于处理用户点击Dock图标时的行为。

因为配置了isReleasedWhenClosed,所以在SwiftUI中不会在关闭窗口后,销毁窗口。

func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool

sender:当前的 NSApplication 实例。

hasVisibleWindows(flag):当前是否有“可见”的窗口(如果所有窗口都被关闭了,就是 false)。

if !flag {
    window.makeKeyAndOrderFront(nil)
}
return true

所以,如果当前没有可见窗口(用户点击关闭窗口按钮,窗口被隐藏,App未推出),就调用window.makeKeyAndOrderFront(nil)显示窗口。

return true表示系统可以继续处理这个点击事件。

2、完全使用SwiftUI + WindowGroup,放弃NSWindowController。

这也就意味着,不去通过NSWindowController创建新的窗口,不在状态栏中新增“打开”。

相关文章

1、macOS核心对象NSApplication:https://fangjunyu.com/2025/06/19/macos%e6%a0%b8%e5%bf%83%e5%af%b9%e8%b1%a1nsapplication/

2、macOS应用代理AppDelegate和NSApplicationDelegate:https://fangjunyu.com/2025/06/25/macos%e5%ba%94%e7%94%a8%e4%bb%a3%e7%90%86appdelegate/

3、macOS管理窗口的控制器类NSWindowController:https://fangjunyu.com/2025/06/30/macos%e7%ae%a1%e7%90%86%e7%aa%97%e5%8f%a3%e7%9a%84%e6%8e%a7%e5%88%b6%e5%99%a8%e7%b1%bbnswindowcontroller/

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注