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.
通过浏览器定义一个继承自Morph
的CrossMorph
类:
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:
方法来完成这项艰巨的任务。
上面的代码中有两个问题。
最明显的是,我们有重复的代码。这是一个基本错误:如果我们发现需要更改horizontalBar
或verticalBar
的计算方式,我们很可能会忘记对这两个方法进行同步更改。解决方案是将这些计算分解为两个新方法,我们将其放入私有协议中:
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
定义了redButtonPressed
和yellowButtonPressed
方法。浏览这个类,看看它提供了什么别的方法来查询鼠标事件。
16.7 键盘事件
要捕获键盘事件,我们需要三个步骤。
将键盘焦点交给某个Morph.例如,当鼠标移动到Morph上时,我们可以让它获得焦点;
使用
keyDown:
方法处理键盘事件本身。当用户按下一个键时,该消息将被发送到拥有键盘焦点的Morph;当鼠标不在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
事件来管理文本,而快捷方式则在KeyUp
和KeyDown
事件中管理。
为了处理击键事件,我们需要一个方法来在按键被按下时得到通知,以及另一个方法来实际处理击键事件。
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中创建ReceiverMorph
和EllipseMorph
的实例:
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
记录当前显示的面的值,如果骰子动画停止运行,isStopped
为true
。要创一个骰子实例,我们在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:
发送new
给DieMorph
类。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
,你可以在Canvas
和FormCanvas
中打到关键的图形方法。这些方法可以放置和缩放绘制点、线、多边形、矩形、椭圆形、文本和图像。
也可以使用其他类型的画布,例如,获得透明的morph, 更多的图形方法,抗锯齿,等等。要使用这些特性,你需要一个AlphaBlendingCanvas
或BalloonCanvas
。但是,当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类,并重新定义关键方法,比如
initialize
和drawOn:
方法。你可能通过重新定义方法
handlesMouseDown:
,handlesMouseOver:
等来控制morph对鼠标和键盘事件的反应。你可以通过定义方法
step
(要做什么)和stepTime
(步骤之间的毫秒数)来使morph动画化。