Scala 2.8 以降の scala.swing パッケージ

http://ingomaier.blogspot.com/2010/11/scalaswing-package-in-28-and-beyond.html
を邦訳メモしてあったので公開してみます。


scala.swing は Java Swing と、いくつかの AWT クラスのラッパーライブラリです。Java と Scala の相互運用のために、ラッパーライブラリは Java Swing コンポーネントを透過的に扱えるようになっています。つまりクライアントはラッパーを経由して、元となるピアの Java Swing コンポーネントを取得することができるということです。そのためラッパー側で追加の状態を管理するには必ずピアの状態との同期処理が必要となります。


Scala の swing API は、2.7 からいくつかの変更があります。バグ修正はもちろんですが、より多くのラッパーが追加され、包括的なコンポーネントキャッシングのメカニズムが導入されています。また window を基底とする型階層の修正も含まれています。ここでは、読者はすでに Java Swing と scala.swing にある程度の理解があるものとして、Scala 2.8 での重要な変更点について説明します。

Window 型階層

Java AWT/Swing の window 型階層は以下のようになっています。


AWT パッケージの Frame と Dialog は共通の Window クラスから派生しています。そして、Swing パッケージによる拡張では、それぞれが AWT には表れない共通の機能性を持っているにも関わらず、クラスの継承関係やインターフェーズが共有されていません。そこで我々は、scala.swing の window を基底とした型階層を次のように再設計しました。


各クラスは図中のピア型と関連しています。Swing の window クラスの共通機能はトレイトとして抽出してミックスインしている点に注意してください。このトレイトには以下のメソッドが含まれています。

def getJMenuBar: JMenuBar
def setJMenuBar(b: JMenuBar)
def setUndecorated(b: Boolean)
def setTitle(s: String)
def getTitle: String
def setResizable(b: Boolean)
def isResizable: Boolean

この設計により共通的なラッパーコードを RichWindow クラスに持たせられるようになりました。


ダイアログ

Scala 2.8 では Dialog コンパニオンオブジェクトが javax.swing.JOptionPane からメッセージユーティリティをラップします。名前付き引数を使用することでダイアログのコードはとても読みやすいものになります。Java Swing チュートリアルからの抜粋として、複数選択のオプションダイアログの例は以下のようになります。

val options = Array[Object]("Yes, please", "No, thanks", "No eggs, no ham!")         
JOptionPane.showOptionDialog(parent,
                "Would you like some green eggs to go with that ham?",
                "A Silly Question",
                JOptionPane.YES_NO_CANCEL_OPTION,
                JOptionPane.QUESTION_MESSAGE,
                null,
                options,
                options(2))


ダイアログのタイトルや質問メッセージの指定場所、null に設定したパラメータなど、引数の順番と意味を覚えておくのは少し厄介です。scala.swing では今では以下のように書くことができます。

Dialog.showOptions(parent,
       message = "Would you like some green eggs to go with that ham?",
       title = "A Silly Question",
       messageType = Dialog.Message.Question,
       optionType = Dialog.Options.YesNoCancel,
       entries = Seq("Yes, please", "No, thanks", "No eggs, no ham!"),
       initial = 2)


タイトルやメッセージの設定内容が明確になっています。アイコン用のパラメータはデフォルト値のnullが適用されるため、null 引数を明示的に指定する必要もありません。名前付き引数により、可読性が上がっただけではなく、引数の順番も任意に指定できるようになっていることにも注意してください。

イベント

Scala の swingコンポーネントは今ではキーイベントを発行するようになりました。イベントクラスは今まで通り Java のクラスと単純に対応しています。これらのイベントは scala.swing.event パッケージにあります。
Scala 2.7 で欠落していた重要な機能の1つに、入力イベントの消費がありました。これについても修正されています。
さらに、チケット#1442 にて、いくつかの Swing コンポーネントは特定のリスナーがインストールされているかどうかによって振る舞いが異なることが指摘されていました。そのため scala.swing では Swing/AWT リスナが遅延評価されるようになっています。これは、scala.swing が対応するパブリッシャーに応答する場合に初めてJava のリスナーをインストールし、応答が削除された時にリスナがアンインストールされることを意味します。その結果、Swing が最初から依存している誤った副作用の誘発を最小限にすることができます。(これは、ピアとの厳密な同期処理なしに追加の状態変数をラッパーで管理する方法の1つとなっています)

ラッパーキャッシュ

以下に示す変更内容は、利用者側が気付くような変更内容ではありませんが、興味のある読者のために少し詳細を紹介します。


先に述べたように、全てのラッパーからピアを取得することができます。既に scala.swing が作成されており、ガベージコレクタにより回収されていない限り、全てピアはラッパーに関連付けられた状態となります。この背後にある関連はラッパーキャッシュにて構築されています。なぜラッパーをキャッシュするのでしょうか。ここで注意したいのが、同じJava Swing コンポーネントのためのラッパーが重複しても同一性に対して問題が発生しない点です。scala.swing.UIElements は scala.Proxy のインスタンスであり、equals と hashCode メソッドはピアに委譲されるためです。キャッシュする主な理由は、コンポーネントの生成に関する全ての処理をコントロールする必要が無い点となります。例としてスクロールパネルを考えてみます。スクロールパネルは内部にスクロールバーのセットを生成し、クライアントが操作のハンドルを取得できます。これはユーザコードにより生成されたラッパーとは対照的に、これらのコンポーネントを通じて生成がコントロールされるのです。クライアントがハンドルを取得する度に新しいラッパーを生成する必要は無いため、我々は弱参照のキャッシュを使用しています(将来的にはソフト参照の利用を考えています)。サブタイプ化により、各Java Swing コンポーネントのための異なる型のラッパーが存在できます。現在は、あるシチュエーションでは使いにくいJava Swing コンポーネントの静的な型に対応したラッパーがあります。将来的にはより動的なものに変更していくつもりなので、キャッシングAPIは変更される可能性がありあます。

スカラ2.9の先に

このポストの大きなニュースとして、我々は scala.swing の開発プロセスを継続しているという点です。我々は現在 git を使用しており、メインリポジトリhttp://github.com/ingoem/scala-swing で見つけることができます。マスターブランチはSVN トランクの双方向のミラーです。incubブランチは開発途上のブランチですので あなたの貢献を歓迎します。incubブランチの特定機能が安定した段階で、master ブランチに反映され、次のScalaリリースの一部となるべくSVN トランクへ含まれるようになります。もしコントリビュータになりたい場合は、ahead からフォークするだけです。既に2つのラッパーに対して素晴らしい仕事をしてくれた Andreas Flierl と Ken Scambler に感謝します。