Pharo by Example - 第十六章 Morphic

标签: pharo ; smalltalk ;


Morphic 是 Pharo 图形界面的名称。Morphic 支持两个主要方面:一方面,Morphic 定义了所有低级图形实体和相关基础设施(事件、绘图等);另一方面,Morphic 定义了 Pharo 中可用的所有小部件。Morphic 是用 Pharo 编写的,因此它在操作系统之间完全可移植。因此,Pharo 在 Unix、MacOS 和 Windows 上看起来完全相同。Morphic 与大多数其他用户界面工具包的区别在于,它没有单独的界面组合和运行模式:所有图形元素都可以随时由用户组装和拆卸。我们感谢 Hilaire Fernandes 允许本章基于他的原始法语文章。

16.1 Morphic的历史

Morphic 由 John Maloney 和 Randy Smith 为 Self 编程语言开发,始于 1993 年左右。Maloney 后来为 Squeak 编写了新版本的 Morphic,但 Self 版本背后的基本思想在 Pharo Morphic 中仍然活跃:直接性和活跃性。直接性意味着屏幕上的形状是可以直接检查或更改的对象,即通过使用鼠标点击它们。活跃性意味着用户界面始终能够响应用户操作:屏幕上的信息随着它所描述的世界的改变而不断更新。一个简单的例子是,你可以分离菜单项并将其保留为按钮。

打开"World"菜单,按下 Alt + Shift 键的同时鼠标点击菜单,它就会弹出Morphic光环,然后在你想要分离的菜单项上再次重复这个操作,就会弹出该项的光环(如图16-1所示)

[译注] 在我的系统(Linux)上,调出Morphic光环的方法是直接用鼠标中键点击,不需要任何的修饰键。

现在通过抓住绿色手柄在屏幕上的其他地方复制该项目,如图16-1所示。

一旦放开菜单项,该菜单项将与菜单分离,您可以与它交互,就像它仍然在菜单中一样(参见图16-2)。

这个例子说明了我们所说的直接和生动。这为开发替代用户界面和替代交互原型提供了强大的能力。

Morphic有些过时了,Pharo社区几年来一直在研究一个可能的替代品。替换Morphic意味着同时拥有新的底层基础设施和新的小部件集。这个项目名为Bloc,经过了多次迭代。Bloc是关于基础设施的,而Brick是建立在基础上的一组小部件。现在还是让我们好好享受Morphic吧。

16.2 Morphs

当你在运行Pharo时,你在屏幕上看到的所有对象都是Morph,也就是说,它们是Morph类的子类的实例。Morph类本身是一个具有许多方法的大类;这使得其子类可以用很少的代码实现有趣的行为。

要创建一个Morph来表示一个字符串对象,在Playground窗口中执行以下代码:

('Morph' asMorph onenInWorld.

这将创建一个Morph来表示字符串'Morph',然后在World中打开它(即显示它),World是Pharo对整个屏幕的称呼。你应该已经获得了一个图形元素(Morph),可以通过元点击来操作它。

当然,可以定义比您刚才看到的更有趣的Morph变体。asMorph方法在Object类中有一个默认的实现,它只创建一个StringMorph。例如,Color tan asMorph返回一个用Color tan printString的结果标记的StringMorph。让我们改变一个,这样我们就得到了一个彩色的矩形。

现在,在Playground窗口中执行(Morph new color: Color orange) openInWorld。你得到一个橙色的矩形(图16-4)

16.3 操纵Morph

Morph 是对象,所以我们可以像在Pharo中操纵其他对象那样操纵它们:通过发送消息,我们可以更改它们的属性,创建Morph的新子类,等等。

每一个Morph, 即使它目前没有在屏幕上打开,也有一个位置和大小。为了方便起见,所有Morph都被认为占据屏幕上的一个矩形区域;如果它们是不规则开头的,它们的位置和大小就是围绕它们的最小的矩形框,这被称为Morph的边界框,或仅仅是它的边界。

  • position方法返回一个Point,描述了Morph的左上角(或其边界框的左上角)的位置。坐标系统的原点位于屏幕的左上角,y坐标沿屏幕向上递增,x坐标向右递增。

  • extent方法也返回一个Point,描述的是Morph的宽度和高度,而不是位置。

在Playground窗口中输入下面的代码,然后Do it

joe := Morph new color: Color blue.
joe openInWorld.
bill := Morph new color: Color red.
bill openInWorld.

接着,输入joe position,然后Print it。执行joe position: (joe position + (10@3))移动joe(如图16-5所示)

对于大小也可以做类似的操作,joe extent返回joe的大小;要增加它的大小,执行joe extent: (joe extent * 1.1)。要改变它的颜色,可以发送color:消息,并将所需的Color对象作为参数,例如,joe color: Color orange,要添加透明度,请尝试joe color: (Color orange alpha: 0.5)

要让bill跟随joe, 你可以重复执行以下代码:

bill position: (joe position + (100@0))

如果你使用鼠标移动joe, 然后执行此代码,bill将移动到joe的右侧100像素的地方。如图16-6所示。没有什么好奇怪的。

注意,你可以通过下面的方法删除你创建的Morph:

  • 给它们发送delete消息;

  • 通过 Alt + Shift + 单击 选中 Morph, 出现光环后再点击左上角的叉叉即可删除。

16.4 组合Morph

创建新的图形表示的一种方法是将一个Morph放在另一个Morph中。 这叫做合成。Morph可以以任意深度进行组合。你可以给作为窗口的Morph发送消息addMorph:,将另一个Morph放入其中。

尝试下面的代码:

balloon := BalloonMorph new color: Color yellow.
joe addMorph: balloon.
balloon position: joe position

最后一行代码将气球定位到与joe相同的坐标。注意,所包含的Morph的坐标仍然是相对于屏幕的,而不是相对于作为容器的Morph.这种绝对定位的方式并不是很好,它让Morph编程感觉有点奇怪。但是有许多方法可以定位Morph.浏览Morph类的geometry协议,自己看看。例如,要使气球在joe内部居中,可以执行balloon center: joe center

现在,如果你尝试用鼠标抓取气球,你会发现你实际上抓取了joe,并且两个Morph一起移动,气球已经嵌入到joe中了。在joe体内嵌入更多的Morph是可能的。除了以编程方式实现外,还可以通过直接操作嵌入Morph.

16.5 创建并绘制你自己的Morph

虽然可以通过组合Morph来制作许多有趣和有用的图形表示,但有时个你需要创建一些完全不同的东西。

要做到这一点,你需要定义一个Morph的子类,并覆盖drawOn:方法来改变它的外观。

当需要在屏幕上重新显示Morph时,Morph框架发送消息drawOn:给Morph.drawOn:的参数是某种画布Canvas。预期的行为是Morph将在画布的边界内绘制自己。让我使用这个知识来创建一个十字形状的Morph.

通过浏览器定义一个继承自MorphCrossMorph类:

Morph subclass: #CrossMorph
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'PBE-Morphic'

我们可以像下面这样定义drawOn:方法:

CrossMorph >> drawOn: aCanvas
  | crossHeight crossWidth horizontalBar verticalBar |
  crossHeight := self height / 3.
  crossWidth := self width / 3.
  horizontalBar := self bounds insetBy: 0 @ crossHeight.
  verticalBar := self bounds insetBy: crossWidth @ 0.
  aCanvas fillRectangle: horizontalBar color: self color.
  aCanvas fillRectangle: verticalBar color: self color

给Morph发送消息bounds返回它的边界框,它是一个Rectangle(矩形)类的实例。Rectangle理解许多绘制矩形的消息。这里,我们使用insetBy:消息和一个Point作为参数,首先创建一个高度减少的矩形,然后再创建另一个宽度减少的矩形。

要测试你的新Morph, 执行CrossMorph new openInWorld

结果应该如图16-8所示。然而,你会发现,你可以点击抓住Morph的敏感区域仍然是整个边界框。接下来我们解决这个问题。

当Morphic框架需要找出哪些Morph位于光标下方时,它将消息containsPoint:发送给所有边界框位于鼠标下方的Morph.因此,为了将Morph的敏感区域限制为十字区域,我们们需要覆盖containsPoint:方法。

CrossMorph类中定义下面的方法:

CrossMorph >> containsPoint: aPoint
  | crossHeight crossWidth horizontalBar verticalBar |
  crossHeight := self height / 3.
  crossWidth := self width / 3.
  horizontalBar := self bounds insetBy: 0 @ crossHeight.
  verticalBar := self bounds insetBy: crossWidth @ 0.
  ^ (horizontalBar containsPoint: aPoint) or: [ verticalBar
    containsPoint: aPoint ]

这个方法使用与drawOn:相同的逻辑,所以我们可以确信containsPoint:返回值为true的点也被drawOn:着色的点是相同的。注意,我们如何利用Rectangle类中的containsPoint:方法来完成这项艰巨的任务。

上面的代码中有两个问题。

最明显的是,我们有重复的代码。这是一个基本错误:如果我们发现需要更改horizontalBarverticalBar的计算方式,我们很可能会忘记对这两个方法进行同步更改。解决方案是将这些计算分解为两个新方法,我们将其放入私有协议中:

CrossMorph >> horizontalBar
  | corssHeight |
  crossHeight := self height / 3.
  ^ self bounds insetBy: 0 @ crossHeight
  
CrossMorph >> verticalBar
  | crossWidth |
  crossWidth := self width / 3.
  ^ self bounds insetBy: crossWidth @ 0

然后我们可以使用以下方法定义drawOn:containsPoint:

CrossMorph >> drawOn: aCanvas
  aCanvas fillRectangle: self horizontalBar color: self color.
  aCanvas fillRectangle: self verticalBar color: self color
  
CrossMorph >> containsPoint: aPoint
  ^ (self horizontalBar containsPoint: aPoint) or: [ self verticalBar
    containsPoint: aPoint ]

这段代码很容易理解,很大程度上是因为我们给私有方法赋予了有意义的名称。事实上,它是如此简单,你可能已经注意到了第二个问题:在十字中心的区域,即水平和垂直条交叉的地方,画了两次。当我们用不透明的颜色填充十字时,这并不重要,但是如果我们画一个半透明的十字,问题立刻就会表现出来,如图16-9所示

在Playground窗口中执行以下代码:

CrossMorph new openInWorld;
  bounds: (0@0 corner: 200@200);
  color: (Color blue alpha: 0.4)

解决方法是将竖线分成三部分,只填充顶部和底部。再次地,我们在Rectangle类中找到了一个方法,它为我们完成了这项艰巨的任务:r1 areasOutside: r2返回由 r1 到 r2 的部分组成的矩形数组。修正后的代码如下:

CrossMorph >> drawOn: aCanvas
  | topAndBottom |
  aCanvas fillRectangle: self horizontalBar color: self color.
  topAndBottom := self verticalBar areasOutside: self horizontalBar.
  topAndBottom do: [ :each | aCanvas fillRectangle: each color: self color ]

这段代码看起来可行,但如果尝试调整某个十字的大小,你可能会注意到,在某些尺寸下,一条1像素宽的线将十字的底部与其它部分隔开了,如图16-10所示。这是由于四舍五入造成的,当矩形的大小被一具非整数填充,fillRectangle:color:似乎不一致地四舍五入,留下一行像素未被填充。

我们可以通过显式地舍入来解决这个问题

CrossMorph >> horizontalBar
  | crossHeight |
  crossHeight := (self height / 3) rounded.
  ^ self bounds insetBy: 0 @ crossHeight
  
CrossMorph >> verticalBar
  | crossWidth |
  crossWidth := (self width / 3) rounded.
  ^ self bounds insetBy: crossWidth @ 0

16.6 鼠标交互事件

要使用Morph构建实时的用户界面,我们需要能够使用鼠标和键盘与它们进行交互。此外,Morph需要能够通过改变自己的外观和位置来响应用户的输入——也就是说,它们要能够动起来。

当鼠标的按键被按下时,Morphic将会给鼠标指针下的每一个Morph发送消息handlesMouseDown:,如果某个Morph返回true,那么Morphic立即向它发送消息mouseDown:消息;当用户释放鼠标按键时,它发送mouseUp: 消息。如果所有Morph都回答false,那么Morphic将启动一个拖放操作。正如我们下面将要讨论的,mouseDown:mouseUp:消息在发送时带有一个参数:一个MouseEvent对象,安编码了鼠标操作的细节。

让我们扩展CrossMorph以处理鼠标事件。我们首先确保所有的CrossMorph对象对handlesMouseDown:消息响应true。将下面的方法添加到CrossMorph

CrossMorph >> handlesMouseDown: anEvent
  ^ true

假设当我们点击十字时,我们想要将十字的颜色更改为红色,而当我们进行 action-click 十字时,我们希望将它的颜色改成黄色。我们可以定义mouseDown:方法如下:

CrossMorph >> mouseDown: anEvent
  anEvent redButtonPressed
    ifTrue: [ self color: Color red ]. "click"
  anEvent yellowButtonPressed
    ifTrue: [ self color: Color yellow ]. "action-click"
  self changed

注意,上面的代码除了改变Morph外,还发送了self changed消息。它可以确保Morphic及时地给对象发送drawOn:消息。

还要注意的是,一旦Morph处理(重写)了鼠标事件,你就不能再用鼠标点击抓取并移动它了。相反,你必须使用光环:使用 Option-Command-Shift 在Morph上点击,让光环出现,并抓住它顶部的棕色移动手柄或黑色的拾取手柄。

mouseDown:的参数anEvent是一个MouseEvent的实例,它是MorphicEvent的子类。MouseEvent定义了redButtonPressedyellowButtonPressed方法。浏览这个类,看看它提供了什么别的方法来查询鼠标事件。

16.7 键盘事件

要捕获键盘事件,我们需要三个步骤。

  1. 将键盘焦点交给某个Morph.例如,当鼠标移动到Morph上时,我们可以让它获得焦点;

  2. 使用keyDown:方法处理键盘事件本身。当用户按下一个键时,该消息将被发送到拥有键盘焦点的Morph;

  3. 当鼠标不在Morph上方时,释放键盘焦点。

我们来扩展CrossMorph,使其对按键作出反应。首先,我们需要安排当鼠标在Morph上方时收到通知。如果我们的Morph对handlesMouseOver:消息响应为true,它就会在鼠标移动到上方时收到通知。

CrossMorph >> handlesMouseOver: anEvent
  ^ true

这条消息与handlesMouseDown:相当。当鼠标指针进入或离开Morph时,会给它发送mouseEnter:mouseLeave:消息。

定义两个方法,使CrossMorph捕获并释放焦点,第三个方法在按下键时被通知,第四个方法实际处理键盘事件。

CrossMorph >> mouseEvent: anEvent
  anEvent hand newKeyboardFocus: self
  
CrossMorph >> mouseLeave: anEvent
  anEvent hand releaseKeyboardFocus: self
  
CrossMorph >> handlesKeyDown: anEvent
  ^ true
  
CrossMorph >> keyDown: anEvent
  | key |
  key := anEvent key.
  key = KeyboardKey up ifTrue: [ self position: self position - (0 @ 10) ].
  key = KeyboardKey down ifTrue: [ self position: self position + (0 @ 10)].
  key = KeyboardKey right ifTrue: [ self position: self position + (10 @ 0)].
  key = KeyboardKey left ifTrue: [ self position: self position - (10 @ 0)].

我们编写的方法可以使用方向键移动Morph.发知道具体按键的值,你可以打开一个Transcript窗口,并将Transcrip show: anEvent keyValue添加到handleKeystroke:方法中。

handleKeystroke:anEvent参数是MorphicEvent的另一个子类keyboardEvent的实例。浏览这个类以了解关于键盘事件更多的信息。

如果你想通过类似于Ctrl的快捷键移动Morph,你可以使用代码如下:

anEvent controlKeyPressed ifTrue: [
  anEvent keyCharacter == $d ifTrue: [
    self position: self position + (0 @ 10)]]

16.8 Morphic动画

Morphic提供了一个简单的动画系统,有两个主要的方法:step以固定的时间间隔发送给morph, stepTime指定步骤之间的时间,以毫秒为单位。stepTime实际上是步骤之间的最小时间。如果你要求1毫秒的stepTime,Pharo经常不能对你的morph进行步行,对此你不要感到惊讶。此外,startStepping开启步进机制,而stopStepping则关闭。isStepping可以用来确定一个morph是否正在进行步进。

通过下面的步骤可以使用CrossMorph闪烁:

CrossMorph >> stepTime
  ^ 100
  
CrossMorph >> step
  (self color diff: Color black) < 0.1
    ifTrue: [ self color: Color red ]
    ifFalse: [ self color: self color darker ]

首先,你可以在一个CrossMorph对象上打开检查器,使用调试手柄(看起来像光环中的扳手),在底部的小窗格中输入self startStepping,然后 Do it

你也可以像这样重新定义初始化方法:

CrossMorph >> initialize
  super initialize.
  self startStepping

或者,你可以处理key stroke,以便你可以使用+-键来开始和停止步进。

通常使用Keystroke事件来管理文本,而快捷方式则在KeyUpKeyDown事件中管理。

为了处理击键事件,我们需要一个方法来在按键被按下时得到通知,以及另一个方法来实际处理击键事件。

CrossMorph >> handlesKeyStroke: anEvent
  ^ true
  
CrossMorph >> keyStroke: anEvent
  | keyValue |
  keyValue := anEvent keyCharacter.
  keyValue == $+ ifTrue: [ self startStepping ].
  keyValue == $- ifTrue: [ self stopStepping ]

16.9 Interactors

要提示用户输入,UIManager类提供了大量可直接使用的对话框。例如,request:initialAnswer:方法返回用户输入的字符串(图16-11)

UIManager default
  request: 'What''s your name?'
  initialAnswer: 'no name'

要显示一个弹出菜单,可以使用chooseFrom:方法(图16-12)

UIManager default
  chooseFrom: #('circle' 'oval' 'square' 'rectangle' 'triangle')
  lines: #(2 4) message: 'Choose a shape'

浏览UIManager类并尝试它所提供的一些交互方法。

16.10 拖放

Morphic 还支持拖放操作。让我们检查一个有两个Morph的简单例子,一个作为接收者的Morph和一个被拖放的Morph.只有当被拖放的morph符合给定的条件时,接收者才会接受该morph:在我们的例子中,morph应该是蓝色的。如果拒绝,被拖放的morph将决定做什么。

我们首先定义接收者morph:

Morph subclass: #ReceiverMorph
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'PBE-Morphic'

现在用通常的方式定义初始化方法:

ReceiverMorph >> initialize
  super initialize.
  color := Color red.
  bounds := 0 @ 0 extent: 200 @ 200

我们如何决定接收者Morph将接受或拒绝被拖放的Morph?一般来说,两种Morphf必须同意交互作用。接收者Morph通过响应wantsDroppedMorph:event:来实现这一点。它的第一个参数是被拖放的Morph, 第二个参数是鼠标事件,因此接收者可以查看,在放开鼠标时是否按下了任何修改键。被拖放的Morph也有机会通过wantsToBeDroppedInto:消息检查它是否喜欢被拖放的Morph.这个方法的默认实现(在Morph类中)是true

ReceiverMorph >> wantsDroppedMorph: aMorph event: anEvent
  ^ aMorph color = Color blue

如果接收者不接受它,被拖放的Morph会发生什么?默认行为是它什么也不做,也就是说,它位于接收都的顶部,但是不与它交互。更直观的行为是,当接收者不想接受被拖放的Morph时让它回到原来的位置。这可以通过让接收者对repelsMorph:event:消息回答true来实现。

ReceiverMorph >> repelsMorph: aMorph event: anEvent
  ^ (self wantsDroppedMorph: aMorph event: anEvent) not

对于接收者来说,这就是所需要的全部。

在Playground中创建ReceiverMorphEllipseMorph的实例:

ReceiverMorph new openInWorld;
  bounds: (100@100 corner: 200@200).
EllipseMorph new openInWorld.

尝试将黄色的EllipseMorph拖放到接收器上。它将被拒绝并送回原来的位置。

要改变此行为,请将椭圆形的颜色改为蓝色(通过new后面跟着消息color: Color blue)。蓝色的Morph应该被ReceiverMorph接受。

让我们创建一个特定的Morph子类,命名为DroppedMorph,这样我们就可以做更多的实验。让我们定义一种名为DroppedMorph的新morph.

Morph subclass: #DroppedMorph
  instanceVariableNames: ''
  classVariableNames: ''
  package: 'PBE-Morphic'
  
DroppedMorph >> initialize
  super initialize.
  color := Color blue.
  self position: 250 @ 100

现在,我们可以指定被拖放的Morph在接收者拒绝的时候应该做什么。在这里它会一直附着在鼠标指针上:

DroppedMorph >> rejectDropMorphEvent: anEvent
  | h |
  h := anEvent hand.
  WorldState addDeferredUIMessage: [ h grabMorph: self ].
  anEvent wasHandled: true

给事件发送消息hand会响应一个"hand", 它是一个HandMorph类的实例,代表鼠标指针和它所持有的任何东西。在这里,我们告诉World, hand应该抓住自己,即被拒绝的Morph.

创建两个不同颜色的DroppedMorph实例,然后将它们拖放到接收者上。

ReceiverMorph new openInWorld.
morph := (DroppedMorph new color: Color blue) openInWorld.
morph position: (morph position + (70@0)).
(DroppedMorph new color: Color green) openInWorld.

绿色的morph被拒绝,因此保持在招标指针上。

16.11 一个复杂的例子

我们来设计一个Morph玩掷骰子。点击它将快速地循环显示骰子的所有面,再次点击则停止动画。

将骰子定义为BorderedMorph而不是Morph的子类,因为我们会用到边框。

BorderedMorph subclass: #DieMorph
  instanceVariableNames: 'faces dieValue isStopped'
  classVariableNames: ''
  package: 'PBE-Morphic'

实例变量faces记录了骰子上的面数;我们允许骰子最多有9个面。dieValue记录当前显示的面的值,如果骰子动画停止运行,isStoppedtrue。要创一个骰子实例,我们在DieMorph的类侧定义facts: n方法,以创建一个有n个面的新骰子。

DieMorph class >> faces: aNumber
  ^ self new faces: aNumber

initialize方法以通常的方式在实例端定义;记住,new会自动向新创建的实例发送initialize消息。

DieMorph >> initialize
  super initialize.
  self extent: 50@50.
  self
    useGradientFill;
    borderWidth: 2;
    useRoundedCorners.
  self setBorderStyle: #complexRaised.
  self fillStyle direction: self extent.
  self color: Color green.
  dieValue := 1.
  faces := 6.
  isStopped := false

我们使用了BorderedMorph的一些方法为给骰子一个漂亮的外观:一个带有凸起效果的厚边框,圆角,以及在可见面上的颜色渐变。我们定义实例方法faces:来检查一个有效的参数,如下所示:

DieMorph >> faces: aNumber 
  "Set the number of faces"
  
  ((aNumber isInteger and: [ aNumber > 0 ]) and: [ aNumber <= 9 ])
    ifTrue: [ faces := aNumber ]

在创建骰子时,检查消息发送的顺序可能是有用的。例如,如果我们从求值DieMorph faces: 9开始:

  • 类方法DieMorph class >> faces:发送newDieMorph类。

  • new的方法(由DieMorph类从Behavior继承)创建新的实例并向它发送initialize消息。

  • DieMorph中的initialize方法将faces设置为初始值6

  • DieMorph class >> new返回类方法DieMorph class >> faces:,然后将消息faces: 9发送给新实例。

  • 现在执行实例方法DieMorph >> faces:,将实例变量faces设置为9

在定义drawOn:之前,我们需要一些方法来在显示的骰子表面上放置点:

DieMorph >> face1
  ^ {(0.5 @ 0.5)}
  
DieMorph >> face2
  ^ {0.25@0.25 . 0.75@0.75}
  
DieMorph >> face3
  ^ {0.25@0.25 . 0.75@0.75 . 0.5@0.5}
  
DieMorph >> face4
  ^ {0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75}
  
DieMorph >> face5
  ^ {0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75 . 0.5@0.5}
  
DieMorph >> face6
  ^ {0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75 . 0.25@0.5 . 0.75@0.5}
  
DieMorph >> face7
  ^ {0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75 . 0.25@0.5 . 0.75@0.5 . 0.5@0.5}
  
DieMorph >> face8
  ^ {0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75 . 0.25@0.5 . 0.75@0.5 . 0.5@0.5 . 0.5@0.25}

DieMorph >> face9
  ^ {0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75 . 0.25@0.5 . 0.75@0.5 . 0.5@0.5 . 0.5@0.25 . 0.5@0.75}

这些方法为每个面定义点的坐标集合。坐标在一个大小为1x1的正方形中;我们只需要缩放它们来放置实际的点。

drawOn:方法做了两件事:它用super-send绘制背景,然后像下面这样画点:

DieMorph >> drawOn: aCanvas
  super drawOn: aCanvas.
  (self perform: ('face', dieValue asString) asSymbol)
    do: [ :aPoint | self drawDotOn: aCanvas at: aPoint ]
(DieMorph faces: 6) openInWorld.

这个方法的第二部分使用了Pharo的反射能力。绘制骰子面的点是一件简单的事,在faceX方法返回的collection上迭代,为每个坐标发送drawDotOn:at:消息。为了调用正确的factX方法,我们使用了perform:方法,该方法发送一个从字符串构建的消息,('face', dieValue asstring) asSymbol

DieMorph >> drawDotOn: aCanvas at: aPoint
  aCanvas
    fillOval: (Rectangle
      center: self position + (self extent * aPoint)
      extent: self extent / 6)
    color: Color black

由于坐标归一化到 [0:1] 区间, 我们将它们缩放到我们的骰子尺寸:self extent * aPoint。我们已经可以在Playground上创建一个die实例了(见图16-17)

为了改变显示的面,我们创建了一个访问器,可以以myDie dieValue: 5的方式使用:

DieMorph >> dieValue: aNumber
  ((aNumber isInteger and: [ aNumber > 0 ]) and: [ aNumber <= faces ])
    ifTrue: [
      dieValue := aNumber.
      self changed ]

现在我们将使用动画系统来快速显示所有的面:

DieMorph >> stepTime
  ^ 100
  
DieMorph >> step
  isStopped ifFalse: [ self dieValue: (1 to: faces) atRandom ]

现在,骰子开始滚动起来了!

要通过单击启动或停止动画,我们将使用以前尝到的关于鼠标事件的知识。首先,激活接收鼠标事件:

DieMorph >> handlesMouseDown: anEvent
  ^ true

其次,我们将通过鼠标点击交替开始和停止:

DieMorph >> mouseDown: anEvent
  anEvent redButtonPressed
    ifTrue: [ isStopped := isStopped not ]

现在,当我们点击骰子时,它会开始滚动或停止滚动。

16.12 更于画布的更多内容

drawOn:方法有一个Canvas实例作为唯一的参数;画布是morph绘制自身的区域。通过使用画布的图形方法,你可以自由地给morph一个你想要的外观。如果浏览Canvas类的继承层次结构,你会看到它有几个变体。Canvas的默认变体是FormCanvas,你可以在CanvasFormCanvas中打到关键的图形方法。这些方法可以放置和缩放绘制点、线、多边形、矩形、椭圆形、文本和图像。

也可以使用其他类型的画布,例如,获得透明的morph, 更多的图形方法,抗锯齿,等等。要使用这些特性,你需要一个AlphaBlendingCanvasBalloonCanvas。但是,当drawOn:接收一个FormCanvas的实例作为它的参数时,你如何在drawOn:方法中获得这样一个画布?幸运的是,你可以将一种画布转换成另一种。

要在画布中使用透明度0.5的DieMorph,需要重新定义drawOn::

DieMorph >> drawOn: aCanvas
  | theCanvas | 
  theCanvas := aCanvas asAlphaBlendingCanvas: 0.5.
  super drawOn: theCanvas.
  (self perform: ('face', dieValue asString) asSymbol)
    do: [ :aPoing | self drawDotOn: theCanvas at: aPoint ]

这就是你所需要做的全部!

16.13 本章总结

Morphic是一个可以动态组合图形元素的图形框架。

  • 你可以将一个对象转换成一个morph, 并通过发送morph openInWorld消息将它显示在屏幕上。

  • 你可以使用Command-Option+Shift操作morph, 点击它使用使用出现的手柄。(手柄上有帮助气球,可以解释它们的用途)

  • 你可以将一个morph嵌入另一个morph中,通过拖放或发送消息addMorph:来组合morph

  • 你可以继承一个已有的morph类,并重新定义关键方法,比如initializedrawOn:方法。

  • 你可能通过重新定义方法handlesMouseDown:, handlesMouseOver:等来控制morph对鼠标和键盘事件的反应。

  • 你可以通过定义方法step(要做什么)和stepTime(步骤之间的毫秒数)来使morph动画化。