これはSmalltalk Advent Calendar 2019の記事です。
Spec2は、Pharo Smalltalk で採用されているUIフレームワークであるSpec の新しいバージョンです。Pharo 7.0 では Spec が使われていましたが、Pharo 8 では(ほぼ?)全てのUIが Spec2 で書き直されるようです。この記事では Spec2 を使った UI の実装方法について簡単に紹介します。
Squeak/Pharo Smalltalk では、UIの実現のために長らく Morphic を用いてきました。Morphic はシンプルで強力なフレームワークですが、複数のコンポーネントを組み合わせていくとアナーキーな実装をしがちで、リファクタリングや再利用の際に泣きたくなるような状況に陥りがちです。Spec/Spec2 はUIの構築に秩序をもたらし、コンポーネントの再利用がしやすく設計されています。では、能書きはこれまでにして、具体的に Spec2 を用いた UIの実装方法を紹介しましょう。
Presenterの作成
Spec/Spec2 はVisual WorksやDolphin Smalltalk で用いられているMVP(Model View Presenter)パターンの影響を受けています。主要なUIコンポーネントを Presenterと呼んでおり、UIにおけるロジックに加えて、アプリケーションドメインとウィジェットとの連携を管理します。
Spec2ではウィジェットである Presenter を作成するのに、SpPresenter クラスを継承したクラスを作成します。Spec2に関連する全てのクラスは Sp という接頭辞が付いています。現状のPharo 8.0は Spec(1.0) と Spec2 が混在しているため、Spec2 でUIを実現するのであれば、Sp接頭辞の付いたクラスだけを用いる必要があります。
SpPresenter を継承したクラスでは、そのクラスが管理する子ウィジェットをインスタンス変数で管理します。例として、2つのボタンと1つのラベルを持つウィジェット SpecSample を作成してみましょう。
SpPresenter subclass: #SpecSample instanceVariableNames: 'buttonUp buttonDown labelCounter counter' classVariableNames: '' package: 'SpecSample'
ウィジェットとなるのは buttonUp と buttonDown、そしてlabelCounterです。上下ボタンを押すとカウンターが増減するため、内部状態として counter を設けています。
まずは counter を初期化するために initialize メソッドを作ります。
initialize counter := 0. super initialize
ウィジェットを作成する
ウィジェットは initialize で作成するのではなく、initializePresenters というメソッドで作成します。ボタンやラベルを作るためのヘルパーメソッドがあるのでそれらを利用します。また、ボタンラベルなども合わせて設定しておきます。
initializePresenters buttonUp := self newButton. buttonDown := self newButton. labelCounter := self newLabel. buttonUp label: 'Up'. buttonDown label: 'Down'. labelCounter label: counter asString. self focusOrder add: buttonUp; add: buttonDown
メソッドの最後にフォーカス順序を設定するためのコードを加えています。実際には機能しているように見えないのですが、設定するのが推奨されているようです。
レイアウトを設定する
レイアウトはクラス側で定義するため、クラス側のメソッドとして defaultSpec を追加します。
defaultSpec ^ SpBoxLayout newVertical add: #labelCounter; add: #buttonUp; add: #buttonDown; yourself
ここでは SpBoxLayout のインスタンスを作ってレイアウトしています。 newVertical はウィジェットを縦に並べるためのものです。インスタンス変数のシンボルを add: していくと、その順で上から下に配置されます。
試してみる
ここまでの作業でも実際に動かして試すことができます。Playground で SpecSample new openWithSpec を DoIt すればウィンドウが表示されます。
ボタンの動作を設定する
Upボタンでカウンターを増やし、Downで減らすようにしましょう。そのためにはボタンが押されたらラベルの内容を更新する必要があります。ウィジェット同士を関連づけるには、connectPresenters メソッドを定義します。
connectPresenters buttonUp action: [ counter := counter + 1. labelCounter label: counter asString ]. buttonDown action: [ counter := counter - 1. labelCounter label: counter asString ]
ボタンにaction:メソッドを送ってブロックを設定すると、ボタンが押された時にブロックの内容が実行されます。ここではカウンターの変数を増減し、その結果をラベルに設定しています。先ほどと同様にウィンドウを表示してボタンを押すと、カウンターが増減されることが確認できます。
Spec2は簡単
Spec2 の簡単な例について説明しました。まだ、装飾など何もしていないので殺風景ですが、ヘルパーメソッドも多く用意されていたり、細かくレイアウト調整ができるので Spec2 を使って UI を構築するのは簡単です。後の記事でも引き続き Spec2 について説明していくつもりです。