0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

時計アプリを作る

Last updated at Posted at 2025-05-21

導入

今回は 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」を選びます。
tokei.png
このようなアナログ時計が Pharo のデスクトップの左上に現れます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?