導入
今回は Pharo を使ってアナログ時計を作ってみます。
Pharo
Pharo は以下からダウンロードできます。
https://pharo.org/
Pharo の紹介
Pharo は Smalltalk 処理系の一種です。Smalltalk は「すべてはオブジェクトである」というパラダイムに基づくプログラミング言語です。Smalltalk においてはプログラムを書く人間も一個のオブジェクトと捉え、すべてのオブジェクトは「メッセージ」をやりとりすることによって対話し相互に作用する、という考え方に基づきます。
ここで注意して欲しいのは、オブジェクト指向言語にも「方向性」があり、それぞれ違っているという事実があります。ですので C 系のオブジェクト指向言語(C++, C#, Python など)の目指すオブジェクト指向と、Smalltalk の目指すオブジェクト指向は異なります。この事実を知っていないと、いろいろ不便な目に遭うというか、
「えらい遠回りしちまったなーこりゃ...。」
となりかねないので注意してください。目指している「方向」が違います。
すべてはオブジェクトである
よく C 系のオブジェクト指向言語では、
・こういう継承はすべきじゃない
・これは良い継承で、これは悪い継承のしかただ。
・それは「なんとかパターン」で作るべき
とかよく言いますが、Smalltalk においては
すべてはオブジェクトである!
なのでとにかく継承しないとプログラミングが始まりません。「良い継承」とか「悪い継承」とかありません。最低でも Object クラスを継承しなければなりません。Object クラスを継承しない「何か」などが存在することは絶対に許されません。なぜなら「全てはオブジェクト」でなければならないからです。
オブジェクト指向原理主義。怖いでしょう?w
パターンについては「ベストプラクティスパターン」と呼ばれるものならありますが、
あっ、これ◯◯パターンだ。いつの間にか使ってたわw
ってかんじです。あんまり意識しないというか気にしてません。たぶん。
とにかく継承する
最初にすべきは、時計アプリ (ClockMorph) を作ります。Morph というのは Pharo における図形オブジェクトみたいなものです。Pharo の Browser (クラスブラウザ)や Playground のような GUI 部品はすべて Morph クラスのサブクラスです。Pharo のルート画面(デスクトップ)さえも Morph のサブクラスです。
EllipseMorph subclass: #ClockMorph
instanceVariableNames: 'time'
classVariableNames: ''
package: 'PBE-Clock'
今回作る ClockMorph は EllipseMorph のサブクラスにします。
インスタンス変数は time です。現在の時刻を持ちます。
package は PBE-Clock となっていますが、ここは適当で良いです。例えば「My-Application」でも「Practice」でも何でもいいです。好きな名前にしてください。
メソッドを実装していきます。
Pharo(Smalltalk)ではメソッド群を分類する「プロトコル」というものがあります。最初は「accessing」プロトコルからにしましょう。「accessing」プロトコルには主にインスタンス変数を参照するメソッドを書きます。自分自身の幅や高さを返すメソッド群も accessing で良いでしょう。
accessing プロトコルメソッド
インスタンス変数や、自分オブジェクトの幅や高さ、針(長針、短針、秒針)の開始点・終了点の値などにアクセスするメソッドを書きます。
centerOfClock
^ self halfWidth @ self halfHeight
halfHeight
^ self height / 2
halfWidth
^ self width / 2
headOfHourHand
^ self halfWidth @ (self halfHeight * (5/10))
headOfLongScale
^ self halfWidth @ 0
headOfMinuteHand
^ self halfWidth @ (self halfHeight * (2/10))
headOfSecondHand
^ self halfWidth @ (self halfHeight * (2/10))
headOfShortScale
^ self halfWidth @ 0
tailOfHourHand
^ self centerOfClock
tailOfLongScale
^ self halfWidth @ (self halfHeight * (3/20))
tailOfMinuteHand
^ self centerOfClock
tailOfSecondHand
^ self halfWidth @ (self halfHeight * (12/10))
tailOfShortScale
^ self halfWidth @ (self halfHeight * (1/10))
constants プロトコルメソッド
定数を返すメソッドを書きます。
colorOfHourHand
^ Color black
colorOfMinuteHand
^ Color black
colorOfSecondHand
^ Color black
radiusOfcenterPoint
^ 3
widthOfHourHand
^ 3
widthOfMinuteHand
^ 2
widthOfSecondHand
^ 1
converting プロトコルメソッド
変換する処理を行うメソッドを書きます。
offset: coord
^ coord translateBy: self bounds origin
point: aPoint rotateBy: radian
| x y |
x := radian cos * aPoint x - (radian sin * aPoint y).
y := radian cos * aPoint y + (radian sin * aPoint x).
^ x @ y
point: aPoint rotateBy: radian centerAt: center
| newPoint |
newPoint := self point: (aPoint translateBy: center negated) rotateBy: radian.
^ newPoint translateBy: center
drawing プロトコルメソッド
自分自身の領域内を描画するためのメソッドを書きます。
drawCenter: aCanvas
| pt1 pt2 |
pt1 := self offset: self centerOfClock - self radiusOfcenterPoint.
pt2 := self offset: self centerOfClock + self radiusOfcenterPoint.
aCanvas fillOval: (pt1 corner: pt2) color: Color black
drawClockFace: aCanvas
aCanvas fillOval: self bounds color: Color paleGreen darker.
self drawScales: aCanvas.
self drawCenter: aCanvas.
drawHourHand: aCanvas
self
drawLineOn: aCanvas
from: self headOfHourHand
to: self tailOfHourHand
width: self widthOfHourHand
color: self colorOfHourHand
rotate: self rotationOfHourHand
drawLineOn: aCanvas from: pt1 to: pt2 width: w color: c rotate: r
self
drawLineOn: aCanvas
from: pt1
to: pt2
width: w
color: c
rotate: r
centerAt: self centerOfClock
drawLineOn: aCanvas from: pt1 to: pt2 width: w color: c rotate: r centerAt: center
self
canvas: aCanvas
line: (self point: pt1 rotateBy: r centerAt: center)
to: (self point: pt2 rotateBy: r centerAt: center)
width: w
color: c
drawMinuteHand: aCanvas
self
drawLineOn: aCanvas
from: self headOfMinuteHand
to: self tailOfMinuteHand
width: self widthOfMinuteHand
color: self colorOfMinuteHand
rotate: self rotationOfMinuteHand
drawOn: aCanvas
self
drawClockFace: aCanvas;
drawHourHand: aCanvas;
drawMinuteHand: aCanvas;
drawSecondHand: aCanvas
drawScales: aCanvas
| aBlock |
aBlock := [ :radian :isLongScale |
| head tail w |
isLongScale
ifTrue: [ head := self headOfLongScale.
tail := self tailOfLongScale.
w := 2 ]
ifFalse: [ head := self headOfShortScale.
tail := self tailOfShortScale.
w := 1 ].
self
drawLineOn: aCanvas
from: head
to: tail
width: w
color: Color black
rotate: radian].
6 to: 360 by: 6 do: [ :each | aBlock value: each / 360 * 2 * Float pi value: each % 30 == 0 ]
drawSecondHand: aCanvas
self
drawLineOn: aCanvas
from: self headOfSecondHand
to: self tailOfSecondHand
width: self widthOfSecondHand
color: self colorOfSecondHand
rotate: self rotationOfSecondHand
initializing プロトコルメソッド
初期化を行うメソッドを書きます。initialize メソッドはスーパークラスにあるメソッドをオーバーライドしているので、最初に「super initialize.」と書き、スーパークラスの initialize (初期化処理)を実行するのを忘れないように注意してください。
initialize
super initialize.
self extent: 150 @ 150.
time := Time now.
self startStepping
private プロトコルメソッド
インスタンス変数への代入など、他のクラスから使ってもらいたくないプライベートなメソッドを書きます。
canvas: aCanvas line: pt1 to: pt2 width: w color: c
aCanvas line: (self offset: pt1) to: (self offset: pt2) width: w color: c
time: newTime
time := newTime
rotation プロトコルメソッド
座標を回転するメソッドを書きます。
短針だけは長針の進み具合によって少しずつ回転するのをシミュレートします。例えば時刻が 10:30 のとき、短針はずっと 10 を指すのは不自然なので、10 と 11 の間の中央を指すようにします。
rotationOfHourHand
^ (time hour12 / 6 + (time minute / 360)) * Float pi
rotationOfMinuteHand
^ (time minute / 30) * Float pi
rotationOfSecondHand
^ (time second / 30) * Float pi
stepping プロトコルメソッド
時計の時刻を定期的に更新するメソッドと、更新間隔(ミリ秒)を書きます。
step
time := Time now.
self changed
stepTime
^ 500
Pharo において自身を更新したい場合は、自分で何かを描画するメソッドを実行するのではなく、システムに対して「自分を更新したので再描画して欲しい」という事を伝えます。システムはすべてのアプリを管理しており、再描画依頼をもらった各アプリに対して順番に再描画を行います。この取り決めを守らない場合、アプリは必死に自分を描画しようとするため、激重になってシステム全体に悪影響を及ぼす事になります。
自身を再描画して欲しい時は、changed メッセージを送ります。
アプリ(送り側) ----- changed -----> システム(受け側)
すると、しばらく経った後、システム側から再描画していいよ!というメッセージが送られてきます。
アプリ(受け側) <---- drawOn: ----- システム(送り側)
アプリは drawOn: に自身を描画する方法を書いておきます。このようにして他のアプリと協調しながら自身を再描画します。
overrides プロトコルメソッド
ここは自動的に作成されます。オーバーライドしているメソッドは以下の4つです。
drawOn:
initialize
step
stepTime
実行する
Playground を開き、以下を入力し、マウスを左クリックしながらなぞって全選択します。
ClockMorph new openInWorld
右クリックして表示されるプルダウンメニューから「do-it」を選びます。
このようなアナログ時計が Pharo のデスクトップの左上に現れます。