本文是Apple Documentation Archive Window Programming Guide的学习笔记。

窗口(Window)是任何带图形界面的应用程序的基本组成部分。是应用程序与操作系统的图形子系统交互的媒介。电脑屏幕显示区域是是一种共享资源,由系统统一管理。所以一个图形化的操作系统(不管是Windows还是Mac),都带有一个窗口管理器,用来协调不同的应用程序对屏幕显示区域的使用。

ADA上关于窗口管理的文档是[Window Programming Guide]。macOS上的应用程序使用AppKit来构造图形程序,iOS上的应用程序则使用UIKit。

窗口是如何工作的

How Windows Work

AppKit提供一个NSWindow类,来帮助应用程序在显示区域创建和管理窗口。简单而言,一个NSWindow对应屏幕上的一个窗口,通过操控NSWindow,应用程序得以与窗口管理器协作,实现对窗口的管理。窗口管理器提供图形和框架,应用程序提供数据和行为。

窗口管理器提供给NSWindow的是一块矩形区域,也称为一个帧框(frame)。macOS使用类似数学坐标轴的方式来管理屏幕,屏幕左下角为原点,屏幕所在区域为坐标轴的第一象限。NSWindow的帧框需要用两组值来表示,分别是坐标和大小,均用整形值表示。NSWindow的帧框坐标系统的单位和象限均与屏幕坐标系统一致。

当一个NSWindow被创建时,会同时创建两个默认的NSView,一个不透明的,用来填满窗口帧框,绘制窗口部件,比如标题栏、工具栏、状态栏等等。另一个透明的NSView用来填充窗口的内容区域。NSWindow内容区域的NSView的位置和大小由NSWindow决定,无法通过调用NSView的setFrame方法来修改。但是内容区域默认的NSView却可以替换成其他NSView,只需调用NSWindow的setContentView即可。

实际上,因为NSView可以嵌套子的NSView(使用addSubView方法),可以添加子的NSView到NSWindows内容区域的NSView中,这样可以形成一整颗NSView树。然后NSWindow可以根据这些NSView的层次结构,以及叠加区域,把键盘或者鼠标事件派发给这些NSView。

NSPanel是NSWindow的一个子类

其他话题:

  • NSWindow的delegate
  • NSWindow的initializers

窗口是如何显示的

How a Window is Displayed

在NSWindow需要显示的时候,NSWindow会调用内容区域顶层的NSView的display方法,然后顶层的NSView会逐级调用子的NSView的display。这样导致的结果是层级浅的NSView先显示,层级深的NSView后显示,然后后显示的NSView会覆盖一部分先显示的NSView的区域,最终形成我们所看到的GUI界面。NSWindow所在的帧框其实可以表示为一个二维数组,每个元素代表屏幕上的一个像素。一个NSWindow的显示过程,其实就是填充这个二维数组的过程。填满后,再调用显卡驱动来在屏幕上显示这个二维数组。

NSWindow包含的NSView们在显示的时候是可以并发绘制的,allowsConcurrentViewDrawingsetAllowsConcurrentViewDrawing:可以用来查询和控制是否并发绘制。

直接调用NSWindow/NSView的display方法会强制重新绘制其帧框。如果使用displayIfNeeded则只会导致那些被setViewsNeedDisplay:了的NSWindow/NSView被重新绘制。一般来说当一个NSWindow的某个子NSView被重新绘制了,那么这个NSWindow也要跟着刷新。这个功能是自动的,嵌套在NSWindow的事件循环中。如果需要关闭,可以使用setAutodisplay:方法。

一个应用可以由多个窗口。在每一次的事件循环中,应用对象会调用updateWindows方法来给每一个窗口发送update消息。NSWindow的派生类可以覆盖这个方法,在自己的方法中检查应用对象的状态,来调整自身显示,比如禁用菜单,隐藏工具栏等操作。

前面提到,每个NSWindow/NSView的帧框其实是一个二维的像素数组,可以调用print:或者dataWithEPSInsideRect:来打印这些像素。

其他参考:

模态窗口如何工作

How Modal Windows Work

一个窗口或者Panel(面板)可以工作在模态方式。一个模态窗口的例子是当你准备关闭尚未保存的文档时候弹出来的“是否保存文档“的对话框。 NSApplication的runModalForWindow方法可以使一个窗口(或者面板)变成模态。当一个窗口变成模态之后,它会独占NSApplication的事件循环,导致其他窗口失去响应,直到操作完并关闭模态窗口。stopModalWithCode:可以用来关闭模态窗口。abortModalstopModalstopModalWithCode:的两个包装。需要注意的是,stopModal不能用在timer的回调,以及分布式对象上,因为这些处理在事件循环之外。

另外一种达到模态的方法是使用模态会话(modal session),使用场景是应用自身要去忙一些别的操作,比如花费很长时间发送一个很大的文件,然后又不能让窗口停滞,需要时不时更新窗口,保持响应,并允许用户做一些操作,比如停止发送文件。调用应用对象的beginModalSessionForWindow:可以开启一个会话,这个方法会返回会话标识,后续用来做一些和会话相关的操作。开始会话之后,应用对象可以去忙自己的了,但是应用对象必须时不时调用runModalSession: 来派发事件到模态窗口,保持响应。runModalSession: 会返回用户对模态窗口的操作,比如点击了停止发送的操作,这样应用就知道没有必要再继续当前的操作,可以结束,然后调用endModalSession: 来终止会话,恢复到正常的事件循环之中。

前面提到,模态窗口会独占事件循环,导致其他窗口失去响应。但是对于辅助性的窗口,比如菜单和面板,还是需要保持一定的响应度来处理用户输入。像有这种需求的窗口,可以派生NSWindow或者NSPanel(通常用后者),然后覆盖worksWhenModa方法,并在其中返回YES。这样一来,派生类就可以处理鼠标和键盘事件了。

模态窗口甚至会阻止用户关闭应用窗口。在OS X 10.6之后,可以通过应用对象的setPreventsApplicationTerminationWhenModal:方法来将其设置成就算有模态窗口,也可以关闭应用窗口。

你也可以不使用模态会话,而使用分布式对象(Ditributed Object)来在事件循环之外的线程中处理需要花长时间的操作。需要注意的是AppKit本身不是线程安全的,所以分布式对象需要和一个指定对象(Designated Object)联络,然后AppKit再和这个指定对象联络。

其他:

“Responding to User Events and Actions” in Creating a Custom View.

How Panels Work

How Panels Work

面板(Panel)是一种特殊的窗口。NSPanel是NSWindow的派生类,包含一些特殊的属性:

  • 默认情况下,关闭面板时,并不会释放相应的资源,以留待重用。
  • NSPanel的hidesOnDeactivate属性默认返回YES,所以应用最小化的时候面板也会跟着隐藏。注意,alert对话框除外。
  • 面板可以成为接收输入的键窗口,但是无法成为主窗口。当面板是键窗口,并且关闭按键处于显示状态的时候,可以通过点击关闭按键关闭。

除了上面的特性以外,NSPanel

  • setBecomesKeyOnlyIfNeeded:可以让面板只在需要时成为键窗口
  • setFloatingPanel: 可以使面板浮动
  • 前面提到的,setWorksWhenModal:可以在应用进入模态的时候让面板保持交互性

(本篇完)