GO BACK

Squeak教室 上級編その4

「オリジナルモーフの作成」



■モーフのサブクラスを作る

 Squeakの一番のウリは、なんといってもモーフです。さまざまな部品が手軽に使える、あの環境は他に変えがたいものがあります。せっかくSmalltalkを使えるようになったのですから、オリジナルのモーフ部品に挑戦してみたいですね。

 モーフといえども、Smalltalkのクラスとして用意されていることに変わりはありません。もちろん、非常に多くの機能が組み込まれていますから、その全てを実装しようとすると膨大な作業になります。が、別にそれらを全て作る必要はありません。ほら、継承を使えば、受け継いだクラスの全機能を使うことができたでしょう? だから、モーフの基本クラスを継承して新しいクラスを作れば、基本的な事柄はすべてそっちがやってくれるはずです。後は、そのモーフ特有の機能だけを組み込めばいいのですよ。

 では、簡単なモーフを作ってみましょう。作成するのは「ぐるぐるモーフ」です。楕円のモーフというのがありますが、あんな形でぐるぐると何何重にも円が重なり表示されるモーフを作ってみることにしましょう。

 では、デスクトップのワールドメニューの「開く」から「package browser」を選び、ブラウザを呼び出して下さい。そして、モーフのクラス「MyMorph」を作ってみましょう。前に、ワークスペースからMyTestClassを作りましたが、ブラウザを使ってもやり方は同じです。クラスを作成するコードを書き、それを実行すればいいのです。

 先に作成した「Tuyano」の「TestClasses」カテゴリを選択してみて下さい。その下のコード編集のエリアに「Object subclass: #NameOfSubclass・・」といったテキストが表示されるはずです。これは、クラス定義のテンプレートです。つまり、「こんな形で書けばクラスができるよ」ということを示していたんですね。


 では、この部分にクラス定義を書き込みましょう(このテンプレートのテキストは消しちゃっていいです)。以下のコードを記述して、このエリアでescキーを押して現れるメニューから「了解」を選びます。これで、新しいクラス「MyMorph」が、Tuyano-TestClassカテゴリ内に作成されます。

Morph subclass: #MyMorph
instanceVariableNames: 'frameColor frameWidth '
classVariableNames: ''
poolDictionaries: ''
category: 'Tuyano-TestClasses'



 今回のクラスは、「Morph subclass: #MyMorph」となっていますね? つまり、「Morph」というクラスを継承して作っていることになります。このMorphクラスが、モーフの最も基本となるものです。Squeakで使われているモーフは、全てこれを継承して作られているのです。

 また、今回は「instanceValiableNames」というところに「frameColor frameWidth」という2つのものが書かれていますね? これは「インスタンス変数の変数名一覧」を示すものです。つまり、この新しいクラスMyMorphでは、「frameColor」と「frameWidth」という2つのインスタンス変数を持っている、というわけです。

 インスタンス変数というのは、タイルスクリプティングのところでも登場しましたが、「それぞれのインスタンス(つまりオブジェクト)ごとに値を持てる変数」のことです。これとは反対に「クラスごとに1つの値しか持てない(つまりすべてのインスタンスで同じ値になる)変数」を「クラス変数」といいます。インスタンス変数を用意すれば、作成したオブジェクトごとに細かな設定などをそこに保存しておけます。今回は、フレーム(輪郭線)の色と太さをインスタンス変数で持たせることにしました。



■メソッドを作る

 では、次にモーフの機能を組み込んでいくことにしましょう。機能というとわかりにくいですが、オリジナルのモーフということを考えると、「どんなふうに表示されるか」「どんなイベントでどんな処理を実行するか」といったことが思い浮かびますね。最低、この2つが用意されていないと、「これが俺の作ったモーフだ」と自信を持っていえない感じがあります。

 こうした機能は、「それに対応するメソッドを用意する」ことで割と簡単に実現できます。——モーフの土台となるMorphクラスには、表示の更新が必要になったり、さまざまな操作が行われたりすると、それに対応するメソッドを呼び出すような仕組みがあらかじめ組み込まれています。ですから、それに対応するメソッドを用意するだけで、こうした機能を実装させることができるのです。

 では、今回必要となるメソッド類を整理してみましょう。ざっとこんなものを用意することにします。


 では、簡単なところで「インスタンス変数用のメソッド」からいきましょう。インスタンス変数というのは、直接値をやり取りすることもできるんですが、値を取り出したり変更したりするための専用のメソッドを用意して、それを使って操作するのが普通です。そうすることで、おかしな値が設定されたりするのを防ぐことができるからです。

 では、MyMorphクラスにメソッドを作りましょう。作り方は覚えてますか? ブラウザから「MyMorph」クラスを選び、その下の「Instance」ボタンが選択されていることを確認します(今回は、インスタンスから呼び出して使うメソッドなので、ClassでなくInstanceのほうに作ります)。そして、escキーで現れるメニューから「new category」を選んでカテゴリを用意します。今回は、「event handling」というものを1つ用意しましょう。面倒なので、ここに全メソッドを放り込んでおくことにします。そして下のエリアにコードを記述し、escキーのメニューから「了解」を選べばいいんでしたね。

 作成するメソッドは、計4つになります。以下にリストをあげるので、頑張って作りましょう。

frameColor
^ frameColor
frameColor: t1 
frameColor _ t1
frameWidth
^ frameWidth
frameWidth: t1 
frameWidth _ t1

 まあ、どれも単純なものなのですぐにわかりますね。frameColorとframeWidthはインスタンス変数の値をただ返すだけですし、frameColor:とframeWidth:は引数の値をインスタンス変数に設定するだけです。



■初期化と部品の描画

 では、それ以外の「初期化」「画面表示」「マウスプレス時の処理」といったものに進みましょう。これらは、あらかじめモーフに用意されているイベントによって自動的に呼び出されるメソッドです。これらは、「こういうときにはこのメソッド」ということが決まっているので、メソッド名やキーワードなどが少しでも違うとうまく機能しません。また値を返すものなどは指定された型の値を返さないとトラブルが起きたりします。基本的にイベント関係のメソッドは「決められた通りの形で書く」のが基本、と思って下さい。

 では、「インスタンスの初期化」の処理からいきましょう。これは、「initialize」というメソッドで行ないます。ここにインスタンス変数の初期値の設定などの処理を記述しておけば、新たにインスタンスが作られる際にこのメソッドが呼び出され、自動的に初期化作業ができるというわけです。

initialize
bounds _ 0 @ 0 corner: 50 @ 40.
owner _ nil.
submorphs _ EmptyArray.
color _ self defaultColor.
frameColor _ Color red.
frameWidth _ 5

 1つ、よくわからないのは「bounds」というやつですね。これは部品の領域を示すものなんですが、「0 @ 0 corner: 50 @ 40」といったものが設定されています。これはSmalltalkで位置や大きさを指定する際に用いられる値なのです。Smalltalkで位置や大きさを指定する時にはどのように書くか、整理するとこうなります。

 領域の指定は2つの方法があります。corner:で右下の位置を指定するのと、extent:で縦横幅を指定するものです。どちらでも可能ですので、状況に応じて使い分けるとよいでしょう。

 さて、次は「モーフの表示」に進みましょう。表示といってもさまざまな状況があるのですが、もっとも基本となる表示処理は「drawOn:」というメソッドを使うことにします。ここに部品を描画する処理を用意すれば、それにより部品の表示が描かれるわけです。

drawOn: t1 
| x y w h x1 y1 w1 h1 m n |
x _ self left.
y _ self top.
w _ self width.
h _ self height.
m _ self frameWidth * 4.
m < 1
ifTrue: [m _ 1].
w > h
ifTrue: [n _ h // m]
ifFalse: [n _ w // m].
n < 1
ifTrue: [n _ 1].
0
to: n
do: [:i |
x1 _ w / 2 // n * i.
y1 _ h / 2 // n * i.
w1 _ x + w - x1.
h1 _ y + h - y1.
w1 <= 0
ifTrue: [w1 _ 1].
h1 <= 0
ifTrue: [h1 _ 1].
t1
fillOval: (x + x1 @ (y + y1) corner: w1 @ h1)
color: self color
borderWidth: self frameWidth
borderColor: self frameColor]

 ちょっと長いですが、それほど複雑なわけではないので間違えないように書きましょう。ここでは、部品を描くのに新しいものが登場してますね。最後のdo: のブロック文にある「t1 ・・」というやつです。これは、整理するとこんなものになります。

t1 fillOval: 領域 color: borderWidth: 輪郭の太さ borderColor: 輪郭色

 最初の「t1」は引数で渡されるものですが、これはCanvasというオブジェクトが設定されています。このCanvasにある描画用のメソッドを呼び出すと、このモーフに描画がなされるような仕組みになっているのです。今回は「fillOval」という塗りつぶした円を描画するメソッドを使いましたが、この他に輪郭のみの円を描く「frameOval」や、四角形を描く「fillRectangle」「frameRectangle」といったものも用意されています。

 それから、最初のところで「self left」・・というようにして、自身の位置と大きさの値を変数に取り出しているところがありますね。この「self」っていうのは、自分自身(つまりこのインスタンス自身)を示すものです。また、「left」「top」「width」「height」は、それぞれ横位置・縦位置・横幅・縦幅を得るメソッドです。

 さあ、これで部品の表示だけはできるよになりました。とりあえず、実際に部品が動くか確かめてみましょう。ワークスペースを開き、そこに「MyMorph new openInHand」と書き込んで評価してみましょう。マウスポインタのところにMyMorphが現れますよ。


 そのまま適当な場所に配置し、ハロを呼び出して大きさを変えてみましょう。大きくすると、自動的にぐるぐるが増えて、常に部品の中をぐるぐる模様でいっぱいにします。どうです、自分だけのオリジナルなモーフが動いているのは、ちょっと感動的でしょう?



 ところで、クラスからインスタンスを作る場合には、通常「new」というのを使うんですが、今回は「openInHand」というのを使ってますね。これ、単に「マウスポインタのとこにできたほうが便利だろう」というだけの問題ではありません。

 前にいったように、Squeak・トーイってのは、モーフの部品の他に、パネルスクリプティングなどで操作するための「プレーヤー」というものがあります。なので、単純にモーフのインスタンスを作っただけだと、プレーヤーがなくていろいろ面倒になっちゃうんです。なので、この「openInHand」など、あらかじめ用意されているメソッドでインスタンス生成して使うのが基本のようです。


■イベント処理を作る

 では、最後に「マウスでプレスしたらインスタンス変数の値を変更する」という機能を組み込みましょう。これは、2つのメソッドからなります。1つは「handlesMouseDown:」というもの、もう1つは「mouseDown:」です。これらは、以下のようになります。

handlesMouseDown: evt 
evt shiftPressed
ifTrue: [^ true].
^ false

mouseDown: evt 
| str1 n c |
str1 _ FillInTheBlank request: 'width of frame:'
initialAnswer: self frameWidth asString.
str1 = ''
ifTrue: [^ self].
n _ str1 asInteger.
self frameWidth: n.
str1 _ FillInTheBlank request: 'frame color name:'
initialAnswer: 'red'.
str1 = ''
ifTrue: [^ self].
c _ Compiler evaluate: 'Color ' , str1.
self frameColor: c.
self flash.
PopUpMenu notify: 'set now'

 この2つのメソッドは、handlesMouseDown:が「マウスダウンのイベントを受け付けるか」、mouseDown:が「マウスダウンのイベント処理」となります。イベント処理は、まずそのインスタンスがイベントを受け付けるどうか確認し、受け付けるようならば対応するメソッドを呼び出すようになっています。handlesMouseDown:でtrueを返すと「このイベントを受け付ける」と判断され、mouseDown:が呼び出されるのです。もし、handlesMouseDown:でfalseが返されていたら、mouseDown:は呼び出されません。

 今回は、普通にhandlesMouseDown:でtrueを返すと常にmouseDown:の処理が呼び出されるようになってうっとおしいので、シフトキーを押していたときだけイベントを受け付けるようにしました。「shiftPressed」というのがシフトキーの状態を調べるものです。これはイベントのオブジェクトevtにあるメソッドで、このようにevt内のメソッドを呼び出すことで、発生したイベントに関する情報を得ることができます。

 今回やっているのは、FillInTheBlank request:を使って輪郭線の太さと色の名前を入力してもらい、それをもとにインスタンス変数の値を変更して、部品の表示を変える、というものです。まあ、輪郭線の太さを変更するのは、数字を入力してもらえばいいのでわかりますね。問題は、「色の名前」を入力してもらって、どうやって色を変更するか、です。

 それには、「Compiler evaluate:」というものを使います。これはコンパイラのクラスメソッドで、引数のテキストをその場で評価実行するものなのです。これを利用し、「Color red」というような文をテキストで用意してやれば、Colorインスタンスを作成することができます。後はこれをframeColorに設定すればいい、ってわけです。

 最後に、部品の表示を更新しておく必要がありますが、これは「self flash」というようにして、部品オブジェクトのflashを呼び出します。

 さあ、これでイベント処理ができました。MyMorphの部品をマウスでクリックしてみましょう。普通にクリックすると、そのまま部品をドラッグして移動できますが、シフトキーを押しながらクリックすると、画面にダイアログが現れ、輪郭線の太さと色を尋ねてきます。これでそれぞれ入力していけば、そのMyMorphの表示を変えることができます。まあ、色の名前がわからないとエラーが出たりしますが、とりあえず動けばオッケー、ってことで。



 まだ基本的な仕組みだけしか作ってませんが、とりあえずこれで「モーフの部品がどうやって動いているか」という、その基本的な仕組みの一端ぐらいは見えてきたんじゃないかと思います。

 部品の描画やマウスイベントは、ここであげたdrawOn:とmouseDown:だけで行っているわけじゃありません。他にもさまざまな状況に応じて呼び出されるメソッドが多数定義されています。一度、ブラウザでMorphクラスの中身を覗いてみると、どんな機能が組み込まれているのか見えて勉強になりますよ。


GO NEXT


GO HOME