Skip to content

Latest commit

 

History

History
2005 lines (2004 loc) · 74.5 KB

Windows and Window Controllers in OS X Tutorial.md

File metadata and controls

2005 lines (2004 loc) · 74.5 KB

Windows250x250

window是相应于全部OS X app的UI的“容器”。它们定义了你的app当前负责的屏幕的区域,用户可以使用容易理解的多任务范式(multi-tasking paradigm)来进行交互。OS X app总是落入如下种类中的一种:

  • 单窗口的工具 ,就像计算器
  • 单窗口,库风格的“鞋盒 ,就像Photos
  • 多窗口基于文档的 ,就像TextEdit

不管是哪种类型的app,几乎每种OS X app都利用了 MVC (Model-View-Controller),一种核心的设计模式。

在Cocoa中,window是 NSWindow 类的一个实例,相应的控制器对象则是 NSWindowController 类的实例。在一个有着很好设计的app中,你通常(typically)都会使用一对一关系的window和它的controller。这个模型层会根据你app类型和设计各有不同。

在这个OS X教程中的window和window controller,你将创建 BabyScript ,一个多窗口的基于文档的app,它的灵感来自于TextEdit。在这个过程中,你将学到:

  • Window 和 window controller
  • 文档的架构
  • NSTextView
  • 模型window
  • 菜单栏和菜单项

预备知识

这篇教程是面向新手的。话虽如此,它要求下列的基础知识:

  • Swift
  • Xcode,特别的,storyboards
  • 创建一个简答的 Mac (OS X) app

如果你对上面的任意一项都不熟悉,你可能要复习(brush up)一些这个网站上的其它教程:

开始吧!

运行 Xcode ,并选择 File / New / Project… 。选择 OS X / Application / Cocoa Application ,然后点击 Next

1-CocoaApp

在下一屏,填写如下所示的字段,但输入你自己的名字(或超级英雄的别名),而不要像我一样。

2-XcodeTemplate

点击 Next 并保存你的项目。

运行项目,你将看到:

3-FirstWindow

为了打开更多的文档,选择 File / New 。全部的文档都位于 同一位置 ,因此你只有在点击和拖拽他们时,才会看到顶部的文档。这不是理想的效果,所以把它添加到你的to-do列表中,但先不要钻进去。

4-Open-Many

也是可以使用 Windows 菜单来讲window带到前台。

5-Bring-ToFront

文档:在Hood之下

现在你已经看到了它在行动,让我们花几分钟来看看它的工作原理。

文档结构

文档是在内存中的数据的容器,你可以在window中查看它。最终,它可以从磁盘或iCould中写入和读出。从程序上讲,文档是一个 NSDocument 类的实例,充当数据对象的控制器 - 也就是说 - 相应于文档的模型。

在文档架构中的其它的两个主要的类是 NSWindowcontroller NSDocumentController 。这些是它们的角色:

  • NSDocument :穿件,展示及保存文档数据
  • NSWindowController :管理展示文档的那个window
  • NSDocumentController :管理app中全部的文档对象

还是可视化比较好,所以这里有一个表,来展示这些类如何在一起工作:
DocArchitecture

禁用文档的保存和打开

文档的建构也提供了文档的保存和打开机制。

Document.swift 中,你会发现实现为空的 dataOfType ,它是为了写入的,还有 readFromData ,是为了读出的。保存和打开文档是在这个教程的范围之外的,因此你要做出一些变化来避免令人困惑的行为的出现。

Document.swift 中, 移除 autosavesInPlace

  override class func autosavesInPlace() -> Bool {
    return true
  }

现在你将禁掉全部关于打开和保存的菜单项,但是在你行动之前,注意到全部你所期望的功能早已在这里了。例如,选择 File / Open 并find的对话框,包括控制器,侧边栏,工具栏等等已经都在这里了:

OpenDialog

一个菜单项,当没有定义动作时是无用的。相同的禁用的效果同样也发生在,当响应者链中没有相应的能够响应那个动作selector的对象时。

因此,你将断开需要禁用的,为菜单项定义的动作。

no-saving-for-you

在storyboard中,在 Main Menu Application Scene ,选择 File / Open

选择 Connections Inspector 并点击 Open 。正如你看到的,它通过 openDocument selector连接到了第一响应者,也就是在响应者链中,第一个响应这个selector的对象。就像下面这样,通过点击 x 来删除这个连接:

TargetAction

Save Save As Revert to Saved 重复该步骤。

运行项目,切换到 Open 菜单,并检查它是否看起来像下面这样:

7-OpenMenu

Window的位置

当你运行BabyScript的时候,window会在靠近左边,但稍低于屏幕中心的的位置打开。

为何它选择了这个位置?

找到storyboard,在 outline view 中选择 Window ,然后选择 Size Inspector 。运行BabyScript - 或将它带到前面 - 你现在看到的屏幕应该是下面这样的:

5-WindowSizeInspector

Initial Position 下输入 X Y 的数值。这是设置window的位置的一种方法。你也可以通过拖动下面灰色的矩形直观地(graphically)设置它。

6-QuartzCoordinates

注意 :在Cocoa中,一个可见对象(window, view, control等)的原点位于 左下 角。在向上和向右的方向上,坐标值会增加。

相反地,在很多图形环境下,尤其是iOS,其原点是位于 左角的,则在向下和向右的方向上,坐标值会增加。

假设你期望你的window打开的位置,是从左上角水平和垂直偏移200个点。你可以用Xcode在window中的 Size Inspector 来设置它,或依靠编程来实现。

使用Interface Builder来设置window的位置

基本可以打赌,用户会在各种尺寸的屏幕上运行BabyScript。由于你的app没有一个水晶球,能在编译时看到它将打开在一个怎样的屏幕尺寸上,因此Xcode使用了一个虚拟的屏幕尺寸,和一个类似于自动布局的概念,来在运行时决定window的位置。

为了设置这个位置,你将用到 Initial Position 下的两个值 X Y ,和两个下拉式(drop-down)菜单。

找到 Initial Position ,依据屏幕坐标系来设置window的打开位置。给 X Y 都输入200,并在下拉式菜单中选择 Fixed from Left Fixed from Bottom 。这就设置了window在x和y的方向上都是200个点的偏移。

运行项目,你应当看到:

跟随下面的步骤来让window定位到左上角:

  1. 在预览图中拖拽灰色的矩形到虚拟屏幕的左上角 - 这就改变了初始的位置。
  2. X 处输入200,在 Y 这里,输入最大的值“负200”,在这个case中就是557.
  3. 在底部的下拉菜单中选择 Fixed from Top

下面右侧的图片展示了你应该输入的地方和值:

Window757557

注意 :OS X在每次启动时会记忆window的位置。为了查看你做的变化,你需要确保关掉了app的window - 而不仅仅是重新运行。

关掉所有的窗口,然后运行项目。

10-Window200x200Xcode

编程设置window的位置

现在你要完成和刚刚在Interface Builder中所完成的一样的工作,但这一你要通过编程来完成。

采取“硬的方式”(hard way)的原因有两个。第一,你可以对 NSWindowController 有一个更好的理解。第二,它是一种更灵活和很直接的方式。

在运行时,app将在得知屏幕尺寸之后执行窗口最终的位置。

Project Navigator 中选择 BabyScript 组,然后选择 File / New / File.. 。从弹出的对话框中,选择 OS X / Source / Cocoa Class 并点击 Next.

创建一个名叫 WindowController 的新的类,让它作为 NSWindowController 的子类。不要勾选 XIB ,将 Language 设为 Swift

11-WindowController

选择一个位置来保存新的未见。之后,你会看到一个新的名叫 WindowController.swift 的文件出现在了 BabyScript 组中。

找到storyboard,在 Outline View 中从 Window Controller Scene 选择 Window Controller 。选择 Identity Inspector ,再从 Class 下拉菜单中选择 WindowController

12-WindowController-2

windowDidLoad 方法被调用时,这个window就完成了从storyboard加载的过程,因此这是你的任何配置都会覆盖在storyboard中的设置。

打开 WindowController.swift 并用下列代码替换 windowDidLoad 方法:

  override func windowDidLoad() {
    super.windowDidLoad()
    if let window = window, screen = window.screen {
      let offsetFromLeftOfScreen: CGFloat = 20
      let offsetFromTopOfScreen: CGFloat = 20
      let screenRect = screen.visibleFrame
      let newOriginY = CGRectGetMaxY(screenRect) - window.frame.height
        - offsetFromTopOfScreen
      window.setFrameOrigin(NSPoint(x: offsetFromLeftOfScreen, y: newOriginY))
    }
  }

这个逻辑将window的位置定位在了其左上角距离屏幕左上角的位置上。

正如你看到的, NSWindowController 有一个 window property,而 NSWindow 一个 screen property。你可以使用这两个property来访问window和屏幕的几何信息(geometry)。

在确定了(ascertaining)屏幕的高度之后 ,你的window的frame就被减去了期望的偏移。记住Y的值是随着你在屏幕中向上移而增大的。

visibleFrame 排除了被dock和菜单栏占用的区域。如果你不把这个纳入考量,你可能最终会让dock模糊掉了你的window的一部分。

当你隐藏了dock和菜单的时候, visibleFrame 可能仍然会比 frame 小,因为系统会持有了一个小的边界区域来当展示dock时进行检测。

运行项目,这个window应该被设定为,距离屏幕左上角的每个方向都是20个点的位置上。

Cascading window

为了进一步提升你的window的位置,你将采用 Cascading Windows ,意思是整理多个互相重叠的window的排列,让每个window的标题栏可以被看到。

WindowController.swift 中, WindowController 定义下面添加下列代码:

  required init?(coder: NSCoder) {
    super.init(coder: coder)
    shouldCascadeWindows = true
  }

通过覆盖 NSWindowController required init 方法,你将 NSWindowController shouldCascadeWindows property设置为true。

运行app,然后打开5个window。你的屏幕看起来应该就更友好些了:

13-CascadingWindows

将BabyScript制成一个迷你的文字处理器

现在到了这个教程最令人兴奋的部分。只需 行代码并添加一个 NSTextView 的控制到你window的 contentView 上,你将添加激动人心(blow your mind)的功能!

The Content View

一个window在创建时,自动地创建了两个view:一个不透明的带有边界、标题栏等的框架view,和一个通过window的 contentView property访问的透明content view。

content view是一个window的view图层的根,你可以用一个定制的view来替换这个默认的。注意,你必须使用 NSWindow setContentView 方法来定位content view - 你不可以用标准的 NSView setFrame 方法来定位它。

ContentView

注意 :如果你是一个iOS的开发者,请注意在Cocoa中, NSWindow 不是 NSView 的子类!在 iOS 中, UIWindow UIView 的一个特殊的子类。 UIWindow 它自己是view图层的根,就由它来扮演content view的角色。

添加Text View

在storyboard的 contentView 中,通过选择并按 delete 键, 移除 那个说“Your document contents here”的text field。

为了创建将要构成你的UI的主要部分的新的 NSTextField ,跟随下列的说明:

  1. 在storyboard中,打开 Object Library
  2. 搜索 nstextview
  3. 拖动 Text View 并将其放到content view上。
  4. 调整text view的尺寸,让它的每边都距离content view相应边20个点。
  5. Outline View 中,选择 Bordered Scroll View 。注意到text view是被嵌入到了 Clip View 中,Clip View则被嵌入到了一个scroll view中。
  6. 选择 Size Inspector 。输入 X Y 均为20, Width 为440, Height 为230。

15-TextViewCreation

运行项目 - 你应该看到如下这样:

16-EmptyText

看起来那么友好,闪烁的文字插入点邀请你输入你的文本!开始你的宣言(manifesto),或仅仅让它保持简单的“Hello World”,然后选择文本。用 File / Copy command – C 来拷贝它,然后粘贴几次,感受这个app!

探索 Edit Format 菜单来获得有用的idea。你可能会注意到 Font / Show Fonts 是被禁用的。现在你就来打开它!

启用字体面板

在故事版中,找到主菜单,点击 Format 菜单,然后是 Font ,然后点击 Show Fonts

找到 Connections Inspector ,你会发现这个菜单项中没有动作被定义。这就解释了为何菜单项被禁用了,但是你该将它连接到何处?

显然,这个动作早已被定义在了Xcode没有直接导入的代码中,你只是需要连接一下。

右击 Show Fonts 并将其拖动到 Application Scene First Responder 上,然后释放鼠标。一个带有全部动作的小型的可滚动的列表window就会弹出。找到并选择 orderFrontFontPanel 。你也可以输入这个名称以便更快地找到它。

17-ConnectFontPanel

现在,选择 Show Fonts 来看一下 Connections Inspector 。你会看到,现在这个菜单已经被连接到了在响应者链中响应 orderFrontFontPanel 这个selector的第一响应者。

运行app,然后输入一些文本并选择它。选择 Format / Font / Show Fonts 来打开字体面板。调整(Play with)字体面板右侧的垂直的滑动条,观察文本的大小如何实时地变化。

18-FontPanel

等等,你甚至都没有输入一行关于text view的代码,它就有了改变文本大小的能力。你太棒了!

Word Processing...Like A Boss

使用富文本(Rich Text)来初始化text view

为了看到这个app全部的能力,请从 这里 下载一些格式化的文本,并使用它作为这个text view的初始化文本。

使用TextEdit打开它,全选并拷贝到剪贴板中。找到storyboard,选择Text View,然后 Attributes Inspector ,然后将文本粘贴到 Text Storage 中。

20-RichText

运行项目,你应该会看到:

21-EditMe

使用自动布局

你确实可以滚动不适合当前window大小的文本,但尝试一下改变window的尺寸吧。

哎呀!text view不能随着window尺寸的改变而改变。

TextNoGrow

使用 自动布局 ,这是一个非常简单的修复。

注意 :自动布局在你的Cocoa和iOS app的UI中都可以使用。它创建了一套规则来定义元素之间的几何关系,而你以约束的形式来定义这些关系。

使用自动布局,你可以创建一个动态的界面,以恰当地相应屏幕大小、window大小、设备方向和本地化的变化。

关于自动布局还有更多的内容,但为了本教程的缘故,所有你需要做的只是跟随下面的步骤 - 你可以在之后学习更多关于Auto Layout的内容。这里有一些很好的教程供查看:在iOS 7中开始自动布局教程, 第一部分 第二部分

在storyboard的 Outline View 中,选择 Bordered Scroll View ,点击画布右下角的 Pin 按钮。

分别点击四个小红色的条的约束;那个“破碎的凋谢的红色”就会转变成“实的红色”。点击底部的叫做 Add 4 Constraints 的按钮。

PinTextView

运行项目,观察window和text view怎样一起改变大小:

22-AutoLayoutFixed

默认地展示尺子

为了在window打开时自动地展示尺子,你需要在代码中有一个 IBOutlet 。从菜单中选择 Format / Text / Show Ruler 。在 ViewController.swift 中,添加一行代码方法 toggleRuler viewDidLoad 中,并添加一个 IBOutlet 在这个方法之上,就像下面这样:

  @IBOutlet var text: NSTextView!
 
  override func viewDidLoad() {
    super.viewDidLoad()
    text.toggleRuler(nil)
  }

现在你将在storyboard中,连接text view到view controller上。

在storyboard中,右击 ViewController ,不放手并拖拽到text view直到它变得高亮,然后放开鼠标。你将会看到一个带有 Outlets 列表的小的window。选择 text 的outlet:

23-TextConnect

运行项目,现在这个window就会默认id展示尺子:

RulerShowing

就像我承诺的,仅靠两行代码和storyboard,你就创建了一个小的迷你文字处理器 - 致敬(Chapeau),苹果!

模态Window

你可以让window运行在模态方式下。这个window仍然使用app的标准时间循环,但输入仅限于模态window。

有两种方式来利用模态窗口。你可以调用 NSApplication 的方法 runModalForWindow 。这种方式为指定的窗口垄断(monopolizes)了事件,直到它得到了一个你可以通过 stopModal abortModal stopModalWithCode 来调用的请求才停止。

对于这个case,你将使用 stopModal 。另一种方式被称作 模态session ,不会包含在本教程中。

添加一个字符统计window

你可以添加一个模态窗口来在活动的window中统计字数和段落数。由于会关联于一个指定的窗口和指定的状态,它必须是模态的。

Object Library 中,拖拽一个新的 window controller 到画布上。它创建了两个新的场景:一个 window controller scene 和一个 view controller scene

NewScenes

从新的 window controller scene 中选择 Window ,并使用 Size Inspector 来设定它的宽为300,高为150。从新的 view controller scene 中选择 View 并调整它的大小来匹配这个window:

WordCountReduced

由于字数统计是模态的,它的标题栏上如果有关闭、最小化和重新调整大小的按钮就会显得很奇怪,并违反了 HIG (苹果的人机交互指南)。

对于 关闭 按钮,它还会引入一个严重的bug,因为点击它就会关闭window,但没有调用 stopModal 。这样,这个app就会永远停留在“模态状态”下。

从模态中删除按钮

在storyboard中,选择 Word Count window并选择 Attributes Inspector 。取消勾选 Close Minimize Resize 。同时改变 Title Word Count

WordCountAppearance

现在,你将从Object Library中添加四个label控件和一个push button到Word Count window的 contentView 上。

选择 Attributes Inspector 。相应地改变Title为 Word Count Paragraph Count 0 0 。同时两个0的label的 alignment 为右对齐。将push button的Title改为 OK

WordCountFields

接下来为Window Count ViewController创建一个子类。选择 File / New / File.. ,选择 OS X / Source / Cocoa Class 。在 Choose Options 的对话框中,在 Class 后输入 WordCountViewController ,而在 Subclass of 后输入 NSViewController

WordCountViewController

点击 Next 创建新的文件。确认 WordCountWindowControll.swift 现在已经在project navigator中了。

找到storyboard. 为文字计数在 view controller scene 的view controller中选择proxy图标。打开 Identity Inspector , 从 Class 的下拉菜单中选择 WordCountViewController 。 注意在画板和 Outline View 上的名字是如何从通用的改变成 Word Count View Controller 的。

SetWCViewController

创建统计Label

现在你可以为两个label创建outlet了,让它们来展示统计的值 - 就是那两个0 label。在 WordCountViewController.swift 中的类的定义下,添加下列代码:

  @IBOutlet weak var wordCount: NSTextField!
  @IBOutlet weak var paragraphCount: NSTextField!

在storyboard中,右击 word count view controller 的proxy图标,拖拽到0 label,并当它高亮时释放鼠标。从弹出的 Outlets 列表中,选择 wordCount

为下面那个0 label重复同样的操作,但是这次选择 paragraphCount 。查看 Connections Inspector 中的每一个label,让它们的 Outlets 都像下面这么连接:

WordCountConnected

很快,你就会添加代码来加载count window controller。这要求它有一个 storyboard ID 。从storyboard的 word count window 中选择 window controller 。选择 Identity Inspector ,在 Storyboard ID 中输入 Word Count Window Controller

WCControllerStoryboardId

展示给我模态

现在到了展示模态window的基本逻辑了。在文档window的view controller中,找到并选择 ViewController.swift ,在 viewDidLoad 下面添加如下代码:

  @IBAction func showWordCountWindow(sender: AnyObject) {
 
    // 1
    let storyboard = NSStoryboard(name: "Main", bundle: nil)
    let wordCountWindowController = storyboard.instantiateControllerWithIdentifier("Word Count Window Controller") as! NSWindowController
 
    if let wordCountWindow = wordCountWindowController.window, textStorage = text.textStorage {
 
      // 2
      let wordCountViewController = wordCountWindow.contentViewController as! WordCountViewController
      wordCountViewController.wordCount.stringValue = "\(textStorage.words.count)"
      wordCountViewController.paragraphCount.stringValue = "\(textStorage.paragraphs.count)"
 
      // 3
      let application = NSApplication.sharedApplication()
      application.runModalForWindow(wordCountWindow)
    }
  }

一步一步来看:

  1. 使用你之前指定的storyboard ID初始化word count window controller。
  2. 设置检索自text view的相应的值到字数统计window中
  3. 模态地展示字数统计window

注意 :在第二步,你在两个view controller间传递了数据。这就类似于当transition涉及到一个segue时,你经常在 prepareForSegue 中做的一样。由于展示模态window是直接通过调用 runModalForWindow 完成的,没有涉及到segue,你只需在调用之前传递数据。

走开,模态

现在你将添加代码来让字符统计window消失。在 WordCountViewController.swift 中,添加下列代码到 paragraphCount outlet的下边:

  @IBAction func dismissWordCountWindow(sender: NSButton) {
    let application = NSApplication.sharedApplication()
    application.stopModal()
  }

这是当用户点击 字符统计window 中的 OK 时,应当被调用的 IBAction

找到storyboard,右击 OK ,并拖动带 word count view controller 的proxy图标上。释放鼠标并在出现的列表中选择 dismissWordCountWindow:

ConnectOK

添加UI来调用它

剩下的要来展示window的唯一一件事,就是添加UI并去调用它了。找到storyboard,在 Main Menu 中点击 Edit 。从 Object Library 中拖出一个 Menu Item Edit 菜单的底部。选择 Attributes Inspector 并设置title为 Word Count

WCMenu

花几分钟来通过输入 command – K 创建一个快捷键,作为 key equivalent

现在你将在 ViewController.swift 中连接新的菜单项到 showWordCountWindow 方法上。

找到storyboard,右击 Word Count 菜单项,拖拽到 application scene First Responder 上。从列表中选择 showWordCountWindow

ConnectWCMenuItem

注意 :你可能会奇怪为什么你要连接菜单项到first responder,而不是直接到 showWordCountWindow 这里。这是因为文档view的主菜单和view controller是在不同的storyboard的scene中,因此,不能直接连接。

运行app,选择 Edit / Word Count ,瞧(voila),字数统计window展示了它自己。

WordCountFinal

单击OK关闭window。

从这儿去向哪里?

这里是BabyScript的 最终版本

在这个OS X的window和window controller教程中,你已经覆盖了很多基本的内容!但它对于你可以利用window和window controller做的事来说,只是冰山的一角。

你已经覆盖了:

  • 动作的MVC设计模式
  • 怎样创建一个多window的app
  • OS X app的典型app架构
  • 怎样使用Interface Builder 编程方式来定位和排列window
  • 使用自动布局来来根据window调整view的大小
  • 使用模态window来展示附加的信息

还有更多!

我强烈地建议你探索苹果在 El Capitan’s Mac 开发者库 中提供的的大量文档。特别的,参考 Window编程指南 .

为了更好地理解Cocoa app的设计,以及如何使用文档开头提到的app的类型,请查看 Mac App编程指南 。这个文档还扩展了基于多窗口文档的应用程序的概念,你会在这里找到改善BabyScript的idea。