前言

近期重读 An introdution to Morphic, 本文翻译自其中的部分章节。

Morphic 是我最喜欢的 UI 框架, An introdution to Morphic 则是我最喜欢的 Morphic 教程,教程的作者正好也是 Morphic 的设计者: John Maloney。

教程中涉及的一些例子,我已经整理放到了 Github 上 (修复了因 Squeak 的版本变化导致的 bug): Morphic-Fun

Morphic 介绍

Morphic 是一个用户界面框架,它使构建生动的(lively)交互式用户界面变得简单而有趣。Morphic 处理了大部分显示更新、事件调度、拖放、动画和自动布局的繁琐工作,从而将编程者解放出来,专注于设计而不是机制。有些用户界面框架需要大量的模板代码来完成简单的事情, 而在 Morphic 中,少量的代码就能发挥很大的作用,几乎不浪费任何努力。

译文

这一节概述了 Morphic 的工作原理,其详细程度足以帮助 Morphic 编程者从系统中获得最大收益。

UI 循环(The UI Loop)

每个交互式用户界面框架的核心, 都是最早的交互式计算机系统中的 “读取(read)-评估(evaluate)-打印(print)” 循环(loop)的现代版。然而,在这个现代版本中,“读取” 处理事件而不是字符,“打印” 执行绘图操作以更新图形显示而不是输出文本。Morphic 版本的这个循环增加了两个额外的步骤,为自动布局和活性(liveness)提供钩子(hook)。

1
2
3
4
5
do forever:
    process inputs
    send step to all active morphs
    update morph layouts
    update the display

有时,这些步骤都无事可做;没有事件需要处理,没有 Morph 需要步进(step),没有布局更新,也没有显示更新。在这种情况下,Morphic 会休眠几毫秒,这样它就不会在空闲的时候占用 CPU 了。

输入处理

输入处理是将传入的事件分派给适当的 Morph 的问题。击键事件被发送到当前的键盘焦点 Morph,它通常由鼠标点击建立。如果没有建立键盘焦点,按键事件就会被丢弃。在任何时候,最多只有一个键盘焦点 Morph。

鼠标按下(Mouse down)事件是按位置派发的;事件发生位置的最前面的 Morph 可以处理该事件。事件不会穿过 Morph;你不可能意外地按下藏在其他 Morph 后面的按钮。Morphic 需要知道哪些 Morph 对获取鼠标事件感兴趣。它通过向每个候选 Morph 发送 handlesMouseDown: 消息来实现这一目的。事件的提供是为了让 Morph 可以根据事件发生时 按下的鼠标键和按下的修改键 来决定是否要处理这个事件。如果没有找到处理该事件的 Morph,默认行为是拾起光标下最前面的 Morph。

在一个复合 Morph 中,其最前面的 submorph 被给予第一个机会来处理一个事件,这与 submorph 出现在其所有者(owner)前面的事实一致。如果该 submorph 不想处理该事件,它的所有者会得到一个机会。如果它的所有者不想处理,那么它的所有者的所有者就会得到一次机会,以此类推,沿着所有者链向上。这个策略允许一个对鼠标敏感的 Morph,比如一个按钮,被装饰上标签或图形,仍然可以得到鼠标的点击。在我们第一次尝试事件调度的时候,鼠标在 submorph 上的点击并没有传递给它的所有者,所以点击被按钮的标签阻止了。 要想点击一个按钮而不碰到它的标签,并不是那么容易的事。

鼠标移动(mouse move)和鼠标抬起(mouse up)的事件呢?考虑一下当用户拖动一个滚动条的手柄时会发生什么。当鼠标在滚动条上向下移动时,滚动条开始跟踪被拖动的鼠标。如果光标移动到滚动条之外,甚至光标被拖到一个按钮或其他滚动条上,它也会继续跟踪鼠标。这是因为 Morphic 认为鼠标按下、鼠标重复移动和鼠标抬起的整个序列是一个单一事务。无论哪个 Morph 接受了鼠标按下的事件,都被认为是鼠标焦点,直到鼠标再次抬起。鼠标焦点 Morph 保证获得整个鼠标拖动事务:一个鼠标按下事件,至少一个鼠标移动事件,以及一个鼠标抬起事件。因此,Morph 可以在鼠标按下时执行一些初始化,在鼠标抬起时进行清理,并保证初始化和清理总是能够完成的。

活性

活性的处理方法是保留一个需要被步进(step)的 Morph 列表,以及它们所期望的下一个步进时间。每一个周期,步进信息都会被发送到任何一个需要步进的 Morph 上,并且更新它们的下一个步进时间。被删除的 Morph 将从步进列表中删减掉,这既是为了避免步进不再出现在屏幕上的 Morph,也是为了让这些 Morph 能够被垃圾回收。

布局更新

Morphic 渐进地维护 Morph 的布局。当一个 Morph 的变化可能影响到布局时(例如,当一个新的 submorph 被添加到它上面时),消息 layoutChanged 就会被发送给它。这将触发一连串的活动。首先,改变后的 Morph 的布局被更新。这可能会改变分配给它的 submorp(s)的空间量,导致它们的布局被更新。然后,如果改变后的 Morph 的空间需求发生了变化(例如,如果它需要更多的空间来容纳一个新增加的 submorph),它的所有者的布局就会被更新,可能还有它的所有者的所有者的布局,依此类推。在某些情况下,一个深度嵌套的复合 Morph 中的每个 submorph 的布局都可能需要更新。幸运的是,在很多情况下,布局的更新可以被局部化,从而节省大量的工作。

和改变消息(changed message)一样,Morph 客户端通常不需要明确地发送 layoutChanged,因为影响 Morph 布局的最常见的操作已经做了这个, 这些常见操作包括添加和删除 submorph 或改变 Morph 的大小…警觉的读者可能会担心,在建立一个有很多 submorph 的行或列时,在添加 Morph 后更新布局可能会使事情变得缓慢。事实上,由于更新布局的成本与该行或列中已存在的 Morph 数量成正比,那么一次添加 N 个 Morph,并在每个 Morph 之后更新布局,其成本与 N 的平方成正比。当建立像 ScorePlayerMorph 这样复杂的 Morph 时,这个成本会迅速增加。为了避免这个问题,Morphic 将所有布局的更新推迟到下一个显示周期。毕竟,在下一次重绘屏幕之前,用户无法看到任何布局的变化。因此,一个程序可以在两个显示周期之间对一个给定的 Morph 进行任何数量的布局改变操作,而 Morphic 只会更新这个 Morph 的布局一次。

显示更新

Morphic 使用一种双缓冲、增量的算法来保持屏幕的更新。这种算法是高效的(它试图在变化后做尽可能少的工作来更新屏幕)和高质量的(用户不会看到屏幕被重新绘制)。它也大多是自动的;许多应用程序可以在编程者不知道如何维护显示的情况下建立。这里的描述主要是为了让那些对该系统的工作原理感到好奇的人受益。

Morphic 保留了一个列表,称为损坏列表(必须重新绘制的屏幕部分)。每个 Morph 都有一个边界矩形,包围着它的整个可见表示。当一个 Morph 改变了外观的任何方面(例如,它的颜色),它就会向自己发送改变(changed)消息,这就把它的边界矩形添加到损坏列表中。Morphic UI 循环的显示更新阶段负责将屏幕更新。对于损坏列表中的每个矩形,它都会重绘(按从后到前的顺序)所有与损坏矩形相交的 Morph。这种重绘是在屏幕外的缓冲区完成的,然后复制到屏幕上。由于单个 Morph 是在屏幕外绘制的,用户永远不会看到绘制过程的中间阶段,而且从屏幕外的缓冲区到屏幕的最终拷贝也相当快。其结果是,无论单个绘制操作的顺序如何,物体的动画效果都很流畅,看起来很牢固。当所有的损坏矩形都被处理后,Morphic 清除损坏列表,为下一个周期做准备。