如何在 macOS 的非文档型应用中添加选项卡?

12

我希望用Xcode 8中的Swift 3语言构建一个应用程序,并使用苹果标签栏。它不是基于文档的。我从这里了解到,如果我在窗口控制器中重写方法@IBAction func newWindowForTab(_ sender: Any?),就可以启用选项卡。为了测试这个功能,我在Xcode中创建了一个新项目,使用storyboard添加了NSWindowController的子类并在storyboard中分配了它。然后我实现了...

@IBAction override func newWindowForTab(_ sender: Any?) {}

应用程序构建时,选项卡栏会出现。 重新构建后,我注意到如果在构建之前关闭应用程序时未显示选项卡栏,则“+”按钮仅会出现一次。 这是一个错误吗? 我该如何添加新选项卡?


使用 addTabbedWindow(window: ordered:) 函数添加选项卡窗口。 - Peter Ahlberg
@Dis3buted 谢谢,但我无法按照自己的意愿使其正常工作。我基本上想要的行为就像Safari一样:当应用程序启动时,会打开某种默认窗口,并通过单击“+”按钮在选项卡中添加另一个默认窗口。因此,在新项目(非文档型)中,我对NSWindowController进行了子类化,并将其分配给Storyboard。在Storyboard中,我已经有了一个“View Controller Scene”,我希望每个新的选项卡式窗口都能出现。但是我不知道如何正确地覆盖newWindowForTab以使来自我的Storyboard的视图出现。 - DaPhil
4个回答

11

从概念上讲,发生了以下情况:

  • 只需调用NSWindow.addTabbedWindow(_:ordered:)即可将窗口添加到本机选项卡栏。
  • NSResponder.newWindowForTab(_:)放入主窗口的响应者链中后,"+"按钮就会可见。
  • 当您设置window.tabbingMode =.preferred时,选项卡栏将始终可见。

但是,在实现这些方法时存在一些注意事项。

实施newWindowForTab

那么何处添加@IBAction override func newWindowForTab(_ sender: Any?)以便您可以调用NSWindow.addTabbedWindow(_:ordered:)

  • 如果使用Storyboards,则将其放入您拥有的NSWindowController子类中。 这是获取NSWindow以调用addTabbedWindow的最简单方法。
  • 如果使用Xib,则AppDelegate将引用主窗口。 您可以在此处放置该方法。
  • 如果使用编程设置,请将其放置在您知道的主要NSWindow实例所在的位置。

使“+”按钮起作用

TL;DR:当您初始化新窗口时,请将窗口的windowController存储在某个位置。 为了防止窗口事件(在控制器中)被处理,您需要维护强引用。

我编写了一个带有TabManager的示例应用程序来处理此问题:https://github.com/DivineDominion/NSWindow-Tabbing

以及详细信息的博客文章:https://christiantietze.de/posts/2019/07/nswindow-tabbing-multiple-nswindowcontroller/

考虑事件如何分派。主菜单消息沿响应者链发送,newWindowForTab也是如此。如果调用源没有连接到最后——至少要连接到您的NSWindowController,甚至可能要连接到您的AppDelegate——则NSApp.sendAction将无法处理标准事件。

您必须确保添加的任何其他窗口实际上都是与原始窗口相同的响应者链的一部分,否则菜单项将停止工作(并且变灰色/禁用)。 同样,当单击它时,“+”按钮停止工作。

这就是其他答案的评论中@JohnV所说的:“没有子视图变量,您无法创建多个选项卡”的效果。 这是效果,但不是真正的解释。 您始终可以创建更多选项卡,但只能从原始窗口/选项卡创建,而不是从新窗口创建; 这是因为另一个选项卡未响应newWindowForTab

“其他标签”本身只是NSWindow。 您的newWindowForTab实现位于控制器中。 那是上一级。

通过@Peter Ahlberg的代码进行调整,这将起作

class WindowController: NSWindowController {

    @IBAction override func newWindowForTab(_ sender: Any?) {

        let windowController: WindowController = self.storyboard?.instantiateInitialController() as! WindowController
        let newWindow = windowController.window

        self.window?.addTabbedWindow(newWindow, ordered: .above)

        newWindow.orderFront(self.window)
        newWindow.makeKey()

        // Store the windowController in a collection of sorts
        // to keep a strong reference and make it handle events:
        // (NSApp.delegate as? AppDelegate).addManagedWindowController(windowController)
    }
}

我不需要在AppDelegate中添加newWindowForTab就可以使用Storyboard使一切正常工作——因为这种方式窗口控制器可以继续工作而不需要回退!


1
很好的回答,但共享控制器会让处理插座变成一场噩梦。我一直在尝试你的方法,直到我找到了一篇很棒的文章,解释了如何为每个选项卡使用单独的控制器。结果发现这篇博客文章也是你写的!请更新答案,至少包含一个指向你的文章的链接:https://christiantietze.de/posts/2019/07/nswindow-tabbing-multiple-nswindowcontroller/ - kontiki
哇,你说得对。谢谢!我已经重新表述了答案。 - ctietze
你好,我使用了你的代码库,但是出现了问题。窗口无响应。但是感谢你的解决方案,它很有效。请问你能回答我的问题吗?https://stackoverflow.com/questions/69752377/cocoa-multi-tab-is-unresponsive-after-creation-of-new-tab-in-swift - Vin
@Vin,如果您能提供更多详细信息(例如macOS版本等),那就太好了,我们可以通过在GitHub上开一个问题来解决这个问题。 - ctietze
1
@ctietze,没问题,这是macOS的Bug。Finder应用程序也会出现相同的问题。 - Vin

8

好的,这里有些新文件,

Appdelegate

(应用程序代理)

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

func applicationDidFinishLaunching(_ aNotification: Notification) {
    // Insert code here to initialize your application
}

func applicationWillTerminate(_ aNotification: Notification) {
    // Insert code here to tear down your application
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
}

@IBAction func newWindowForTab(_ sender: Any?){
} // without this the + button doesn't show from start

}

视图控制器

import Cocoa

class ViewController: NSViewController {

override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
}

override var representedObject: Any? {
    didSet {
    // Update the view, if already loaded.
    }
}
}

以及WindowController

import Cocoa

class WindowController: NSWindowController {

var subview: WindowController?

override func windowDidLoad() {
    super.windowDidLoad()
    // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
}

@IBAction override func newWindowForTab(_ sender: Any?) {

    let story = self.storyboard
    let windowVC: WindowController = story?.instantiateInitialController() as! WindowController

    self.window?.addTabbedWindow(windowVC.window!, ordered: .above)
    self.subview = windowVC

    windowVC.window?.orderFront(self.window)
    windowVC.window?.makeKey()
}

}

您需要在菜单视图中添加菜单项并将其连接到新标签页的FirstResponder,分配键,说cmd+t即可使用。这个例子只是从“+”按钮添加选项卡,并且窗口菜单选项“移动选项卡到新窗口”和“合并所有窗口”可用。您可以将选项卡拖出并放回,水平移动选项卡。

看起来它工作正常。

使用Xcode版本8.2 beta (8C30a)完成。


2
为什么要将新加载的 windowVC 存储到子视图变量 subview 中?只是好奇。 - Eimantas
玩一下它,尝试不使用,创建和销毁子视图。 - Peter Ahlberg
谢谢!我也很好奇为什么你要像@Eimantas一样将新加载的windowVC存储在subview变量中。对我来说没有它也完全可以工作(xcode 9.2)。 - JohnV
1
搞定了 - 没有 subview 变量,你无法创建超过两个选项卡。再次感谢。 - JohnV
1
subview 保证了新窗口的控制器不会被释放。如果您不存储该对象,它将被垃圾回收。由于 windowVC 与事件处理程序是相同类型的,因此您可以设置 windowVC.window?.windowController = self,并仍然使“+”按钮和所有菜单项正常工作。请参阅下面的我的回答。 - ctietze
1
不要忘记在窗口的身份检查器中将类设置为WindowController - user1531352

0
请注意,如果在应用程序委托中实现newWindowForTab(而不使用NSWindowController),我发现以下方法效果最佳:
@IBAction func newWindowForTab(_ sender: Any?) {
    let newWindow = NSWindow(...)
    ...
    app.mainWindow?.addTabbedWindow(newWindow, ordered: .above)
    newWindow.makeKeyAndOrderFront(sender)
}

mainWindow是当前活动的NSWindow。当mainWindow不存在时,它会简单地创建一个新的非标签窗口,在没有窗口的情况下非常有用。


0

如果上述“+”符号总是在第一个窗口后添加新标签,如果您关闭第一个窗口,则会在当前窗口下重新创建它。

有方法可以使其正常工作。

override func newWindowForTab(_ sender: Any?) {
    let wc: NSWindowController = self.storyboard?.instantiateInitialController() as! NSWindowController
    wc.window?.windowController = self
    NSApplication.shared.mainWindow!.addTabbedWindow(wc.window!, ordered: .above)
    wc.window?.orderFront(self)
}

在 NSApplication.shared.mainWindow 中的技巧!您始终将选项卡添加到当前活动窗口。

如果我们需要在最后创建窗口,我们应该使用这个技巧

let tabbedWindows = NSApplication.shared.mainWindow!.tabbedWindows!
let lastTabIdx = tabbedWindows.count - 1
tabbedWindows[lastTabIdx].addTabbedWindow(wc.window!, ordered: .above)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接