はじめてのMorphicチュートリアル(第13回)「サブクラス化とキーイベントの処理」

クラスの機能を拡張するにはメソッドを追加していく必要があることを第5回の記事以降で学んできた。
既存のクラスの機能を維持しつつ、似たような新たな機能を持った異なるクラスを必要とする場合、第4回で学んだクラス定義によって新たなクラスを作成する。
例えば、今まで作成したMyMorphクラスと同様にScratch Catが移動していくのだが、マウスではなくキーボード操作によって移動するようなクラスが欲しい場合、MyMorphを親(基底)クラスとする新たなクラスを作成する。そうすればMyMorphの機能を引き継いだ、新たなクラスを作ることができる。
ここでは、そのような新しいクラスをMyMorph2として定義してみよう。

MyMorph subclass: #MyMorph2
    instanceVariableNames: ''
    classVariableNames: ''
    category: 'Hajimeteno Morphic Tutorial'

この新しいMyMorph2では、モーフをキーボードで操作してみよう。以前の記事ではキーボードイベントの取り扱いが中途半端になっていたので、この回でより具体的にキーボードイベントを扱う。
MyMorph2の基本的な仕様としては、上下左右の矢印キーが押されている間だけ矢印の向きにモーフが動くようにする。
キーボード入力に反応するために定義しなければならないメソッドは以下の通りである。

  • handlesKeyDown:
  • handlesKeyUp:
  • keyDown:
  • keyUp:

前2者はモーフがキーボードイベントを受け取るためにtrueを返す必要がある。後2者で受け取ったキーボードイベントを実際に処理する。まず、前2者のメソッドは以下のようになる。

handlesKeyDown: evt
    ^ true
handlesKeyUp: evt
    ^ true

これらのメソッドにより、キーボードが押された瞬間にkeyDown:メッセージがモーフに送られ、離された瞬間にkeyUp:メッセージが送られる。それぞれキーボードイベントが引数となっている。
キーボードイベントの実体はKeyboardEventオブジェクトであり、keyメッセージを送るとKeyオブジェクトが得られる。Keyオブジェクトにnameメッセージを送ると押されたキーの情報を得ることができる。少し寄り道してkeyDown:メッセージの内容を表示して、どんな内容となっているのか確認してみよう。

keyDown: evt
    self inform: evt key name

今までの3つのメソッドを実装したら以下をDo itする。

ActiveHand keyboardFocus: MyMorph2 new openInWorld.

キーボードイベントの説明のところで述べたように、キーボードフォーカスをモーフに移動しないとキーボードイベントを受け取れない。そこで、生成したモーフに強制的にキーボードフォーカスを設定している。MyMorph2が現れたら適当なキーを押してみると、押した瞬間と押し続けている間、画面の左下にメッセージが現れ、Keyオブジェクトのnameが表示される。
矢印キーのnameは以下のような文字列である。

  • KP_UP — 上矢印
  • KP_DOWN — 下矢印
  • KP_LEFT — 左矢印
  • KP_RIGHT — 右矢印

これらの値を使えば押されたキーの判定を行うことができる。
矢印キーが押されている間だけ矢印の向きにモーフを動かすとすると、押されているキーを覚えておく必要がある。そこで、MyMorph2のクラス定義を修正して、keysという名前のインスタンス変数を追加する。

MyMorph subclass: #MyMorph2
    instanceVariableNames: 'keys'
    classVariableNames: ''
    category: 'Hajimeteno Morphic Tutorial'

押されたキーを全てkeysに覚えておくため、keyDown:メッセージを受け取ったタイミングでKeyオブジェクトの名前をkeysに追加し、keyUp:メッセージで除外する。キーが押し続けられるとkeyDown:メッセージを複数受け取ることになるが、そのあたりをうまく処理するためにSetオブジェクトを用いることにする。Setは集合のようにデータを扱うためのクラスで、同じデータを複数追加しても個数はカウントされない。MyMorph2のinitializeメソッドを用いて初期化しておく。ついでにキーボードフォーカスを自分自身にセットするようにしておこう。

initialize
    super initialize.
    ActiveHand keyboardFocus: self.
    keys := Set new

モーフの移動は今までと同じくstepメッセージを受け取ったタイミングで行うが、移動量であるvecはkeyDown:とkeyUp:のタイミングで更新することにする。keysの内容に基づいてvecの値を計算するメソッドcalcVecを新たに作成する。

calcVec
    vec := 0 @ 0.
    (keys includes: 'KP_UP')
        ifTrue: [ vec := vec + (0 @ -10) ].
    (keys includes: 'KP_DOWN')
        ifTrue: [ vec := vec + (0 @ 10) ].
    (keys includes: 'KP_LEFT')
        ifTrue: [ vec := vec + (-10 @ 0) ].
    (keys includes: 'KP_RIGHT')
        ifTrue: [ vec := vec + (10 @ 0) ]

Setオブジェクトであるkeysに送られているincludes:というメッセージは、引数のデータがSetオブジェクトにあるかどうかを調べるためのものである。calcVecメソッドでは押されているキーに応じて上下左右の移動量をセットしている。
keyDown:メソッドでは、単に受け取ったキーボードイベントのKeyオブジェクトの名前をkeysに追加し、自分自身にcalcVecメッセージを送るだけである。

keyDown: evt
    keys add: evt key name.
    self calcVec

Setなど複数のデータを扱うことのできるものをコレクションと呼ぶ。コレクションにデータを追加する場合、たいていadd:メッセージを用いる。
keyUp:メソッドでは、単に受け取ったキーボードイベントのKeyオブジェクトの名前をkeysから取り除き、自分自身にcalcVecメッセージを送る。

keyUp: evt
    keys remove: evt key name ifAbsent: [  ].
    self calcVec

Setオブジェクトからデータを削除するにはremove:メッセージを使うが、削除したいデータがないとエラーになるので、ここではremove:ifAbsent:というメッセージを用いている。2番目の引数は、データがなかった場合の処理を書くが、ここでは何もしないようにしている。
以上で矢印キーにより上下左右に移動するMyMorph2ができた。
(第13回おわり)

はじめてのMorphicチュートリアル(番外編)「メソッドを整理する」

クラスを拡張してメソッドを増やして行くと、System Browserでのメソッドの一覧性が悪くなってくる。たとえば、今までにMyMorphのクラスで定義したメソッドは14個程度だが、ペインに入りきらない状況になっているだろう。
メソッド一覧
Smalltalkでは、メソッドの分類を行うためにプロトコルと呼ばれるものを用意している。プロトコル自体はプログラムの動作に何の影響もなく、単にプログラマにメソッドの分類方法を提供しているだけのものだ。とはいえ、プロトコルがなければ数十ときに数百に上るメソッドを持ったクラスのメンテナンスがとても面倒なものになってしまう。今回のチュートリアルでは、プロトコルによる分類について学ぶ。
まずプロトコルについて。以前、クラスの分類にクラスカテゴリを用いることを述べたが、プロトコルも同様のものである。座標をあらわすPointクラスを例にあげて説明しよう。
座標をあらわすPointクラスには様々なメソッドが定義されており、座標自身の情報の設定や取得に加えて、座標同士の演算や比較、変換や便利な機能など、およそ100のメソッドがある。
Pointクラス

Point methods size. "99"

プロトコルによって多数のメソッドを分類する。Pointクラスには以下のようなプロトコルがある。

Point protocols joinUsing: String cr.
 "'-- all --
copying
*Fuel
accessing
private
point functions
printing
self evaluating
transforming
interpolating
testing
geometry
truncation and roundoff
polar coordinates
extent functions
converting
truncation and round off
comparing
arithmetic'"

先頭の’– all –‘は、全てのメソッドの一覧を得る際の擬似的なものであり、アスタリスクで始まるプロトコルは他のパッケージに属しているものを表す。その他は、Pointクラスにおけるさまざまな役割や機能を表すプロトコルとなっている。
どのメソッドをどのプロトコルに配置すべきかはクラスの設計者が決定しなければならない。このあたりの判断はなかなか難しい。既存のクラスを参考にしながらどのプロトコルに配置するかを決めていく。例えば、printOn:というメッセージが他のクラスではどのようなプロトコルで定義されているか調べるには以下のようにする。

SystemNavigation default browseAllImplementorsOf: #printOn:

printOn:を定義しているクラスの一覧が表示され、クラス名の右側の丸括弧の内側に定義されているプロトコル名が表示される。
Implementers
これを見ればprintingプロトコルで定義されているのがわかる。
最近のブラウザでは親クラスの配置を参考に、自動的にプロトコルへ振り分けてくれる機能を持っている。SystemBrowserのプロトコルペインで右クリックすると、categorize all uncategorizedというメニュー項目が現れるのでクリックすると、プロトコルに分類していないメソッドを自動的に分類してくれる。
categorize methods
メニューを選んで分類が終わると以下のようになる。
categorized list
おおむねそれらしく配置されているが、handlesMouseOver:がevent handlingではなく、recategorizedに分類されているのは違和感がある。このような場合には手動で分類する。handlesMouseOver:をドラッグしながらevent handlingでドロップすれば良い。
なお、どこにも定義されていないような自分メソッドを作った場合、既存のプロトコルで適切なものがなければ自分で作成することになる。新たにプロトコルを作成するには、プロトコルペインでAdd protocol…を選べば良い。
メソッドはどのプロトコルに分類したからといって実行性能に影響するわけではない。プロトコルはクラスの開発者や利用者がメソッドを探しやすくするための工夫である。Smalltalkerは「こんな機能(メソッド)ないかな?」と思ったときにプロトコルを手がかりにメソッドを探すことが多い。うまくメソッドを分類できれば時間短縮につながるので、ひと手間かけてプロトコルに分類することには価値がある。
(番外編おわり)

はじめてのMorphicチュートリアル(第12回)「画像の表示」

今回は画像ファイルをモーフに表示する方法について学ぶ。
はじめてのMorphicチュートリアル(番外編)「ScratchCatをダウンロードする」でダウンロードしたScratch Catをモーフの表面に表示させる。Scratchのメディアサイトから画像ファイルをダウンロードしておいてもよい。いずれにせよ、ダウンロードしたファイルが展開され、画像ファイルが適当なフォルダに格納されている前提で話を進める。
ファイルに保存された画像を表示するには、画像ファイルを読み込んでFormオブジェクトを作り、それを表示するという流れで処理を行う。
画像ファイルを読み込むには以下のようにする。(ファイル名はフルパスで指定してもよい)

f1 := PNGReadWriter formFromFileNamed: 'Cat_1_Bitmap.png'.

PNGReadWriterはPNG形式のファイルの読み書きを行うためのクラスである。他にもBMPReadWriterやGIFReadWriter、AnimatedGIFReadWriter、JPEGReadWriterがある。各クラスに対し、ファイル名の引数を付けてformFromFileNamed:というキーワードメッセージを送れば、指定した画像ファイルを読み込んでFormオブジェクトを生成する。
生成して得られたFormオブジェクトから大きさやビット深度などの情報を得ることができる。

f1 extent. "(184@200)"
f1 depth. "32"

MyMorphで色のついた四角形の変わりにScratch Catを表示するなら、事前に画像を読み込んでおいてモーフ表示の際に描画する必要がある。そのため、画像ファイルを読み込んで得たFormオブジェクトは、formという名前のインスタンス変数で覚えておくことにする。
新たなインスタンス変数を追加するには、MyMorphのクラス定義を以下のように変える必要がある。

Morph subclass: #MyMorph
    instanceVariableNames: 'vec form'
    classVariableNames: ''
    category: 'Hajimeteno Morphic Tutorial'

System BrowserでMyMorphクラスを変更してAcceptするか、適当な場所で上記をDo itする。
画像ファイルはinitializeメソッドの中で(つまりモーフ生成時に)読み込むことにする。モーフ生成のたびに同じ画像ファイルを読み込むのは効率が悪いが、ここでは単純化のためにそうする。また、元のMyMorphは16×16ピクセルの大きさなので、Scratch Catを表示できない。そこで、モーフの大きさを画像の大きさに設定している。また、少し早めに動作するよう10ピクセルずつの移動とした。

initialize
    super initialize.
    vec := 10 @ 0.
    form := PNGReadWriter formFromFileNamed: 'Cat_1_Bitmap.png'.
    self extent: form extent

モーフはdrawOn:メッセージが送られると引数に与えられたキャンバスへ自分自身を描画するようになっており、現状ではモーフの大きさの色の付いた四角形を表示している。そこで、読み込んだFormオブジェクトを表示するために以下のようなdrawOn:メソッドを実装する。

drawOn: aCanvas
    aCanvas drawImage: form at: self topLeft

drawImage:at:というキーワードメッセージは2つの引数を持ち、最初に表示させたいFormオブジェクト、次に画面上の位置を指定する。
以上のような変更を加えた上で、MyMorph new openInWorldをDo itしてみると、以下のように表示される。
Scratch Cat bug
ご覧の通り、Scratch Catが表示されるものの、モーフが動くたびに分身が残って表示されてしまう。このような現象は、今回のように透明部分を含む画像を描画したり、全ての表示範囲を使わずに描画したモーフを動かす際に発生してしまう。
これを回避するには数カ所を変更する必要がある。まず必要なことは、先ほど実装したdrawOn:で、drawImage:at:の代わりにtranslucentImage:at:を用いて、透明部分の描画に対応することである。

drawOn: aCanvas
    aCanvas translucentImage: form at: self topLeft

更にモーフ自身の色が透明であるように設定する。

initialize
    super initialize.
    vec := 10 @ 0.
    self color: Color transparent.
    form := PNGReadWriter formFromFileNamed: 'Cat_1_Bitmap.png'.
    self extent: form extent

中央の行で透明色に設定している。
通常はこれだけで良いのだが、MyMorphではstepのたびに色を変えるようにしていたので、色を変えないようにstepメソッドを修正する。

step
    self topLeft: (self topLeft + vec)

以上で画像ファイルを表示させることができた。
2枚の画像を交互に表示させるのは今までの延長線上でできるので演習にしておこう。
(第12回おわり)

はじめてのMorphicチュートリアル(番外編)「ScratchCatをダウンロードする」

Cat_1_Bitmap
Scratchのキャラクターの猫をサイトからダウンロードする方法について。
Scratch Mediaのウェブページを開いて、そこから「SVG+PNG (2 Costumes)」という圧縮ファイルをダウンロードして展開するということなので、ブラウザとOS上の操作があれば解決する簡単な仕事なのだが、これをPharoでやるとどうなるかということである。
まず、http://wiki.scratch.mit.edu/w/images/ScratchCat.zip のファイルをダウンロードするには、以下のようにZnEasyクラスを使うのが簡単である。

res := ZnEasy get: 'http://wiki.scratch.mit.edu/w/images/ScratchCat.zip'.

こうすると、resにはZnResponseオブジェクトが返ってくる。問題なければ以下のような内容となる。

"a ZnResponse(200 OK application/zip 12017B)"

12017バイトのZIPファイルが得られることがわかる。
ZIPファイルの内容を得るには、contentsというメッセージを送れば良い。

res contents.

ZIPファイルをアーカイブとして読み込むには、ZipArchiveのインスタンスに対してreadFrom:でストリームを与える。

arc := ZipArchive new readFrom: res contents readStream.

アーカイブされたファイル群(メンバー)は以下のように知ることができる。

arc members.
"an OrderedCollection(a ZipFileMember(Cat_1.svg) a ZipFileMember(Cat_2.svg)
 a ZipFileMember(Cat_1_Bitmap.png) a ZipFileMember(Cat_2_Bitmap.png))"

4つの画像ファイルが入っていることがわかる。
アーカイブから特定の画像ファイルを保存するには、以下のようにすればよい。

arc extractMember: 'Cat_1_Bitmap.png'.

簡単!と思ったのだが落とし穴があった。上をDo itするとエラーが発生する。
error
どうやらZipFileMemberクラスの extractToFileNamed:inDirectory: メソッドに問題があるらしい。
エラー内容
エラーを起こしている箇所はここである。

    fullDir
        forceNewFileNamed: file basename
        do: [:stream |  self extractTo: stream]]

よく見るとメソッド内でfullDirの値を設定している箇所がない。実際、fullDirの内容はnilだし、エラーはUndefinedObject(つまりnil)に対するメッセージ送信となっている。
これは想像だが、以前にFileDirectoryをFileSystemに置き換えた際、この部分の修正が取り残されたのではないだろうか。コメントぐらい残しとけよって感じである。Pharo1.4までさかのぼってみると、想像どおりFileDirectoryを用いてfullDirが作られていた。

extractToFileNamed: aLocalFileName inDirectory: dir
    | fullName fullDir |
    self isEncrypted ifTrue: [ ^self error: 'encryption unsupported' ].
    fullName := dir fullNameFor: aLocalFileName.
    fullDir := FileDirectory forFileName: fullName.
    fullDir assureExistence.
    self isDirectory
        ifFalse: [ fullDir
            forceNewFileNamed: (FileDirectory localNameFor: fullName)
            do: [:stream |  self extractTo: stream]]
    ifTrue: [ fullDir assureExistence ]

こういうことでメゲていてはいけない。せっかく全てのソースがアクセス可能なのだから自分で何とかすることを考えるべきである。
fullDirにはforceNewFileNamed:do:というメッセージとassureExistenceというメッセージが送られている。前者は新しいファイルを作ってファイルの中身を流し込んでおり、後者はディレクトリを作っているにすぎない。これを現在のFileSystemで実現することを考える。

    self isDirectory
        ifFalse: [
            file isFile
                ifTrue: [ file delete ].
            self extractTo: file writeStream ]
        ifTrue: [ file ensureCreateDirectory ]

上記が変更部分である。self isDirectory以降を書き換える。
これで晴れてZIPアーカイブからファイルを展開することができる。

arc extractMember: 'Cat_1_Bitmap.png'.
arc extractMember: 'Cat_2_Bitmap.png'.

以下をDo itすれば、展開したファイルをモーフとして取り込むことができる。

(PNGReadWriter formFromFileNamed: 'Cat_1_Bitmap.png') asMorph openInWorld.
(PNGReadWriter formFromFileNamed: 'Cat_2_Bitmap.png') asMorph openInWorld.

(番外編おわり)

はじめてのMorphicチュートリアル(第11回)「状態」

今回はモーフに状態を持たせることについて学ぶ。
もちろん今までもモーフは大きさ、位置、色などさまざまな状態を持っていたが、それとは異なる内部的な状態を持たせたい。
例として今まで右に移動するだけだったMyMorphを、クリックするたびに90度ずつ向きを変えて移動するようにしよう。
さて、MyMorphで「移動」を実現しているのはstepメソッドである。

step
    self color: Color random.
    self topLeft: (self topLeft + (2@0))

MyMorphの左上のX座標を2増やすことで右に2ピクセル移動する。
例えばモーフが右に90度向きを変える(つまり下向きに移動する)とすれば、3行目は以下のようになるだろう。

self topLeft: (self topLeft + (0@2))

さらに90度向きを変えると左向きに進み、コードは以下のようになる。

self topLeft: (self topLeft - (2@0))

モーフの左上座標に対して、一定数の座標値を加減することで向きが変わると考えられるので、以下のようにコードを変更できる。

self topLeft: (self topLeft + vec)

つまり、座標値に加えるvecが2@0であれば右に進み、0@2であれば下に進むので、(vecは)モーフが進む方向を表す状態とみなせる。
このvecは動作しているモーフ固有の情報である。他にMyMorphモーフがあったとしても、それぞれの値は独立している。
このような値を表すのにSmalltalkではインスタンス変数を用いる。インスタンス変数はクラスを定義する際に宣言し、そのクラスに属する全てのモーフが独立した値を持つようになる。
MyMorphにインスタンス変数vecを加えるには、以下のようにクラス定義を変更する。

Morph subclass: #MyMorph
	instanceVariableNames: 'vec'
	classVariableNames: ''
	category: 'Hajimeteno Morphic Tutorial'

文字通りinstanceVariableNames:の後の文字列が、インスタンス変数の並びを表す。複数のインスタンス変数が必要ならば、スペースで区切り両端をシングルクオートで囲んで(つまり文字列として)指定する。
第4回のクラス定義と同じ方法で、クラス定義のテンプレートを上のように修正してAcceptすれば、既存のMyMorphの定義は上書きされる。上書きをしてもメソッド定義は消えないので心配しなくて良いが、動作中のモーフがあるとクラス定義の変更の影響を受ける場合があるので、あらかじめ削除しておいた方が無難である。
インスタンス変数vecを加えたMyMorphをAcceptしたら、stepの内容を以下のように変更する。

step
    self color: Color random.
    self topLeft: (self topLeft + vec)

この状態でMyMorphを動かすと、vecが未定義なためにエラーが発生する。モーフの生成と同時にvecの初期値を設定する必要がある。
そのため第6回で説明したようにinitializeメソッドを以下のように変更し、vecの初期値を2@0とする。(モーフは右に進む)

initialize
    super initialize.
    vec := 2@0.
    self extent: 16@16

クリックするたびにモーフが90度向きを変えるようにするため、mouseDown:メソッドを修正する。

mouseDown: anEvent
    self extent: self extent + (10@10).
    vec := vec y negated @ vec x

インスタンス変数vecの値を変更する部分で説明が必要なのはnegatedメッセージの意味である。negatedメッセージはメッセージを送った数値の正負を反転した値を返すメッセージである。
上記までの変更を行えば、MyMorphはクリックするたびに右へ90度向きを変えながら移動するようになる。
(第11回おわり)

はじめてのMorphicチュートリアル(第10回)「キーボードイベント」

今回はモーフでキーボードイベントを受け取る方法について学ぶ。
まず、テキスト入力などで用いるようなキー入力を処理する方法について説明する。
キーボードイベントは、keyStroke:メッセージの引数としてモーフが受け取る。受け取ったキーボードイベントに対して、keyCharやkeyString、keyValueのメッセージを送ることで、文字や文字列、文字コードを得ることができる。
例えば、受け取った文字を画面に表示したい場合には以下のようにkeyStroke:メソッドを実装する。また、マウスイベントの時と同様、handlesKeyStroke:メソッドがtrueを返して、キーボードイベントを受け取れるようにする必要がある。

keyStroke: anEvent
    self inform: anEvent keyString
handlesKeyStroke: anEvent
    ^ true

加えて、イベントを受け取るモーフがキーボードフォーカスを取得しなければならない。
Morphicでは、ActiveHandモーフがキーボードフォーカスを管理しているので、

ActiveHand keyboardFocus: モーフ

のように明示的にモーフにキーボードフォーカスを移す必要がある。
整理するとキーボードイベントを処理するには、以下の3つが必要である。

  • キーボードフォーカスを取得する
  • handlesKeyStroke:メソッドを実装してtrueを返す
  • keyStroke:メソッドを実装して、キーボードイベントを受け取る

keyDown:メッセージとkeyUp:メッセージを用いると、キーボードを押したのと離したのを区別してイベントを受け取ることができる。この場合もこれまでと同様に、handlesKeyDown:メソッドとhandlesKeyUp:メソッドをそれぞれ実装するか、これらのイベントをまとめて受け取れるようにするhandlesKeyboard:メソッドを実装する。
今回は実際に試せる例を紹介しなかった。近いうちにキーボードイベントを処理する例を紹介しようと思う。
(第10回おわり)

はじめてのMorphicチュートリアル(第9回)「マウスイベント」

モーフはそれぞれが個別にマウスイベントやキーボードイベントを受け取ることができる。
今回は、第7回までで紹介したMyMorphでマウスイベントを受け取る方法について学ぶ。
受け取ることのできるマウスイベントは以下の通りである。

  • マウスボタンを押した
  • マウスボタンを離した
  • マウスボタンを押したままマウスカーソルを動かした
  • マウスボタンを押したままである
  • マウスカーソルが領域に入ってきた
  • マウスカーソルが領域から出た

まず、マウスボタンを押した/離したのイベントを受け取る方法について説明する。この2つのイベントを受け取るには、少なくとも2つのメソッドを定義する必要がある。

handlesMouseDown: anEvent
    ^ true

上記のように、handlesMouseDown:メソッドをMyMorphに加えてtrueを返すようにすると、MyMorphの上でマウスボタンを押した時にmouseDown:メッセージが、離した時にmouseUp:メッセージがそれぞれ送られてくる。
ところで、true/falseは真理値と呼ばれYes/Noのような状態を表す。もし、上のメソッドでtrueをfalseで置き換えたらmouseDown:メッセージは送られてこなくなる。
試しに、mouseDown:とmouseUp:をそれぞれ以下のように定義してみよう。

mouseDown: anEvent
    self extent: self extent + (10@10)
mouseUp: anEvent
    self extent: self extent - (10@10)

いつものようにPlaygroundから、MyMorph new openInWorldでMyMorphを生成すると色を変えながら移動するMyMorphが現れる。モーフにマウスを会わせてクリックすると、押している間だけ大きくなり離すと小さくなる。
次にマウスボタンを押し続けた時にイベントを受け取る方法について、ドラッグ動作であれば以下のメソッドを追加すればよい。

mouseMove: anEvent
    self center: anEvent cursorPoint

上の例ならば、MyMorphを掴んで動かすとマウスポインタにあわせてMyMorphも移動するようになる。
マウスボタンを押しっぱなしにした時にイベントを受け取るには、handlesMouseStillDown:メソッドとmouseStillDown:メソッドを実装する。

handlesMouseStillDown: anEvent
    ^ true

handlesMouseStillDown:はhandlesMouseDown:と同様である。trueの前にハット(^)が書かれているのは、このメソッドがメッセージの送り主に対して、その後に続く値(この場合はtrue)を返すことを表している。実際にはどのメッセージも値を返しており、明示しない場合は送り先のオブジェクト自身(この場合はMyMorph)が返されている。

mouseStillDown: evt
    self delete

以前にも述べたように、deleteメッセージを送れば任意のモーフを消すことができる。当然、自分自身にdeleteメッセージを送れば、自分を消すことができる。
長押しの時間を設定するには、mouseStillDownThresholdメソッドを定義して、ミリ秒単位の数値を返す。

mouseStillDownThreshold
    ^ 1000

この例では、長押しでモーフが削除されるようになる。
マウスカーソルがモーフの領域内に入った/出た場合にイベントを受け取るには、handlesMouseOver:メソッドとmouseEnter:メソッド、mouseLeave:メソッドを実装する。

handlesMouseOver: anEvent
    ^ true

handlesMouseDown:やhandlesMouseStillDown:メソッドと同様に、trueを返すことでイベントが送られるようになる。

mouseEnter: anEvent
    self borderColor: Color black.
    self borderWidth: 1

マウスがモーフの領域内に入ると黒い枠線が表示されるようにした。

mouseLeave: anEvent
    self borderWidth: 0

マウスが領域外に出ると枠線が消される。
以上のような形でマウスイベントを取り扱う。
(第9回おわり)

はじめてのMorphicチュートリアル(第8回)「サブモーフ」

今回はMorphicの大きな特徴ともいえるサブモーフについて学ぶ。
サブモーフとは、モーフの中に別のモーフを埋め込むことで、これにより様々なモーフを関連づけてリッチな表現を生み出すことが可能になる。
ここでは2つのモーフa,bを例にとって説明する。
モーフaは青色で横200px、縦100pxの長方形のモーフ、モーフbは赤色で縦横20pxの正方形のモーフとすると、以下のようなコードで表現できる。

a := Morph new color: Color blue; extent: 200@100.
b := Morph new color: Color red; extent: 20@20.

以下のコードで、この2つのモーフを画面に表示する。

a openInWorld.
b openInWorld.

two morphs
この2つのモーフをクリックするとマウスにくっついて動くが、それぞれは独立していて互いに何の関連もない。
この状態で以下のコードをDo itする。

コードを選択して右クリック後にDo itを選ぶことを、今後は単に「Do itする」と表現する。メニューから選ぶ代わりにCommand-D(Alt-D)を押しても同じである。

a addMorphBack: b.

上記のようにすると青色の長方形モーフaと赤色の正方形モーフbが一緒に動くようになる。これは、モーフaにモーフbが埋め込まれた状態で、いわばモーフaとモーフbが親子関係になったと言える。このとき、モーフbをモーフaのサブモーフと呼ぶ。
興味深いのは、コードをDo itした時点で埋め込まれたモーフbと外側のモーフaとに、レイアウト上何の変化も生じず、それまでの位置状態を保ったままだったことである。Morphicでは特に指定しない限りレイアウトのない状態となっている。
モーフbがモーフaのサブモーフである状態で、モーフbの位置を変えることができる。

b center: a center.

上記のコードはモーフbの中心を、モーフaの中心にあわせる。この後でモーフaを移動させると、モーフbはモーフaの中心に位置したまま一緒に移動する。
centered morph
今度はモーフaの位置を変えてみる。

a topLeft: 20@20.

上記のコードは、左上端が(20,20)という座標になるようにモーフaを移動する。この場合もモーフbはモーフaの中心に位置したまま一緒に移動する。
サブモーフになったモーフは親のモーフと一心同体となる。つまり、親のモーフを削除するとそのサブモーフも自動的に削除される。
モーフは他のモーフを何個でもサブモーフにすることができる。さらにサブモーフも別のモーフをサブモーフにすることができる。
ただし、サイクリックなサブモーフ化はできない。例えば以下のコードをDo itすると(しないほうがいいが)一瞬でハングする。

b addMorphBack: a. "注意:Do itするとハングする"

モーフ側で何のチェックも行われないので、サイクリックなサブモーフ化には気をつけるほうがいいだろう。
(第8回おわり)
【追記】
サブモーフの配置はMorphicで最も頭を悩ませるものの一つである。これに関しては以前にまとめた記事があるので参照してほしい。
Morphの設定

はじめてのMorphicチュートリアル(第7回)「stepping」

今回は、前回作成したMyMorphを使って簡単なアニメーションを作成する。
Morphicではアニメーションを実現する仕組みとしてsteppingが用意されている。
steppingとは、一定の時間間隔でモーフにstepメッセージが送られるという簡単な仕組みのことである。
メインの動作とは別に、モニタした値を定期的に表示することなどが、新たなスレッドを使わずに実現できる。
stepというメソッドを実装すればsteppingを始めることができる。

step
    self color: Color random

第5回で説明したように、System BrowserでMyMorphを表示させ、initializeと同じ方法で上のstepメソッドをMyMorphに追加する。
Pharo2.0以降ならば、Acceptした瞬間からMyMorphの色がランダムに変わっていくのを観察することができる。
古いPharoやSqueakならば以下のメソッドを追加する必要があるかもしれない。

wantsSteps
    ^ true

デスクトップ全体を表すモーフであるWorldMorphの描画サイクルの一部として、stepメッセージが送られてくるため、モーフ側ではstepを実装する以外に設定する必要がない。
デフォルトでは1000ミリ秒間隔でstepメッセージが送られてくるが、その間隔はstepTimeメソッドにミリ秒単位で設定できる。

stepTime
    ^ 500

上のメソッドをMyMorphに登録すると、stepメッセージが500ミリ秒ごとに送られるようになる。
それでは、steppingを使ってモーフを動かしてみよう。
第3回で説明したモーフの位置設定の方法を利用して、先ほどのstepの定義を以下のように変更してみる。

step
    self color: Color random.
    self topLeft: (self topLeft + (2@0))

すると、Acceptした瞬間からゆっくりとMyMorphが右に移動していくのがわかる。MyMorphは500ミリ秒ごとに色を変えながら、右に2ピクセル移動している。
Playgroundから適当な間隔を置いて以下の式をDo itすると、Command-D(Alt-D)を押すと複数のMyMorphが色を変えながら移動していく。

MyMorph new openInWorld.

モーフは画面外に出てしまっても動き続けるので、MyMorphの全てを一度に削除する方法を教えよう。

MyMorph allInstances do: [ :each | each delete ].

モーフに対してdeleteメッセージを送れば、画面から消すことができる。

YouTube動画はこちら
(第7回おわり)

はじめてのMorphicチュートリアル(第6回)「initializeメソッド」

前回MyMorphに追加したinitializeメソッドを見てみよう。

initialize
    super initialize.
    self extent: 16@16

1行目は何のメソッドの定義なのか示しており、この場合は追加したい単項メッセージのinitializeを書いている。
2行目はsuper initializeとなっている。第4回で説明したように、モーフとしての性質をMorphから受け継ぐために、MyMorphの親クラスをMorphに設定している。initializeメッセージを受け取ったときにモーフとして行うことは、Morphクラスのinitializeが定義しているので、super initializeというメッセージを送って利用している。このように、MyMorphの親クラス(この場合Morph)で定義されたメソッドを使う場合にはsuperに対してメッセージを送るようにする。
3行目は、第2回で学んだモーフの大きさを変えるメッセージ extent: を用いて、モーフの大きさを16×16ピクセルにしている。
以上、メソッド本体の2行によって、モーフの初期化を完了させつつ大きさを設定していることがわかる。
続いてモーフの色も変えてみよう。モーフの色は、colorメッセージを用いて取得し、色を引数としてcolor:メッセージを送ることで設定できる。

Morph new color. "Color blue"

MyMorphの色を赤に変えたければ、initializeメソッドを以下のように変更すればよい。

initialize
    super initialize.
    self extent: 16@16.
    self color: Color red

今まで明示的には述べてこなかったが、モーフ(や他のオブジェクトに)メッセージを送る式をメッセージ式と呼ぶ。例えば、super initializeやself extent: 16@16などがメッセージ式にあたる。複数のメッセージ式は、ピリオド(.)によって区切る必要がある。最後のメッセージ式についてはピリオドを省略しても良いため、上のself color: Color redの後にはピリオドがない。
なお、1行目のメッセージの定義部分や、後で紹介する値を返却する部分などにはピリオドを付けてはいけない。
PlaygroundでいつものようにMyMorphを生成すれば、画面上に小さな赤い正方形が現れるだろう。

MyMorph new openInWorld.

blueやredのように、Colorクラスに送って作りだせる色の種類は以下のようにして知ることができる。

Color class selectorsInProtocol: 'defaults'.

上記を選んでCommand-P(Alt-P)を押すと、コメントアウトされたメッセージ名が表示される。
"#(#black #blue #brown #cyan #darkGray #defaultColors #defaultColors2 #defaultColors3 #defaultColors4 #gray #green #lightBlue #lightBrown #lightCyan #lightGray #lightGreen #lightMagenta #lightOrange #lightRed #lightYellow #magenta #orange #paleBlue #paleBuff #paleGreen #paleMagenta #paleOrange #palePeach #paleRed #paleTan #paleYellow #pink #purple #red #tan #transparent #veryDarkGray #veryLightGray #veryPaleRed #veryVeryDarkGray #veryVeryLightGray #white #yellow)"
蛇足だが、固定的な色ではなく毎回異なる色にしたい場合には、Color randomを用いる。

initialize
    super initialize.
    self extent: 16@16.
    self color: Color random

こうすると、MyMorphを生成するたびにいろんな色で正方形が表示される。
(第6回おわり)