Pharo (Smalltalk) で Morph を使って "大石泉すき" を表示する

これは<a href="https://qiita.com/advent-calendar/2019/ohishi-izumi-suki">「大石泉すき」 Advent Calendar 2019</a> 16日目の記事です。私は趣味でたまにプログラミングを嗜む程度なので、他の参加者様方の高品質な記事に震えています。

## この記事で何をするか

![output-palette3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/6fb700d3-1446-956b-2c84-a8ccfa690213.gif)
大石泉をクリックして "idol++" してもらうと、周りにいるオタクが反応して "大石泉すき" とつぶやくプログラムを Pharo (Smalltalk) で実装します。内容の深さは、記事を追って行けば皆さま自身の環境で同じプログラムが動く、という程度にし、動作の中身について詳しくは突っ込みません。ちなみに私は Pharo 初心者なのでそのつもりでお読みいただけますと幸いです。

内容を理解しながらこの記事を読まれたい方は、<a href="https://qiita.com/advent-calendar/2014/smalltalk">Smalltalk Advent Calendar 2014</a> の初心者向けの記事などを参照されながら読まれると良いと思います。



## 環境

+ OS: macOS Catalina 10.15.1
+ Pharo 7.0.4 (PharoLauncher)


## Pharo についてと環境構成

Pharo とはオブジェクト指向の始祖と噂される <a href="https://ja.wikipedia.org/wiki/Smalltalk">Smalltalk</a> の一方言のようなものです。Smalltalk の開発者らが立ち上げた <a href="https://squeak.org">Squeak</a> というプロジェクトからフォークしたもので、実用に耐えうる Smalltalk を目指しているようです。余談ですが本家 Smalltalk 直系の環境 <a href="http://smalltalk.cincom.jp/main/products/visualworks/">VisualWorks</a> も存命です。

### インストールとイメージの起動

まずは Pharo.org へ行きましょう。
<a href="https://pharo.org">https://pharo.org</a>

ここから Pharo Launcher をダウンロードしてきて、アプリケーションフォルダに入れます。上手いこと起動させると次のような画面が表示されます。
<img width="488" alt="launcher.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/3ff524ae-432f-602b-2a3e-7248d40fa308.jpeg">
Smalltalk 環境は仮想化よろしく、イメージファイルを VM (仮想マシン) に読み込ませることで起動します。

Pharo 7.0 - 64bit の上で右クリックし、create image を選択します。するとイメージの名前を入力するよう促されます。何を入力しても大丈夫ですが、ここでは IzumiSuki にしておきます。

次に環境を起動してみます。右側に現われた IzumiSuki を選択し、右上の緑色の矢印をクリックします。
<img width="488" alt="launcher2.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/c5f20f47-a36b-fbb9-6206-202c748c2d5d.jpeg">
すると次の画面が現われます。Pharo ではこの環境の中でプログラミングをして行くことになります。
<img width="488" alt="スクリーンショット 2019-12-16 4.39.24.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/a86cf65d-3a6a-d142-59b8-c20e67cb6989.jpeg">
Welcome スクリーンは閉じてしまってもかまいません。

### 日本語環境の設定

Pharo のデフォルトの状態では日本語を表示することが出来ません。このままではどうしようもないので、まずは日本語のフォントを設定します。

何もないところでクリックをし、World メニューを開きます。
![world.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/cbcba457-bc29-9efc-9284-8c116d6a0121.gif)
Pharo -> Settings を開きます。すると Appearance に Standard fonts という項目がありますので、Default の Source Sans Pro Regular 10 ボタンをクリックします。右下の Update を押すとシステムフォントが読み込まれるので、日本語が表示出来るフォントを選択し、OK ボタンを押します。Settings ウインドウに戻ったら、先ほど選んだボタンの右側にある Force all ボタンをクリックします。これで Pharo で日本語が表示出来るようになりました。Settings Browser は閉じてしまってかまいません。

### Pharo の終了方法

World メニューを開き、Pharo -> Save and quit を選択すると終了します。これは "そのままの状態" を保存してくれますので、休憩したくなったときなどはこれで終了しても、次回起動したとき続きからすぐ始めることが出来ます。


## とりあえずの大石泉すき

### Playground と Transcript

今回はグラフィックを使って "大石泉すき" しますが、実はシェルで実行するように文字を出力することもできます。まず Playground と呼ばれるウインドウを開きます。World メニューを開き、Tools -> Playground をクリックします。また同様に Tools -> Transcript をクリックし、Transcript ウインドウを開きます。
Playground に次を入力します。

```Smalltalk:Playground
Transcript show: '大石泉すき'.
```

これは 「Transcript オブジェクトに show: '大石泉すき' というメッセージを送る」という文です。マウスをドラッグしてこの行を選択したあと、右クリックで開いたメニューの Do it をクリックします。
![transcript.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/78d2da37-c35b-6617-4c0c-f7800caa1352.png)
Transcript ウインドウに "大石泉すき" が出力されました！


### もう少し Pharo っぽく

既に大石泉すきが出力されましたが、さすがにこれではつまらないのでもう少し Pharo っぽくやってみます。Playground に次を入力して Do it します。

```Smalltalk:Playground
'大石泉すき' asMorph openInHand.
```

<img width="301" alt="スクリーンショット 2019-12-16 5.08.35.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/1c04b5c5-5b70-198c-e1ec-4f64f9f4c8ef.jpeg">
するとマウスカーソルのところに大石泉すきが現われました！このテキストはクリックをするとその場所に固定されますが、再度このテキストをクリックをするとまた移動することができます。これが StringMorph と呼ばれるものです。Morph には色々な種類があり、テキストボックスやスクロールバーなどの Morph もあります。Pharo はこのような Morph を組み合わせることで UI を構成しています。今回は小さな Morph を組み合わせることで、もう少し豪華に "大石泉すき" をするのが目的です。
Morph は Option + Shift + クリック によって ハロ と呼ばれるメニューを出すことが出来て、ここから移動やら複製やらの操作をすることが出来ます。 また Morph はハロ左上の×ボタンをクリックすることで消すことが出来ます。
<img width="287" alt="halo.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/40902ff1-4ede-65f3-9a93-6b4695121c93.jpeg">


## 吹き出しを出すクラスを作る

ここまでで既にお腹いっぱいかもしれませんが、ここからが本番です。最初にお見せしたプログラムを作るためにクラスを定義して行きます。

さて、大石泉は超絶美少女アイドルで一方オタクは魑魅魍魎ですが、一応同じ人間なのでクリックをすると吹き出しを出す共通のスーパークラスを作ります。Pharo にはクリックしたときに見た目を変化させる機能を提供する SimpleSwitchMorph (下の gif)がありますので、そのサブクラスとして定義します。
![simplemorph.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/e5544aae-e8ff-0f25-6d5f-c1730989843e.gif)


### 最初のクラス定義

World メニューを開き(何もないところでクリック)、Tools -> System Browser をクリックします。クラスの定義は普通、この System Browser と呼ばれるウインドウで行ないます。既に大量のクラスが定義されていてめまいがしますが、まずは今回定義するクラスを入れるパッケージを作ります。
一番左のペインで右クリック、出てきたメニューから New package をクリックして適当な名前を入力します。ここでは IzumiSuki にしておきます。
<img width="538" alt="category.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/d8a21708-647a-6b66-6a28-5b8e51369b17.jpeg">

クラスを定義するには、IzumiSuki パッケージを選択したあと下側のテキストボックスにクラスの情報を入力し、最後に Accept を行ないます。試しに次の文を入力してみて下さい。

```Smalltalk:System-browser
SimpleSwitchMorph subclass: #IMChara
	instanceVariableNames: 'msgBalloon message performing'
	classVariableNames: ''
	package: 'IzumiSuki'
```
Command+S もしくは右クリックでメニューを開いて Accept を選択するとクラスの定義が確定します。
<img width="430" alt="accept.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/4a1d3be9-863a-8bdb-875c-782ed6656dd1.jpeg">
これでクラスを定義することが出来ました。
少し内容を詳しく見てみますと、1行目はクラスの名前とどのクラスのサブクラスであるかを表わしています。今は SimpleSwitchMorph のサブクラスとして IMChara クラスを定義しました。2行目はインスタンス変数、3行目はクラス変数です。実際はあらかじめ定義しておかなくても、未定義の変数を使用したときに Pharo の方からインスタンス変数として定義するかを聞いて来ます。4行目はクラスが所属するパッケージ名です。

次にメソッドを定義します。左から3番目のペインにある instance side を選択します。すると下のテキストボックスがメソッド定義用のものに変化します。次の文を順番に入力し、メソッドの定義が終わるごとに Command+S もしくは右クリックでメニューを開いて Accept を選択し、確定させます。（1行目の IMChara>> はどのクラスのメソッドであるかを表わしているものなので入力しないでください。）

```Smalltalk:System-browser
IMChara>> initialize
    super initialize.
    self label: ''.
    self borderWidth: 0.
    bounds := (0 @ 0) corner: (100 @ 100).
	
    message := 'hello, world'.
    msgBalloon := BalloonMorph string: message for: self.
	
    self color: (Color transparent).
    self useSquareCorners.
    self turnOff. 
```
<img width="444" alt="initialize.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/4654c795-35be-e603-56d3-efab37dbf6e8.png">



initialize メソッドは他の言語と同じように、オブジェクトを生成したとき最初に呼ばれるメソッドです。ここでは bounds 変数に代入を行なうことでサイズを変更し、吹き出しを出すための Balloon を生成してインスタンス変数 msgBalloon に代入しています。また自分自身の色を透明にしています。

```Smalltalk:System-browser
IMChara>> mouseDown: evt
```
```Smalltalk:System-browser
IMChara>> mouseUp: evt
    self setSwitchState: (performing not)
```

これらはマウスクリックのイベントで実行されるメソッドです。mouseDown はスーパークラスで定義されている動作を行なわないために定義しています。

```Smalltalk:System-browser
IMChara>> message: aString
    message := aString.
    self refreshBalloon.
```
```Smalltalk:System-browser
IMChara>> refreshBalloon
    msgBalloon := BalloonMorph string: message for: self.
```
```Smalltalk:System-browser
IMChara>> turnOff
    performing := false.
    msgBalloon delete.
```
```Smalltalk:System-browser
IMChara>> turnOn
    performing := true.
    msgBalloon openInWorld.
```

基本的には SimpleSwitchMorph のメソッドをオーバーライドしています。新しいメソッドは Balloon 用です。


### 動作確認

ちゃんとクラスが定義出来ているか確認するため、一度表示させてみましょう。Playground を開きます（World -> Tools -> Playground を選択）。ただ initialize メソッドで IMChara の色を透明にしているため、このまま表示しても "何も見えないところをクリックしたら突然「こんにちは世界」と言ってくる謎の物体" になってしまいますので、表示させる前に色をいじります。

```Smalltalk:Playground
IMChara new.
```
行を選択し、右クリックから Inspect it を選びます。するとインスペクタが開き、ここからオブジェクトをいじることが出来ます。インスペクタウインドウ下のテキストボックスに次の通り入力し、行を選択したら右クリックし Do It します。

```Smalltalk:Inspector
self color: (Color black).
```

すると上側に表示されている color の項目が黒に変化します。これでようやくオブジェクトを表示させても目に見えるようになりました。下のテキストボックスで次を Do It します。

```Smalltalk:Inspector
self openInHand.
```

これでマウスポインタのところに黒い物体が現われたと思います。この物体は適当な場所でクリックすることでその場所に固定されます。一度クリックをして固定したらもう一度この黒い四角をクリックします。
![output2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4967/60c9f591-48aa-f784-deaa-457b87e01c06.gif)
これで世界にあいさつをする暗黒物質が誕生しました。しかしオタクはまだしも、大石泉はもちろん暗黒物質ではないのであとで大石泉のサブクラスを定義します。


## LiveStage クラス

大石泉がオタクと繋がるためのライブステージを定義します。これも凝れば綺麗なライブステージが作れますが、今回は省エネでいきます。

システムブラウザを立ち上げ IzumiSuki カテゴリを選択したら下のテキストボックスに次を入力して Accept します。

```Smalltalk:System-browser
Object subclass: #LiveStage
	instanceVariableNames: 'performer listeners'
	classVariableNames: ''
	package: 'IzumiSuki'
```

performer に大石泉が、listeners にオタク達が入ります。
続いて次のメソッドを定義します。

```Smalltalk:System-browser
LiveStage>> initialize
	super initialize.
	performer := nil.
	listeners := OrderedCollection new.
```

```Smalltalk:System-browser
LiveStage>> joinListener: anOtaku
	listeners add: anOtaku.
```

```Smalltalk:System-browser
LiveStage>> listeners
	^listeners 
```

```Smalltalk:System-browser
LiveStage>> performer: anIdol
	anIdol onStage: self.
	performer := anIdol.
```

後で LiveStage を生成しやすくするために、クラスメソッドを定義しておきます。システムブラウザの中央くらいにある Class side を選択します。するとブラウザの表示がインスタンスメソッドからクラスメソッドへと切り変わります。この状態で次のメソッドを定義します。

```Smalltalk:System-browser
LiveStage>> startLive: anIdol listeners: otakuList
	| stage | "<= 一時変数"
	stage := self new.
	stage performer: anIdol.
	otakuList do: [ :otaku | stage joinListener: otaku ].
	^stage
```

定義が終わったら Class side から Inst. side へ戻しておきましょう。


## Izumi クラスと Otaku クラス

### Izumi クラスの定義

まずは Izumi クラスを定義しましょう。システムブラウザを立ち上げ IzumiSuki カテゴリを選択したら下のテキストボックスに次を入力して Accept します。

```Smalltalk:System-browser
IMChara subclass: #Izumi
	instanceVariableNames: 'figure stage'
	classVariableNames: ''
	package: 'IzumiSuki'
```

figure には大石泉の画像が、stage には LiveStage が入ります。続いて以下のメソッドを定義します。

```Smalltalk:System-browser
Izumi>> initialize
	super initialize.
	
	figure := (ImageReadWriter formFromFileNamed:'./izumi.png').
	bounds := (0 @ 0) corner: (figure width) @ (figure height). 
	
	self addMorph: figure asMorph.
	self message: 'idol++'.
```

figure では画像の読み込みをしています。Mac でのカレントディレクトリは "~/Documents/Pharo/images/イメージ名" になりますので、ここに izumi.png を入れれば OK です。今回は文字で "大石泉" と書いた画像を使っていますが、もちろん大石泉自身の画像を入れれば大石泉が表示されます。


```Smalltalk:System-browser
Izumi>> onStage: aStage
	stage := aStage.
```

```Smalltalk:System-browser
Izumi>> liveOnStage
	self turnOn.
	stage listeners do: [ :otaku | otaku genkai ].
```

```Smalltalk:System-browser
Izumi>> setSwitchState: aBoolean
	aBoolean
		ifTrue: [self liveOnStage]
		ifFalse: [self turnOff]. "Smalltalk の条件分岐はメッセージで行なわれる"
```

liveOnStage メソッドを turnOn メソッドの前に呼ぶようにします。liveOnStage メソッドは LiveStage を見ているオタクを genkai (限界)状態にするようにします。


### Otaku クラスの定義

最後にオタクを定義します。

```Smalltalk:System-browser
IMChara subclass: #Otaku
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'IzumiSuki'
```

```Smalltalk:System-browser
Otaku>> initialize
	|  head body |
	super initialize.

	head := PolygonMorph vertices: { 0 @ -10. 20 @ 10. 0 @ 30. -20 @ 10 }
					        color: Color black
					  borderWidth: 0
					  borderColor: Color transparent.
	head beSmoothCurve.
	self addMorph: head.
	body := PolygonMorph vertices: { 0 @ 0. -25 @ 50. 25 @ 50 }
					        color: Color black
					  borderWidth: 0 
					  borderColor: Color transparent.
	self addMorph: body.
	bounds := (-25 @ -10) corner: (25 @ 50).
	
	self message: '大石泉すき'.
```

オタクにも姿形があるので、initialize メソッドでコナンの犯人のような姿を描くようにしています。

```Smalltalk:System-browser
Otaku>> genkai
	self turnOn.
```

限界状態のオタクはスイッチが入った状態になります。


## 大石泉すき

### 準備

Playground (World -> Tools -> Playground) を開き、まず次の4行を入力、選択し Do it します。

```Smalltalk:Playground
izumi := Izumi new.
otakus := { Otaku new. }.
stage := LiveStage startLive: izumi listeners: otakus.
izumi openInHand.
```

大石泉がマウスカーソルに付いて来てくれますので、どこかで適当な場所でクリックして固定します。そして最後に次を Do it してオタクを暗黒召喚します。

```Smalltalk:Playground
(otakus at: 1) openInHand.
```

### 実行

大石泉をクリックすると、idol++ の吹き出しと一緒にオタクが限界を迎え、大石泉すきとつぶやきます。



## おわりに

長々とお読みいただきありがとうございました。もし Smalltalk に興味を持たれた方がいらっしゃれば、是非色々記事を探してみてください。Pharo は昨今のマイナープログラミング環境には珍しく、<a href="https://books.pharo.org">豊富なドキュメント</a>が備わっています。
今回は Morph でグラフィックをいじりましたが、最近は Spec というフレームワークが登場し移行が進んでいるようです。（詳しくは <a href="https://qiita.com/advent-calendar/2019/smalltalk">Smalltalk Advent Calendar 2019</a> の <a href="http://oohito.com/nqthm/archives/3145">razdan3 さんの記事</a>をお読み下さい。）

さて、そろそろモバマスで大石泉が登場するはずなので、みんな震えて眠りましょう... 大石泉すき...


## 参考文献

+ <a href="https://books.pharo.org/updated-pharo-by-example/">Pharo by Example 5</a>
+ <a href="https://qiita.com/tomooda/items/fa60135868df43e7ba78">PharoLauncherで日本語を使えるようにする3つの方法 - Qiita</a>
+ <a href="https://stackoverflow.com/questions/3172798/how-do-i-load-a-picture-image-into-smalltalk">How do I load a picture (image) into smalltalk - Stack Overflow</a>
+ <a href="https://qiita.com/newapplesho/items/323cf9d1be014861a57f">Smalltalk 基本クラス編 - Qiita</a>