Swing Application Frameworkとは、複雑なGUIツールキットであるSwingライブラリの上で簡単にGUIアプリケーション・プログラムを作成するために提供されるフレームワークです。Swing Application Frameworkは、2008年リリース予定の次期Java Standard Edition(Java SE 7)に標準搭載することを目指してJava標準化プロセス(JCP:Java Community Process)においてJSR-296として仕様が策定され、そのリファレンス実装が現在開発されている途上です。
1998年にリリースされたJava 2(JDK 1.2)に標準搭載されているGUIツールキットSwingは、柔軟性が高く高機能な部品の集合ですが、高機能な分だけ巨大で複雑なライブラリとなっています。そのため、Swingの部品を組合せてGUIアプリケーションを作るのは複雑であり、習得が難しいという問題が生じています。
そこで、Swingに習熟していない開発者でも簡単にSwingが提供する機能を使ったGUIアプリケーションを作成できるお手軽GUIフレームワークとしてSwing Application Frameworkが登場しました。
このSwing Application Frameworkは、既にSwing APIを使ってGUIアプリケーションをすらすらと作れるベテランの開発者にとってはあまり有り難みはないかもしれません。ですが、Swingの初心者にとっては、複雑なSwing APIの使用上の注意や使い方をフレームワークがカバーしてくれるありがたい存在になることでしょう。
例えば、以下のようなまずいプログラミングをしている初心者は、このSwing Application Frameworkを使うことで正しいSwingのプログラミングが結果として得られるという恩恵を享受できます。
従来はアプリケーション開発者が注意してプログラミングする必要のあったアプリケーションのライフサイクル管理、リソース、プリファレンス、アクションの管理などの雛形をフレームワーク側で提供します。
フレームワークとしては、基底クラスの提供(Appletクラスのようなもの)およびアノテーションによるリソース(プロパティファイルに定義したデータをインスタンスのフィールドに対応付け:「インジェクション」)、アクションマップ(複数のイベントハンドラー・メソッドを定義したクラスを作成し、個々のGUIパーツのイベントと結びつける)が中心となります。イベントハンドラーとして、別スレッドで処理を実行する非同期アクションも用意されます。
ここ最近のJavaのバージョンアップで取り組まれているEase of Development(EoD)の流れの中で少々取り残された感のあるデスクトップ・アプリケーションですが、近々Swingアプリケーションを容易に開発できるようになるフレームワークとして登場するという位置づけとなります。
初心者向けのイメージが強いフレームワークですが、ベテランの開発者でも、今までは自前でごりごりコーディングしていた部分を標準フレームワークに任せることができるので、それなりにメリットはあるでしょう。
Swingが登場してから10年になりますが、その間部品やレイアウトマネージャ、Look and Feelの追加はあったものの基本的構造には変化はありませんでした。それが、このSwing Application Frameworkの登場によってようやく変化が生じることになります。
2006年10月25日付けの下記URLで公開されているHans Muller氏へのインタビュー記事より、Muller氏が語るSwing Application Frameworkの目的、範囲などを簡単にまとめてみました。
2007年10月現在、JSR-296 Swing Application Frameworkは、java.netコミュニティのプロジェクトの1つとして開発が進められています。appframeworkプロジェクトのページから、ソースを入手可能です。サイズも小さく簡単にビルドできます。また、バイナリも用意されています。
appframeworkプロジェクトのページにある"Downloads,Docs,and Feedback"見出しの下のSource code: AppFramework-1.01-src.zip(1.01の部分はバージョン番号なので変化します)のリンクをクリックしダウンロードします。
appframeworkプロジェクトのページの左側メニューにある"Subversion"のリンクを辿ると、Subversionリポジトリの閲覧ページになります。「ソースコードリポジトリへのアクセス」の説明にしたがってSubversionコマンドなりTortoiseSVNで(Windowsな人はこれが便利)チェックアウトします。
appframeworkプロジェクトのページの左側メニューにある"ドキュメント&ファイル"のリンクを辿り、"releases(0)"をクリックし、"1.01(3)"をクリックします。ダウンロードの画面を以下に示します。
一番簡単なのは、NetBeansでビルドしてしまう方法です。ソースアーカイブを展開すると、ディレクトリの中にnbprojectディレクトリがあるので、これをNetBeansで開きます(5)。
また、コマンドライン環境でJDKでビルドすることも簡単です。本文書では、コマンドライン環境でのビルドを紹介します。
Swing Application Frameworkは、SwingWorkerクラスを使用しています。SwingWorkerクラスは、Java SE 6からSwing APIの部品として追加されたため、J2SE 5.0(Tiger)のSwing APIには含まれません。そのため、現在のSwing Application Framework Ver.1.01はJ2SE 5.0でも利用できるように、Java.netのjdesktopプロジェクトで作成され公開されているSwingWorkerを使用しています。このjdesktopプロジェクトがリリースするSwingWorkerクラスは、JavaSE 6の標準搭載になったjavax.swing.SwingWorkerとはパッケージ名が異なります。
そこで、ビルドするには以下の2つの選択肢があります。
本文書では、1.のJDK 6単体でビルドする手順を紹介します。
Java SE 6単独でビルドできるよう以下2つのファイルでimportしているSwingWorkerのパッケージ名を修正します。
import org.jdesktop.swingworker.SwingWorker;
から以下へ修正します。
import javax.swing.SwingWorker;
同様に、
import org.jdesktop.swingworker.SwingWorker.StateValue;
から以下へ修正します。
import javax.swing.SwingWorker.StateValue;
D:\work\AppFramework-1.01> mkdir classes D:\work\AppFramework-1.01> javac -d classes -cp \ D:\java\jdk1.6.0\jre\lib\javaws.jar \ src/org/jdesktop/application/*.java 注:入力ファイルの操作のうち、未チェックまたは安全ではないものがあります。 注:詳細については、-Xlint:unchecked オプションを指定して再コンパイルしてください。 D:\work\AppFramework-1.01>
Swing Applicationフレームワークは、jnlp APIを使用するので、JDKに含まれるjnlp APIが入っているjavaws.jarをビルド時のクラスパスに含めています。
classesディレクトリの中にコンパイルされたファイルが生成されます。
この手順で生成されたクラスファイルを1つのJARファイルにまとめます。
D:\work\AppFramework-1.01> jar cvf AppFramework-1.01.jar -C \ classes . : D:\work\AppFramework-1.01>
この手順で作成したJDK 6単独用フレームワークのJARファイルは、ダウンロード・コーナーに置いております。
本節では、単純なラベル1つだけのウィンドウを表示するGUIプログラムを作成します。目標として以下に示すウィンドウを表示させます。
ソースコードを以下に示します。public static voidなmainメソッドを持つ1つの小さなクラスです。
/* * Torutk Learning Project for Swing Application Framework. * * $Id: HelloApplication.java 99 2007-10-08 07:25:22Z toru $ */ import org.jdesktop.application.SingleFrameApplication; import javax.swing.JLabel; /** * SingleFrameApplicationを継承する簡単なアプリケーション。 * JLabelを1つ表示する。 * */ public class HelloApplication extends SingleFrameApplication { @Override protected void startup() { JLabel label = new JLabel("Hello World"); show(label); } public static final void main(final String[] args) { launch(HelloApplication.class, args); } }
以下に作成手順を示します。
作成するアプリケーションの継承元クラスを選択する
フレーム(OSの画面上に表示するウィンドウのこと)が1つしかないアプリケーションを作成するときは、SingleFrameApplicationクラスを継承します。
startupメソッドをオーバーライドする
startupメソッドをオーバーライドして、そこにアプリケーションの画面初期化コードを記述します。この単純なHelloApplicationプログラムでは、フレームにJLabelを1つだけ貼った画面構成となります。通常のSwingアプリケーション同様にJLabelを生成します。なお、今回はラベルの文字列はコード中に固定で記述しています。
(補足)Swing Application Frameworkに含まれるサンプルコードによっては、このstartupメソッドがpublicであったりprotectedであったりします。継承元(SingleFrameworkApplicationクラスのさらに継承元)のApplicationクラスを見ると、startupメソッドはprotectedで定義されているので、ここはprotectedとすべきでしょう。
showメソッドに表示させたいSwingコンポーネントを渡して呼び出す
SingleFrameApplicationクラスを継承した場合、トップレベル・ウィンドウのJFrameは継承元のSingleFrameApplicationクラスで管理されるので、アプリケーション作成者が直接JFrameを触る必要はありません。JFrameに対する設定、例えば画面レイアウトの実行(packまたはsetSize)、画面の具現化(setVisible)といった今までSwingアプリケーションを作成するときにコーディングしていたお決まりの記述は、継承元のSingleFrameApplicationクラスのshowメソッドを呼び出した中にあるため、記述量が大分少なくなりました。
mainメソッドからは、Applicationクラスのlaunchメソッドを呼び出す
mainメソッドからは、アプリケーション派生クラス(今回は、HelloApplication)を引数に渡してlaunchメソッドを呼び出します。コマンドライン引数をアプリケーション派生クラスに渡すために、第二引数に指定します。このコマンドライン引数は必要ならアプリケーション派生クラスでinitializeメソッドをオーバーライドして受け取ることができます。
先程作成したSwing Application Frameworkのライブラリファイルをjavacのコマンドラインでクラスパス・オプションに設定しておきます。
D:\work\appframework> javac -cp D:\work\AppFramework-1.01.jar;. \ HelloApplication.java D:\work\appframework>
Swing Application Frameworkをjavaコマンドのコマンドラインでクラスパス・オプションに指定して実行します。
D:\work\appframework> java -cp D:\work\AppFramework-1.01.jar;. \ HelloApplication
さて、実行結果は以下のようになりました。最初の目標画面と少し違います。JLabelに設定した文字列("Hello World")は表示されていますが、ウィンドウのタイトル文字列が空になっています。
この問題には2つの解決方法があります。方法1は少し邪道で方法2がSwing Application Frameworkの思想に沿った解決です。
getMainFrame().setTitle("Hello JSR-296");
Application.title = Hello JSR-296
Swing Application Frameworkは、リソースファイルを積極活用してコーディング量を減らすアプローチを取っています。詳細は後述しますが、まずはアプリケーション・クラスのパッケージ名に".resources"を追加したパッケージの中にある、アプリケーション・クラス名と同じ名前に基づくプロパティファイルにいろいろ設定を記述すると理解しておくことにします。
具体的には、resourcesディレクトリをクラスファイルがある場所に作成し、resourceディレクトリの中に、上記2.で示したリソースファイル"HelloApplication.properties"を記述します。
D:\work\appframework> dir /B HelloApplication.class HelloApplication.java resources D:\work\appframework> dir /B resource HelloApplication.properties D:\work\appframework> java -cp D:\work\AppFramework-1.01.jar;. \ HelloApplication 2007/06/22 23:58:37 application.LocalStorage getId 警告: unspecified resource Application.id using HelloApplication 2007/06/22 23:58:37 application.LocalStorage getId 警告: unspecified resource Application.vendorId using \ UnknownApplicationVendor
無事ウィンドウのタイトル文字列が表示されました。
上記でHelloApplicationを実行したときに、警告のログメッセージが出力されています。これは、リソースファイルに記述されていない項目があった場合にデフォルト値を適用したという内容です。リソースファイルに記述できるApplicationクラスの属性はいくつかありますが、そのうち2つ(Application.idおよびApplication.vendorId)だけ警告のログメッセージに出力されています。これは、この2つの属性がセッション情報を格納するディレクトリ名に使用されるからと思われます。
そこで、以下のように警告のログメッセージが出る2つの属性をリソースファイルに明示的に記述します。
Application.title = Hello JSR-296 Application.id = HelloApplication Application.vendorId = torutk
上述2.で定義したリソースファイルを国際化(ロケール)対応することにします。リソースファイルをロケール別に作成しておき、実行時のロケールに対応したリソースファイルの内容を反映させるというものです。
Swing Application Frameworkで使用するリソースファイルは、Javaの標準機能であるプロパティ・ファイルによるリソースバンドルを使用しています。リソースバンドルは国際化対応されているので、リソースバンドルのルールに従ってロケール別のリソース定義ファイル(プロパティファイル)を記述するだけです。
以下、日本語ロケール用のリソースファイルを記述し、日本語ロケールで実行します。
Application.title = こんにちは、JSR-296
ただし、Javaの規約でリソースバンドル・プロパティは非ASCII文字(日本語文字等)をUnicodeエスケープしたASCII文字で記述することが要求されます。
そこで、上記ファイルを一旦Shift_JISコードで保存し、次のJDK付属コマンドであるnative2asciiコマンドを使ってUnicodeエスケープに変換します。
D:\work\appframework\resources> dir /B HelloApplication.properties HelloApplication_ja.properties.sjis D:\work\appframework\resources> native2ascii \ HelloApplication_ja.properties.sjis \ HelloApplication_ja.properties D:\work\appframework\resources> dir /B HelloApplication.properties HelloApplication_ja.properties HelloApplication_ja.properties.sjis D:\work\appframework\resources>
native2asciiコマンドによってUnicodeエスケープした日本語ロケール用プロパティファイルの中身は以下となります。
Application.title = \u3053\u3093\u306b\u3061\u306f\u3001JSR-296
これを日本語ロケール環境で実行すると以下のように日本語でタイトルが表示されます。
日本語環境のOS上で実行すると、デフォルトで日本語ロケールになりますが、他のロケールで実行させたい場合、javaのコマンドライン・オプション(例:-Duser.language=en)で指定することができます。
ここで、試しに独語ロケールのプロパティファイルを用意して実行してみることにしましょう。
Application.title = Guten Tag JSR-296
上記のプロパティファイルを作成後、javaのコマンドラインで使用言語を独語に指定し実行します。
D:\work\appframework> javac -Duser.language=de -cp \ D:\work\AppFramework-1.01.jar;. HelloApplication
以下のように独語でタイトルが表示されます。
HelloApplicationを実行した際に、画面を移動し、画面サイズも変更して終了させてみて下さい。次にもう一度HelloApplicationを実行すると、先に画面を移動させた場所に変更した大きさで表示されることと思います。
これは、HelloApplicationのスーパークラスであるSingleFrameApplicationクラスの中で、終了時に画面情報をセッション情報としてファイルに保存し、次回起動時にファイルからセッション情報を読み取り画面表示を行っているからです。
Swing Application Frameworkの実装では、セッション情報の格納に、javax.jnlp.PersistenceServiceを使用しています。保存されるファイルの場所はOSによって異なります。Windows Vistaの場合だと、C:\Users\torutk\AppData\Roamingの中になります。
そして、アプリケーション・リソースで指定したApplication.idとApplication.vendorIdが、ファイルを格納するディレクトリ階層に使用されます。Application.idとApplication.vendorIdは指定しない場合デフォルト値が使われます。
ファイル名は、ウィンドウ名+".session.xml"の名前で作成されます。ウィンドウ名はSingleFrameApplicationを使用した場合は"mainFrame"となるので、"mainFrame.session.xml"となります。
C:\Users\torutk\AppData\Roaming\ | ||||
torutk\ | [Application.vendorID]で指定した名前 | |||
HelloApplication\ | [Application.id]で指定した名前 | |||
mainFrame.session.xml |
Javaには標準でリソースバンドル・プロパティ・ファイルという仕組みがあり、テキストデータをリソースファイルに記述して実行時にプログラムから読み込むことができます。しかし、テキストデータをプログラムで使用する各型へ変換し、所定のオブジェクトへ設定するのはアプリケーションで記述する必要があります。
JSR-296では、プログラムで扱う様々なデータをリソースファイルから取り込み所定のオブジェクトへ設定する処理を随分と面倒見てくれるようになっています。
最初の一歩の節では、ウィンドウのタイトル文字をリソースファイルで定義しました。この節では、ウィンドウに貼っているラベル(javax.swing.JLabel)の以下のプロパティ(属性)をリソースファイルで定義する方法を見ていきます。
ソースコードを作成します。前の節で作成したHelloApplication.javaとほとんど同じですが、JLabelのインスタンス生成箇所に違いがあります。ここでは、引数無しのコンストラクタでJLabelインスタンスを生成し、setNameメソッドで名前"myLabel"を指定しています。この名前が後程リソースファイルで使用されることになります。
/* * Torutk Learning Project for Swing Application Framework. * * $Id: LabelApplication.java 100 2007-10-08 07:59:47Z toru $ */ import org.jdesktop.application.SingleFrameApplication; import javax.swing.JLabel; /** * SingleFrameApplicationを継承するリソースファイルを使用するアプリケーション。 * リソースファイルと対応付けるためコンポーネントに名前を指定している。 */ public class LabelApplication extends SingleFrameApplication { @Override protected void startup() { JLabel label = new JLabel(); label.setName("myLabel"); show(label); } public static final void main(final String[] args) { launch(LabelApplication.class, args); } }
ロケールなしのリソースファイル(ルート・リソースファイル)を記述します。上記ソースコードで指定したJLabelインスタンスの名前("myLabel")とJavaBeans仕様のプロパティ名を指定して値を記述しています。
Application.title = Label Resource Application Application.id = LabelApplication Application.vendorId = torutk myLabel.opaque = true myLabel.background = 250, 240, 230 myLabel.foreground = #101010 myLabel.text = Hello Resource myLabel.font = Dialog-PLAIN-32 myLabel.icon = penduke-transparent.gif
プロパティ名 | JLabelクラスの設定メソッド | プロパティの型 | 内容 |
---|---|---|---|
opaque | setOpaque | boolean | trueの場合不透過 |
background | setBackground | Color | 背景色 |
foreground | setForeground | Color | 前景色 |
text | setText | String | 表示するテキスト |
font | setFont | Font | 表示するテキストのフォント |
icon | setIcon | Icon | 表示するアイコン |
今回使用した画像ファイル(penduke-transparent.gif)は、Sunがフリーで公開しているDukeのグラフィックスデータの1つで、以下Webサイトから入手できます。
D:\work\appframework> dir /B LabelApplication.class LabelApplication.java resources D:\work\appframework> dir /B resource LabelApplication.properties penduke-transparent.gif D:\work\appframework> java -cp D:\work\AppFramework-1.01.jar;. \ LabelApplication
ルート・リソースファイルで定義しているリソースの中で、ウィンドウタイトルとラベルのテキストを日本語で記述するように置き代えます。その他のリソースは変更する必要がなければ日本語ロケールのリソースファイルに記述する必要はありません。ルート・リソースファイルの内容が適用されます。
# LabelApplication.propertiesから変更する部分のみ記述すればよい Application.title = ラベル・リソース・アプリケーション myLabel.text = リソース定義
日本語のファイルはnative2asciiコマンドで変換(Unicodeエスケープ文字に変換)します。
D:\work\appframework\resources> dir /B LabelApplication.properties LabelApplication_ja.properties.sjis D:\work\appframework\resources> native2ascii \ LabelApplication_ja.properties.sjis \ LabelApplication_ja.properties D:\work\appframework\resources> dir /B LabelApplication.properties LabelApplication_ja.properties LabelApplication_ja.properties.sjis D:\work\appframework\resources>
# \ LabelApplication.properties\u304b\u3089\u5909\u66f4\u3059\u308b\u90e8\u5206\u306e\u307f\u8a18\u8ff0\u3059\u308c\u3070\u3088\u3044 \ Application.title = \ \u30e9\u30d9\u30eb\u30fb\u30ea\u30bd\u30fc\u30b9\u30fb\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 \ myLabel.text = \u30ea\u30bd\u30fc\u30b9\u5b9a\u7fa9
日本語ロケールのリソースファイル(LabelApplication_ja.properties)を記述して実行します。
D:\work\appframework> javac -cp D:\work\AppFramework-1.01.jar;. \ LabelApplication
以下の画面が表示されます。
GUIコンポーネントのプロパティ以外にも、プログラム外部にデータを記述しておき実行時に取り込むことができます。
この節では、オブジェクトのフィールドに設定したい値をリソースファイルに定義する方法を見ていきます。
リソースファイルから値を設定するフィールドを@Resourceアノテーション付きで定義します。以下のサンプルコードは、Ver.1.0現在サポートされている型を全て挙げています。
import org.jdesktop.application.Resource; import org.jdesktop.application.SingleFrameApplication; public class FieldInjectApplication extends SingleFrameApplication { :(中略) @Resource private boolean booleanValue; @Resource private byte byteValue; @Resource private short shortValue; @Resource private int intValue; @Resource private long longValue; @Resource private float floatValue; @Resource private double doubleValue; @Resource private String stringValue; @Resource private MessageFormat messageFormatValue; @Resource private URL urlValue; @Resource private URI uriValue; }
@Resourceアノテーション付きのフィールドに、リソースファイルから値を読み込み値を設定する処理(インジェクション)を記述します。プログラム起動時に値を読み込ませたいので、アプリケーション派生クラスのstartupメソッドに記述することにします。
@Override protected void startup() { getContext().getResourceMap().injectFields(this); JComponent component = createMainPanel(); show(component); }
リソースファイルに記述した値をフィールドに設定するには、ResourceMapクラスのinjectFieldsメソッドを使用します。injectFieldsメソッドの引数には、値を設定する対象オブジェクトを指定します。
以下にソースコード全体を示します。
import java.net.URI; import java.net.URL; import java.text.MessageFormat; import javax.swing.GroupLayout; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import org.jdesktop.application.Resource; import org.jdesktop.application.SingleFrameApplication; /* * Torutk Learning Project for Swing Application Framework. * * $Id: FieldInjectApplication.java 105 2007-10-08 21:53:30Z toru $ */ public class FieldInjectApplication extends SingleFrameApplication { @Override protected void startup() { getContext().getResourceMap().injectFields(this); JComponent component = createMainPanel(); show(component); } private JComponent createMainPanel() { JPanel panel = new JPanel(); GroupLayout layout = new GroupLayout(panel); panel.setLayout(layout); layout.setAutoCreateGaps(true); layout.setAutoCreateContainerGaps(true); JLabel booleanLabel = new JLabel(); booleanLabel.setName("booleanLabel"); JTextField booleanField = new JTextField(String.valueOf(booleanValue)); JLabel byteLabel = new JLabel(); byteLabel.setName("byteLabel"); JTextField byteField = new JTextField(String.valueOf(byteValue)); JLabel charLabel = new JLabel(); charLabel.setName("charLabel"); //JTextField charField = new JTextField(String.valueOf(charValue)); JTextField charField = new JTextField("Don't support"); JLabel shortLabel = new JLabel(); shortLabel.setName("shortLabel"); JTextField shortField = new JTextField(String.valueOf(shortValue)); JLabel intLabel = new JLabel(); intLabel.setName("intLabel"); JTextField intField = new JTextField(String.valueOf(intValue)); JLabel longLabel = new JLabel(); longLabel.setName("longLabel"); JTextField longField = new JTextField(String.valueOf(longValue)); JLabel floatLabel = new JLabel(); floatLabel.setName("floatLabel"); JTextField floatField = new JTextField(String.valueOf(floatValue)); JLabel doubleLabel = new JLabel(); doubleLabel.setName("doubleLabel"); JTextField doubleField = new JTextField(String.valueOf(doubleValue)); JLabel stringLabel = new JLabel(); stringLabel.setName("stringLabel"); JTextField stringField = new JTextField(String.valueOf(stringValue)); JLabel messageFormatLabel = new JLabel(); messageFormatLabel.setName("messageFormatLabel"); Object[] messageArgs = { "Thomas", "James" }; JTextField messageFormatField = new JTextField( messageFormatValue.format(messageArgs) ); JLabel urlLabel = new JLabel(); urlLabel.setName("urlLabel"); JTextField urlField = new JTextField(String.valueOf(urlValue)); JLabel uriLabel = new JLabel(); uriLabel.setName("uriLabel"); JTextField uriField = new JTextField(String.valueOf(uriValue)); GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup(); hGroup.addGroup(layout.createParallelGroup() .addComponent(booleanLabel) .addComponent(byteLabel) .addComponent(charLabel) .addComponent(shortLabel) .addComponent(intLabel) .addComponent(longLabel) .addComponent(floatLabel) .addComponent(doubleLabel) .addComponent(stringLabel) .addComponent(messageFormatLabel) .addComponent(urlLabel) .addComponent(uriLabel) ); hGroup.addGroup(layout.createParallelGroup() .addComponent(booleanField) .addComponent(byteField) .addComponent(charField) .addComponent(shortField) .addComponent(intField) .addComponent(longField) .addComponent(floatField) .addComponent(doubleField) .addComponent(stringField) .addComponent(messageFormatField) .addComponent(urlField) .addComponent(uriField) ); layout.setHorizontalGroup(hGroup); GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(booleanLabel) .addComponent(booleanField) ); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(byteLabel) .addComponent(byteField) ); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(charLabel) .addComponent(charField) ); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(shortLabel) .addComponent(shortField) ); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(intLabel) .addComponent(intField) ); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(longLabel) .addComponent(longField) ); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(floatLabel) .addComponent(floatField) ); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(doubleLabel) .addComponent(doubleField) ); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(stringLabel) .addComponent(stringField) ); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(messageFormatLabel) .addComponent(messageFormatField) ); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(urlLabel) .addComponent(urlField) ); vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(uriLabel) .addComponent(uriField) ); layout.setVerticalGroup(vGroup); return panel; } public static void main(final String[] args) { launch(FieldInjectApplication.class, args); } @Resource private boolean booleanValue; @Resource private byte byteValue; // @Resource private char charValue; @Resource private short shortValue; @Resource private int intValue; @Resource private long longValue; @Resource private float floatValue; @Resource private double doubleValue; @Resource private String stringValue; @Resource private MessageFormat messageFormatValue; @Resource private URL urlValue; @Resource private URI uriValue; }
リソースファイルにおける定義は、クラス名とフィールド名を'.'で結んだ文字列をキーにとし、値をテキストで記述します。
FieldInjectApplication.booleanValue = true FieldInjectApplication.byteValue = 127 FieldInjectApplication.shortValue = 12345 FieldInjectApplication.intValue = 1234567890 FieldInjectApplication.longValue = 9876543210 FieldInjectApplication.floatValue = 1234.56789 FieldInjectApplication.doubleValue = 1234567890 FieldInjectApplication.stringValue = Hello Resource Injection FieldInjectApplication.messageFormatValue = Hello {0} and {1} FieldInjectApplication.uriValue = urn:isbn:0123456789x FieldInjectApplication.urlValue = http://java.sun.com:80
以下にリソースファイル全体を示します。
Application.title = Field Injection Application Application.id = FieldInjectApplication Application.vendorId = torutk booleanLabel.text = boolean: byteLabel.text = byte: charLabel.text = char: shortLabel.text = short: intLabel.text = int: longLabel.text = long: floatLabel.text = float: doubleLabel.text = double: stringLabel.text = string: messageFormatLabel.text = messageFormat: urlLabel.text = url: uriLabel.text = uri: FieldInjectApplication.booleanValue = true FieldInjectApplication.byteValue = 127 #FieldInjectApplication.byteValue = 0x20 #FieldInjectApplication.byteValue = 447&8 FieldInjectApplication.shortValue = 12345 #FieldInjectApplication.shortValue = 0x3039 #FieldInjectApplication.shortValue = 3039&16 FieldInjectApplication.intValue = 1234567890 #FieldInjectApplication.intValue = 0x499602D2 #FieldInjectApplication.intValue = 499602d2&16 FieldInjectApplication.longValue = 9876543210 #FieldInjectApplication.longValue = 0x24cb016ea #FieldInjectApplication.longValue = 1001001100101100000001011011101010&2 FieldInjectApplication.floatValue = 1234.56789 FieldInjectApplication.doubleValue = 1234567890 FieldInjectApplication.stringValue = Hello Resource Injection FieldInjectApplication.messageFormatValue = Hello {0} and {1} FieldInjectApplication.uriValue = urn:isbn:0123456789x FieldInjectApplication.urlValue = http://java.sun.com:80
D:\work\appframework> java -cp D:\work\AppFramework-1.01.jar;. \ FieldInjectApplication D:\work\appframework>
インジェクション時に使用するリソースファイルは、getResourceMapメソッドの引数で指定するクラスに対応づけられるものとなります。従って、以下のコードでインジェクションする場合は、
MyModel model = new MyModel();
Application.getInstance().getContext().getResourceMap().injectField(model);
アプリケーション・クラスのリソースファイルとなります。
一方、getResourceMapメソッドの引数でインジェクション対象クラスを指定すると、リソースファイルはそのインジェクション対象クラスのものが使用されます。
MyModel model = new MyModel();
Application.getInstance().getContext().getResourceMap(MyModel).injectField(model);
MyModelクラスのリソースファイルMyModel.propertiesが使用されます。
GUIアプリケーションにおいて、ラベルのような実行中に操作を行わない(クリックしても反応がない)GUIコンポーネントは少数で、多くのGUIコンポーネントはユーザーの操作に対して何らかの反応をする必要があります。
Swingでは、GUIコンポーネントにアクションが発生したことをActionListenerインタフェースで通知するイベント通知機構を提供しています。
また、メニュー、ツールバー、ショートカットキーなど複数のGUIコンポーネントに同じアクション(例:ファイルオープン)を割り当てることがよくあります。
これを極力一元化するため、ActionListenerを拡張してactionPerformanceメソッドの他、機能を示す文字列表現(Tooltipで表示される)、アイコン、enable/disableなどのイベントに関する状態をActionオブジェクトとして一括定義し、このActionオブジェクトを同じアクションを取るそれぞれのGUIコンポーネントに設定するという手段も提供されています。
JSR-296では、このActionオブジェクトのコーディングをアノテーションとリソースファイルを組み合わせることで最小限で済むようにしています。
以下、SwingのActionListenerインタフェースによる例、Actionインタフェースによる例を簡単に紹介し、それからJSR-296のアクション定義を説明します。
Swingでは、ユーザ操作等のイベントはイベント発生源のGUIコンポーネントに登録されたActionListenerオブジェクトに通知されます。
例えば、ボタンがクリックされたときに実行したい処理は、ActionListenerインタフェースを実装したクラスを定義し、そのクラスのインスタンスを生成し、ボタン・オブジェクトのaddActionListenerメソッドを呼び登録しておきます。ボタンがクリックされると、登録したActionListener実装クラスのactionPerformedメソッドが呼び出されます。
ソースコードで具体的に見ていきます。まず、以下のようにActionListenerインタフェースを実装したアクションクラスを定義します。
// アクションを定義したクラス class MyButtonAction implements ActionListener { public void actionPerformed(ActionEvent ev) { // イベント処理の記述 } }
次にこのアクションクラスのインスタンスを生成し、イベントと結び付けたいGUIコンポーネント(ここではJButton)に登録します。
JButton myButton = new JButton("押してね"); myButton.addActionListener(new MyButtonAction());
これで、[押してね]ボタンをクリックすると、MyButtonActionクラスのactionPerformedメソッドが呼び出されるようになります。
ActionListenerインタフェースを実装したアクション定義クラスを使用する方法では、イベント発生時の処理記述メソッド以外を共通的に定義していないため、同じアクションを複数のGUIコンポーネントに共通して設定したい場合に似たようなコードがあちこちに散在し汚ないソースコードとなりがちです。例えば、データを保存するアクションを、画面上のボタン、メニューバーの中の保存メニュー、ツールバー上の保存アイコンの3箇所に結び付けたい場合がそうです。(よくあることですね)
Swingでは、このような用途に便利なActionインタフェース(と雛形の実装を定義したAbstractActionクラス)を提供しています。Actionインタフェースは、ActionListenerの役割に加えて、アクションで使用するプロパティをいくつか保有する仕組みを供えています。
以下に、AbstractActionクラスを利用したアクション定義のコード例を示します。
// アクションを定義したクラス class MyButtonAction extends AbstractAction { MyAction() { putValue(NAME, "押してね"); putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_S, ActionEvent.CTRL_MASK) ); } public void actionPerformed(ActionEvent ev) { // イベント処理の記述 } }
JSR-296では、イベントを処理するメソッドを定義するのに、ActionListenerインタフェースやActionインタフェース(またはAbstractActionクラスなど)を実装(継承)した自前のアクションクラスを定義しなくても、簡単にイベント処理を記述できる仕組みとして@Actionアノテーションを提供しています。
まず、アクション(イベント処理)を記述するメソッドを@Actionアノテーション付きで定義します。以下は、アプリケーションの画面中のラベルの前景色、背景色を入れ換える処理を記述しています。
@Action public void swapLabelColor() { Color fore = label.getForeground(); Color back = label.getBackground(); label.setForeground(back); label.setBackground(fore); }
Swingを直に使う時は、ActionListenerインタフェースを実装するクラスを定義した上で、actionPerformedメソッドを定義してイベント処理を記述していました。JSR-296では、アプリケーション・クラスの1メソッドに@Actionアノテーションを追記するだけでイベント処理を記述することができます。
次に、GUIコンポーネント(例ではボタン)に上記アクションを結び付けます。
ActionMap map = getContext().getActionMap();
button.setAction(map.get("swapLabelColor"));
JSR-296では、ApplicationContextにリソースやアクション等のアプリケーション固有情報が管理されています。ApplicationContextは、アプリケーション毎に異なる情報を持つため、ApplicationクラスのメソッドgetContextでApplicationContextインスタンスの参照を取得します。
このApplicationContextインスタンスには、そのアプリケーションで使用するアクション・マップが管理されています。このアクション・マップには、プログラムの初期化時に@Actionアノテーションされたメソッドが保持されており、メソッド名をキーに取り出すことができます。
アクションマップからアクションを取得するときに、文字列でアクション名を指定しますが、スペルミスで合致するアクションが存在しない場合、nullが返ります。設定しているはずなのに、アクションが起動されないといった問題が発生した場合は、アクションが取得できているかチェックするコードを入れた方がよいでしょう。
アクション・マップからボタンに結び付けたいアクションをキーを指定して取得し、これをボタンのsetActionメソッドでボタンに設定します。
以下に、このサンプルのソースコード全体のサンプルを示します。
/* * Torutk Learning Project for Swing Application Framework. * * $Id: ButtonApplication.java 101 2007-10-08 12:32:56Z toru $ */ import java.awt.Color; import java.util.logging.Logger; import javax.swing.ActionMap; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import org.jdesktop.application.Action; import org.jdesktop.application.ApplicationContext; import org.jdesktop.application.SingleFrameApplication; public class ButtonApplication extends SingleFrameApplication { @Override protected void startup() { JPanel panel = new JPanel(); label = new JLabel(); label.setName("myLabel"); panel.add(label); button = new JButton(); button.setName("myButton"); panel.add(button); ActionMap map = getContext().getActionMap(); javax.swing.Action action = map.get("swapLabelColor"); if (action == null) { LOGGER.severe("Action[key=swapLabelColor] not found"); } button.setAction(action); show(panel); } @Action public void swapLabelColor() { Color fore = label.getForeground(); Color back = label.getBackground(); label.setForeground(back); label.setBackground(fore); } public static final void main(final String[] args) { launch(ButtonApplication.class, args); } private JLabel label; private JButton button; private static final Logger LOGGER = Logger.getLogger( ButtonApplication.class.getName() ); }
以下に、このサンプルで使用するリソースファイルを示します。
Application.title = Button & Action Resource Application Application.id = ButtonApplication Application.vendorId = torutk myLabel.opaque = true myLabel.background = 250, 240, 230 myLabel.foreground = #101010 myLabel.text = Hello Action myLabel.font = Dialog-PLAIN-32 myLabel.icon = penduke-transparent.gif myButton.text = Change Color
D:\work\appframework> javac -cp D:\work\AppFramework-1.01.jar;. \ ButtonApplication.java D:\work\appframework>
D:\work\appframework> java -cp D:\work\AppFramework-1.01.jar;. \ ButtonApplication
ButtonApplicationプログラムを実行すると、以下の画面が表示されます。
ボタンにはアクションを結び付けていますので、ボタンを押すと画面の色が変化します。
Swingはシングルスレッドで動作するように設計されているライブラリです。そのスレッドはイベント・ディスパッチ・スレッドと呼ばれます。したがって、イベント・ディスパッチ・スレッドの上で時間のかかる処理を行うと、画面の更新や他の操作が止まってしまいます。そのため、イベント・ディスパッチ・スレッドとは別にスレッドを起こして、そこで時間のかかる処理を行う必要があります。
JDK 5以前は、別のスレッドで行う処理をJavaのスレッドAPIを使ってSwingとは別にアプリケーション開発者が一から記述する必要がありました。JDK 6からは、SwingWorkerというクラスが導入され、Swingライブラリの一部として別スレッドでの処理を記述するフレームワークが用意されました。
JSR-296では、このSwingWorkerをさらに簡単に使うための仕組みが用意されています。
この節では、単純に一定時間スリープするだけの処理をアクションとして定義します。先のサンプル同様にラベルとボタンからなる簡単なウィンドウを表示するプログラム"SleepyTaskApplication"を作成します。起動すると以下の画面を表示します。
JSR-296の普通のアプリケーション作成のとおり、SingleFrameApplicationを継承したアプリケーション・クラスを定義します。画面構成としてラベルとボタンを定義しています。
前のサンプルとの違いは、アクション定義の部分です。@Actionアノテーションを付与したメソッドの定義を見ると、戻り値の型がTaskになっています。JSR-296では、Task型を返すメソッドをアクションとして定義すると、メソッドから返却されるTask型インスタンスのdoInBackgroundメソッドの処理がSwingWorkerの管理する別スレッド(ワーカー・スレッド)で実行されます。
今回のサンプルは、アクションが実行されると、リソースファイルから文字列とアイコンを読み出し、それらをラベルのテキストとアイコンにセットしてから、SleepyTaskのインスタンスを生成します。SleepyTask生成時にスリープ時間はリソースファイルから整数値として読み出し指定ています。
ちなみに、リソースファイルには任意のキー文字列とその値を記述することができ、それを読み込むときは、ResourceMapを使用します。
public class SleepyTaskApplication extends SingleFrameApplication { @Override protected void startup() { JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); messageLabel = new JLabel(); messageLabel.setName("messageLabel"); panel.add(messageLabel, BorderLayout.CENTER); startButton = new JButton(); startButton.setName("startButton"); panel.add(startButton, BorderLayout.SOUTH); ActionMap map = getContext().getActionMap(); startButton.setAction(map.get("startSleepyTask")); show(panel); } /** * 時間のかかる処理はTaskオブジェクトを返すアクションを定義します。 * このアクションは、開始されるとメッセージラベルのアイコンと文字列を * 変更します。 */ @Action public Task startSleepyTask() { ResourceMap map = getContext().getResourceMap(SleepyTaskApplication.class); messageLabel.setText(map.getString("asleepMessage")); messageLabel.setIcon(map.getIcon("asleepIcon")); return new SleepyTask(this, map.getInteger("asleepMillis"), messageLabel); } public static void main(final String[] args) { launch(SleepyTaskApplication.class, args); } private JLabel messageLabel; private JButton startButton; }
以下に、タスク定義を示します。
Task<T,V>は、SwingWorker<T,V>を継承しております。別スレッド(ワーカー・スレッド)で実行したい処理は、doInBackgroundメソッドをオーバーライドして記述します。
ワーカー・スレッドでの処理が終了すると、Swingのイベント・ディスパッチ・スレッドから終了状態に応じたTaskクラスのメソッドが呼び出されます。
これらはサブクラスでオーバーライドするよう設計されたメソッドです。(テンプレート・メソッド設計)
ただし、TaskクラスにはTaskListenerへ通知する機能が用意されているため、通常は上記のTaskクラスのメソッドをオーバーライドして画面更新を行うのではなく、TaskListenerで画面更新を行うのがよいでしょう。
Taskクラス(サブクラス)は、コンストラクタで特に指定しない限りTaskサブクラスのリソースファイルを使用します(この例では、SleepyTask.properties)。アプリケーション・クラスと同じリソースファイルを使用する場合、コンストラクタで特に指定する記述を行うか、この例のようにリソースマップを取得する際にgetResourceMapの引数でアプリケーション・クラスを指定します。
class SleepyTask extends Task<Void, Void> { SleepyTask(Application anApplication, int aSleepMillis, JLabel aLabel) { super(anApplication); sleepMillis = aSleepMillis; label = aLabel; } /** * 別スレッドで実行されるメソッド。 * Taskクラスで定義されるメソッドをオーバーライドします。 * Taskクラスでは、throws Exceptionで定義されています。なので、 * Exceptionのサブクラスをthrows宣言で定義するのは問題ない(かな) */ @Override protected Void doInBackground() throws InterruptedException { Thread.sleep(sleepMillis); return null; } /** * 時間のかかる処理が終了したときに呼ばれる、GUIを更新するメソッド。 * GUI更新はEDT上で行う必要があるため、doInBackgroundメソッドの中で * GUIを更新してはならない。 */ @Override protected void finished() { ResourceMap map = getContext().getResourceMap(SleepyTaskApplication.class); label.setText(map.getString("getupMessage")); label.setIcon(map.getIcon("getupIcon")); } private int sleepMillis; private JLabel label; }
以下に、このサンプルで使用するリソースファイルを示します。
Application.title = Sleepy Task Resource Application Application.id = SleepyTaskApplication Application.vendorId = torutk messageLabel.opaque = true messageLabel.background = 250, 240, 230 messageLabel.foreground = #101010 messageLabel.text = Hello Task messageLabel.font = Dialog-PLAIN-20 messageLabel.icon = penduke-transparent.gif startButton.text = Start Task asleepMessage = Now I fall asleep asleepIcon = snoozeDuke.gif asleepMillis = 8000 getupMessage = Now I got up! getupIcon = PensiveDuke.gif
D:\work\appframework> javac -cp D:\work\AppFramework-1.01.jar;. \ SleepyTaskApplication.java D:\work\appframework>
D:\work\appframework> java -cp D:\work\AppFramework-1.01.jar;. \ SleepyTaskApplication
先の画面で[Start Task]ボタンを押すと、startButtonに設定した"startSleepyTask"アクション、すなわちstartSleepyTaskメソッドが呼び出され、messageLabelのアイコンと文字列を変更します。続いてSleepyTaskをインスタンス化し、そのdoInBackgroundメソッドがワーカスレッド上で実行されます。
この結果、以下の画面となります。
一定時間(リソースファイル中のasleepMillisで指定したミリ秒)が経過すると、SleepyTaskインスタンスのdoInBackgroundメソッドが終了し、イベント・ディスパッチ・スレッド上でSleepyTaskインスタンスのfinishedメソッドが実行されます。finishedメソッドではラベルのアイコンと文字列を変更します。この結果、以下の画面となります。
単にSleepするだけのタスクにはあまり意義がありません。実際アプリケーションで必要とするのは、時間のかかる処理を実行し、その結果を取得することが必要です。
そこで、次は指定したディレクトリ以下のディレクトリにあるファイルを検索するプログラムを作成していきます。
指定したファイル名に一致するファイルを検索し、見つけた場所を表示するプログラムです。
ファイル検索プログラムの画面は以下のようになります。
ファイル検索プログラムの全体構造は、以下の3つのクラスから成ります。
FileSearchApplicationクラス
GUIアプリケーションを定義する
FileSearchTaskクラス
時間のかかるファイル検索を別スレッドで行う
FileSearchTaskListenerクラス
タスクの状態変化をイベント通知され対応する処理を行う
この3つのクラスは、いずれもSwing Application Frameworkが提供する基底クラスを継承しています。
まずFileSearchApplicationクラスでは、以下のメソッド、インナークラスを定義しています。
createUIメソッドが長いですが、ここは大半がGUIのレイアウトを設定しているコードですので、レイアウトに興味がない場合は最後数行のアクションの設定の部分まで読み飛ばしても構いません。なお、GUIレイアウトには、JDK 6で新規追加されたGroupLayoutを使っています。
public static final void main(final String[] args) { LOGGER.entering(FileSearchApplication.class.getName(), "main", args); launch(FileSearchApplication.class, args); LOGGER.exiting(FileSearchApplication.class.getName(), "main"); }
Applicationクラスのlaunchメソッドを呼び出しています。前後のloggerの呼び出し文は、JavaロギングAPIのコードです。このサンプルコードFileSearchApplicationでは、ロギングを各メソッドの入出箇所や要所要所で行っています。
loggerはFileSearchApplicationクラスのstaticフィールドで定義した変数です。
private static final Logger LOGGER = Logger.getLogger( FileSearchApplication.class.getName() );
@Override protected void startup() { LOGGER.entering(FileSearchApplication.class.getName(), "startup"); JComponent component = createUI(); show(component); LOGGER.exiting(FileSearchApplication.class.getName(), "startup"); }
GUIを構築するメソッドです。このサンプルでは画面レイアウトをprivateなcreateUIメソッドに記述しているので、そのメソッドの戻り値をSingleFrameApplicationクラスのshowメソッドに渡して、SingleFrameApplicationクラスが管理するJFrameに貼って表示させます。
private JComponent createUI() { LOGGER.entering(FileSearchApplication.class.getName(), "createUI"); JPanel panel = new JPanel(); panel.setName("myPanel"); GroupLayout layout = new GroupLayout(panel); panel.setLayout(layout); //部品間に若干の隙間を自動で作成 layout.setAutoCreateGaps(true); layout.setAutoCreateContainerGaps(true); titleLabel = new JLabel(); titleLabel.setName("titleLabel"); JLabel nameLabel = new JLabel(); nameLabel.setName("nameLabel"); nameField = new JTextField(); nameField.setName("nameField"); fileListModel = new DefaultListModel(); fileList = new JList(fileListModel); JScrollPane fileListPane = new JScrollPane(fileList); searchButton = new JButton(); searchButton.setName("searchButton"); JButton terminateButton = new JButton(); terminateButton.setName("terminateButton"); // 水平方向のレイアウト・グループ設定 GroupLayout.ParallelGroup hGroup = layout.createParallelGroup(); // タイトル用ラベルは水平方向にリサイズ可能設定 hGroup.addComponent( titleLabel, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE, 480//Short.MAX_VALUE ); // ファイル名ラベルとテキストフィールドは水平方向順番並び hGroup.addGroup(layout.createSequentialGroup() .addComponent(nameLabel) .addComponent(nameField) ); // ファイル一覧は水平方向単独 hGroup.addComponent(fileListPane); // 検索ボタンと終了ボタンは水平方向順番並び、ただし間隔は最大限空ける hGroup.addGroup(layout.createSequentialGroup() .addComponent(searchButton) .addPreferredGap( LayoutStyle.ComponentPlacement.RELATED, 120, Short.MAX_VALUE ) .addComponent(terminateButton) ); layout.setHorizontalGroup(hGroup); // 検索ボタンと終了ボタンの水平サイズを同じサイズにする layout.linkSize(searchButton, terminateButton); // 垂直方向のレイアウト・グループ設定 GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); vGroup.addComponent(titleLabel); // ファイル名ラベルとファイル名入力フィールドの横並びを揃える vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(nameLabel) .addComponent(nameField) ); // ファイル一覧は垂直方向単独 vGroup.addComponent(fileListPane); // 検索ボタンと終了ボタンの横並びを揃える vGroup.addGroup( layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(searchButton) .addComponent(terminateButton) ); layout.setVerticalGroup(vGroup); // アクションの設定 // 検索ボタンのクリックで"search"アクション発動 searchButton.setAction(getAction("search")); // ファイル名入力フィールドの[Enter]キーで"search"アクション発動 nameField.setAction(getAction("search")); // 終了ボタンのクリックで"terminate"アクション発動 terminateButton.setAction(getAction("terminate")); LOGGER.exiting(FileSearchApplication.class.getName(), "createUI", panel); return panel; }
表示する画面を作成するメソッドです。FileSearchApplicationでは、画面のレイアウトにJDK 6で新規導入されたjavax.swing.GroupLayoutクラスを使用しています。レイアウトの処理は、createUIメソッドに記述しています。Swing Application Frameworkを使ったアプリケーションでもレイアウトを構成する処理はSwingと変わりません。
ここで使用しているGroupLayoutは、元々はNetBeansのGUIビルダーで使われてきたもので、一般にはツールで画面を構成するときに使われるものというイメージがあります。しかし、ハンドコーディングでも割と容易に画面を構成することができます。昔からJavaにあるGridBagLayoutより簡単に望みの画面構成が作れます。
以下にFileSearchApplicationの画面レイアウトをGroupLayoutで構成した際のレイアウトの階層構造を水平方向、垂直方向について図示します。
GroupLayout:水平方向のレイアウト指定
水平方向に見ていくと、最上位の構造は横に1つですが縦に4列の要素があります。そこで、最上位をparallelGroupとしています。
縦の1つ目は、JLabelコンポーネント1つだけなので、最上位のparallelGroupの子要素として直接JLabelコンポーネントを追加します。
縦の2つ目は、JLabelコンポーネントとJTextFieldコンポーネントが水平方向に順番に並んだ要素です。そこで、最上位のparallelGroupの子要素としてsequentialGroupを追加します。そして、このsequentialGroupの子要素として、JLabelコンポーネントとJTextFieldコンポーネントを追加します。
縦の3つ目は、JListコンポーネントを保有するJScrollPaneコンポーネントが1つだけなので、1つ目のJLabelと同様最上位のparallelGroupの子要素として直接JScrollPaneコンポーネントを追加します。
縦の4つ目は、JButtonコンポーネントが2つ水平方向に並んでいます。ただし、JButtonコンポーネントはそれぞれ左端と右端に寄っており、その中間には空間があいています。 そこで、最上位のparallelGroupの3番目の子要素としてsequentialGroupを追加し、そのsequentialGroupの子要素として左端に寄せるJButtonコンポーネント、preferredGap(空間)、右端に寄せるJButtonコンポーネントを追加します。
GroupLayout:垂直方向のレイアウト指定
垂直方向に見ていくと、最上位の構造は縦に順番に4つの要素が並んでいます。そこで、最上位をsequentialGroupとしています。
縦の1つ目は、JLabelコンポーネント1つのみなので、最上位のsequentialGroupの1番目の子要素として直接JLabelコンポーネントを追加します。
縦の2つ目は、JLabelコンポーネントとJTextFieldコンポーネントが横に2列並んだ要素です。そこで、最上位のsequentialGroupの子要素としてparallelGroupを追加します。そして、このparallelGroupの子要素としてJLabelコンポーネントとJTextFieldコンポーネントを追加します。
縦の3つ目は、JScrollPaneコンポーネントが1つだけなので、最上位のsequentialGroupの子要素として直接JScrollPaneコンポーネントを追加します。
縦の4つ目は、JButtonコンポーネントが横に2つ並んでいます。そこで、最上位のsequentialGroupの子要素としてparallelGroupを追加し、このparallelGroupの子要素としてJButtonコンポーネントを2つ追加します。
ボタンやメニューを増やすと、何箇所にも同じアクションの取得処理を記述しなくてはなりません。そこで、ユーティリティメソッドgetActionを定義し、これを利用するようにします。
private javax.swing.Action getAction(final String anActionName) { LOGGER.entering( FileSearchApplication.class.getName(), "getAction", anActionName ); ActionMap map = getContext().getActionMap(); javax.swing.Action action = map.get(anActionName); LOGGER.exiting( FileSearchApplication.class.getName(), "getAction", action ); return action; }
ロギング用のコードがあるので一見長いですが、やっていることは2行です。
このメソッドを使ってボタンにアクションを設定します。
searchButton.setAction(getAction("search")); nameField.setAction(getAction("search")); terminateButton.setAction(getAction("terminate"));
アクションは次の2つがあります。
検索アクション("search")は、検索ボタン(searchButton)をクリックしたとき、およびテキストフィールド(nameField)で[Enter]キーが押されたときに実行します。そこで、searchButtonとnameFieldの両方にsetActionメソッドで"search"アクションを設定しています。
終了アクション("terminate")は、終了ボタン(terminateButton)をクリックしたときに実行します。そこで、terminateButtonにsetActionメソッドで"terminate"アクションを設定しています。
@Action(block=BlockingScope.ACTION) public Task search() { LOGGER.entering(FileSearchApplication.class.getName(), "search"); JFileChooser fileChooser = new JFileChooser(); fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); int ret = fileChooser.showOpenDialog(getMainFrame()); File path = null; if (ret == JFileChooser.APPROVE_OPTION) { path = fileChooser.getSelectedFile(); } Task task = new FileSearchTask(this, path, nameField.getText()); task.addTaskListener(new FileSearchTaskListener()); LOGGER.exiting(FileSearchApplication.class.getName(), "search", task); return task; }
検索アクションの定義は、searchメソッドです。検索は時間がかかる処理なので、Swing Application Frameworkで用意されているTaskを使って別スレッドで処理します。そこで、このsearchメソッドは戻り値型をTaskとしています。Taskを戻り値とするアクションは、Swingのスレッド(イベント・ディスパッチ・スレッド)とは別にSwingWorkerで用意されるバックグラウンド・スレッドでイベント処理をさせたいときに使用します。
アノテーション@Actionで、block属性にBlockingScope.ACTIONを指定しています。これは、1度このアクションが起動してタスクを実行開始したときに、タスクが終了するまでは再度タスクを実行しないようブロックするときに指定します。タスクが実行中の間、このアクションが設定されたボタン、テキストフィールドが非活性化されます。つまり、アクションが発動しないようになります。
また、このsearchメソッドは、Taskを生成する前に、Taskへ渡す検索開始場所をユーザーに指定してもらうために、JFileChooserダイアログを表示します。
検索タスク(FileSearchTask)の実行状況(実行完了したかどうか)を状態遷移イベントとして受けとり、表示を更新するためにTaskListenerインタフェースを実装したFileSearchTaskListenerクラスを設けています。このFileSearchTaskインスタンスを生成し、FileSearchTaskインスタンスに登録しています。
@Action public void terminate() { LOGGER.entering(FileSearchApplication.class.getName(), "termintate"); exit(); LOGGER.exiting(FileSearchApplication.class.getName(), "termintate"); }
終了アクションの定義は、terminateメソッドです。当初アプリケーションの終了なのでexitメソッドにしようとしました(その方が普通の発想ですね)。しかし、exitという名前は既にSwing Application Frameworkが提供するApplicationクラスにおいて定義されてしまっており、名前が衝突してしまうため、terminateという名前にしました。
このterminateメソッドの処理は、Applicationクラスのexitメソッドを呼び出すだけです。このexitメソッドは、アプリケーションを終了させるために使用します。
FileSearchTaskListenerは、Taskの状態遷移をイベントとして受け取るためのリスナーです。Swing Application Frameworkでは、TaskListenerインタフェースとして定義されています。TaskListenerインタフェースが受け取ることのできるTaskの状態遷移は以下です。
アプリケーション側で必要とするイベントはこれらの中の一部(恐らくはsucceededやfinished)だけなのに、これらすべてのメソッドを実装するのはとても面倒です。そこで、TaskListener.Adapter<T, V>が用意されています。これは、TaskListenerインタフェースの各メソッドを空実装しているだけのアダプタ・クラスですが、アプリケーション側の労力を大きく軽減してくれます。
interfaceの中にネストしたstaticなclassが定義されているというのも興味深いJavaプログラミング例です。
class FileSearchTaskListener extends TaskListener.Adapter<List<File>, File> {
@Override public void doInBackground(TaskEvent<Void> event) { LOGGER.entering( FileSearchApplication.FileSearchTaskListener.class.getName(), "doInBackground", event ); LOGGER.finest("current thread is : " + Thread.currentThread()); Task task = (Task)event.getSource(); titleLabel.setText(task.getMessage()); fileListModel.clear(); LOGGER.exiting( FileSearchApplication.FileSearchTaskListener.class.getName(), "doInBackground" ); }
タスクのmessageプロパティを取得しtitleLabelに設定します。ちなみに、タスクの参照は本メソッドの引数で渡されるTaskEventインスタンスに対してgetSourceメソッドを呼び出して取得します。
@Override public void process(TaskEvent<List<File>> event) { LOGGER.entering( FileSearchApplication.FileSearchTaskListener.class.getName(), "process", event ); List<File> files = event.getValue(); for (File file : files) { fileListModel.addElement(file); } LOGGER.exiting( FileSearchApplication.FileSearchTaskListener.class.getName(), "process" ); }
タスクが処理の途中で中間結果を随時出力(publish)した場合にこのメソッドが呼び出されます。タスクがpublishした内容は、本メソッドの引数で渡されるTaskEventインスタンスに対してgetValueメソッドを呼び出して取得します。
取得した内容をファイル一覧表示用のJListコンポーネントのモデルに追加します。
@Override public void succeeded(TaskEvent<List<File>> event) { LOGGER.entering( FileSearchApplication.FileSearchTaskListener.class.getName(), "succeeded", event ); JOptionPane.showMessageDialog( getMainFrame(), getContext().getResourceMap().getString("succeededMessage") ); // 結果を使用していないが、Taskから結果を取得するコードの例示。 List<File> finds = event.getValue(); LOGGER.fine(Arrays.toString(finds.toArray(new File[0]))); LOGGER.exiting( FileSearchApplication.FileSearchTaskListener.class.getName(), "succeeded" ); }
タスクが成功裏に処理を完了した場合にこのメソッドが呼び出されます。タスクが完了時に返却したオブジェクト(TaskクラスのdoInBackgroundの戻り値)は、本メソッドの引数で渡されるTaskEventインスタンスに対してgetValueメソッドを呼び出して取得します。
@Override public void finished(TaskEvent<Void> event) { LOGGER.entering( FileSearchApplication.FileSearchTaskListener.class.getName(), "finished", event ); Task task = (Task)event.getSource(); titleLabel.setText(task.getMessage()); LOGGER.exiting( FileSearchApplication.FileSearchTaskListener.class.getName(), "finished" ); }
タスクが成功/失敗いずれかによらず、タスクの実行が終了した場合にこのメソッドが呼び出されます。ここでは、messageプロパティを取得しtitleLabelに設定します。
FileSearchTaskクラスには、指定されたディレクトリ以下から指定された名前のファイルが存在するかを再帰的に検索するsearchFileメソッド、それをバックグラウンド・スレッドで起動するdoInBackgroundメソッド、検索終了後に実行されるfinishedメソッドが定義されています。
class FileSearchTask extends Task<List<File>, File> { /** * Task専用のリソースを使用するコンストラクタ。 * * @param anApplication このタスクを利用するアプリケーション * @param aRoot 検索開始場所 * @param aName 検索対象ファイル名 */ FileSearchTask(Application anApplication, File aRoot, String aName) { super(anApplication); LOGGER.entering( FileSearchTask.class.getName(), "FileSearchTask", new Object[] {anApplication, aRoot, aName} ); startDirectory = aRoot; name = aName; findFiles = new ArrayList<File>(); LOGGER.exiting(FileSearchTask.class.getName(), "FileSearchTask"); } @Override protected List<File> doInBackground() throws InterruptedException { LOGGER.entering(FileSearchTask.class.getName(), "doInBackground"); LOGGER.finest("current thread is : " + Thread.currentThread()); message("startMessage", name); findFiles.clear(); boolean isFind = searchFile(startDirectory); message("finishedMessage", isFind); LOGGER.exiting(FileSearchTask.class.getName(), "doInBackground", isFind); return findFiles; } private boolean searchFile(File aPath) { LOGGER.entering(FileSearchTask.class.getName(), "searchFile", aPath); boolean isFind = false; try { if (aPath == null) { isFind = false; return isFind; } else if (aPath.isFile()) { LOGGER.fine(aPath.getName() + ".equals(" + name + ")"); if (aPath.getName().equals(name)) { findFiles.add(aPath); publish(aPath); isFind = true; } else { isFind = false; } return isFind; } else { File[] files = aPath.listFiles(); for (File file : files) { if (searchFile(file)) { isFind = true; } } } return isFind; } finally { LOGGER.exiting(FileSearchTask.class.getName(), "searchFile", isFind); } } private final File startDirectory; private final String name; private List<File> findFiles; private static final Logger LOGGER = Logger.getLogger( FileSearchTask.class.getName() ); }
コンストラクタでは、アプリケーションのインスタンス、検索開始場所を示すFileインスタンス、検索対象ファイル名を受け取ります。アプリケーションのインスタンスは親クラスのコンストラクタに渡し、残りの2つはフィールドに保持します。
Swing Application Frameworkが提供する基底クラスTaskのコンストラクタには、リソースマップを指定するコンストラクタとリソースマップを指定しないコンストラクタがあります。
リソースマップを指定しないコンストラクタで生成したタスクは、Taskクラスおよびそれを継承したアプリケーション側のクラスに基づくリソースファイルを使用します。例えば、FileSearchTaskの場合、リソースファイルとしてTask.propertiesおよびFileSearchTask.propertiesの2つが適用されます。
一方、デフォルト以外のリソースマップを定義するためにリソースマップおよびプレフィックス文字列を指定するコンストラクタが提供されています。これは、アプリケーションとリソースファイルを共有したいときに、アプリケーション・クラスのリソースマップを指定する等のために使用できます。
doInBackgroundメソッドでは、まず検索中を示すmessageプロパティを設定します。messageメソッドは、第1引数で指定した文字列をキーにリソースファイルから文字列を取得し、messageプロパティに設定します。次に指定された名前のファイルを検索します。再帰処理を記述するので、直接doInBackgroundメソッドの中で検索コードを記述せず、別なメソッドsearchFileを作成しています。このsearchFileメソッドは検索対象ファイルが見つかれば、そのファイルのFile型インタンスを返し、見つからなければnullを返します。searchFileメソッドがリターンすると、検索終了を示すmessageプロパティをmessageメソッドを使って設定します。
アプリケーション・クラスに対応するリソースファイル(FileSearchApplication.properties)と、タスク・クラスに対応するリソースファイル(FileSearchTask.properties)の2つを記述します。
Application.title = File Search Application Application.id = FileSearchApplication Application.vendorId = torutk titleLabel.opaque = true titleLabel.background = 250, 240, 230 titleLabel.foreground = #101010 titleLabel.text = Welcome to File Search Application titleLabel.font = Dialog-PLAIN-16 titleLabel.icon = penduke-transparent.gif nameLabel.text = File name: nameField.columns = 20 searchButton.text = Search terminateButton.text = Exit succeededMessage = File search completed.
startMessage = Now searching %s ... finishedMessage = Find file : %s
D:\work\appframework> javac -cp D:\work\AppFramework-1.01.jar;. \ FileSearchApplication.java D:\work\appframework>
D:\work\appframework> java -cp D:\work\AppFramework-1.01.jar;. \ FileSearchApplication
別スレッドでの検索処理を実行している画面を以下に示します。
検索結果を表示している画面を以下に示します。
Look And Feelの設定は、Applicationクラスのリソースファイルに記述します。
Application.lookAndFeel = sun.swing.plaf.nimbus.NimbusLookAndFeel
本資料で使用したプログラムのダウンロードを以下に用意しました。
バージョン | 1.01 SwingWorker JDK6対応修正版 |
作成日 | 2007/10/08 |
ダウンロード | AppFramework-1.01.jar |
ソースコード | HelloApplication.java |
リソースファイル | HelloApplication.properties |
HelloApplication_ja.properties | |
HelloApplication_de.properties | |
日本語リソースファイル元ネタ | HelloApplication_ja.properties.sjis |
ソースコード | LabelApplication.java |
リソースファイル | LabelApplication.properties |
LabelApplication_ja.properties | |
日本語リソースファイル元ネタ | LabelApplication_ja.properties.sjis |
使用した画像ファイル | penduke-transparent.gif |
ソースコード | FieldInjectApplication.java |
リソースファイル | FieldInjectApplication.properties |
ソースコード | ButtonApplication.java |
リソースファイル | ButtonApplication.properties |
ソースコード | SleepyTaskApplication.java |
リソースファイル | SleepyTaskApplication.properties |
使用した画像ファイル | snoozeDuke.gif |
PensiveDuke.gif |
ソースコード | FileSearchApplication.java |
リソースファイル | FileSearchApplication.properties |
FileSearchTask.properties |
バージョン | リリース日 | 備考 |
---|---|---|
0.1 | 2007/01/30 | |
0.17 | 2007/02/09 | |
0.20 | 2007/02/24 | |
0.21 | 2007/03/06 | |
0.30 | 2007/04/19 | |
0.40 | 2007/05/25 | |
0.41 | 2007/06/01 | |
0.42 | 2007/06/07 | |
0.43 | 2007/06/14 | |
0.50 | 2007/06/23 | |
0.51 | 2007/07/19 | |
0.60 | 2007/08/25 | |
1.0 | 2007/08/25 | パッケージ名をapplication→org.jdesktop.applicationに変更 |
1.01 | 2007/09/21 | |
1.02 | 2007/10/17 | |
1.03 | 2007/11/01 |
Sun Developer向け技術記事。
JavaOne2007 TS-3942"JSR296:The Swing Application Framework"(Joshua Marinacci, Hans Muller著、2007/5)
JavaOne 2007での発表資料。以前の資料から少し(だけ)アップデートされただけ。
機運高まるクライアントサイドJava - Swing Application Frameworkを検証する(竹添直樹,木村真幸著、2007/05/21)
JSR-296 Ver.0.21でのNetBeans5.5への設定、ライフサイクル、アクション、非同期アクション、データの保存、SingleFrameApplication、および正規表現サンプル・アプリケーションの作成と一通り扱っている。
[ハウツー]Swing開発者待望 - Swing Application Framework(竹添直樹著、2007/02/26)
JSR-296 Ver.0.20での、ごく簡潔にサンプルを紹介。紹介しているのはボタン・クリックでダイアログ表示する@Action、別スレッドで動く@Action、簡単なリソースファイル記述。
Swing開発の救世主となるか - Swing Application Framework(杉山貴章著、2007/03/26)
JSR-296 Ver.0.21での@Actionの使い方だけだが、その部分は上記記事より少し詳しく丁寧に書かれている。
NetBeans 6.0 Desktop Applicationの作成
筆者によるNetBeans 6.0で標準搭載されたJSR-296のアプリケーション"Java Desktop Application"開発について紹介した記事です。
Improve Application Performance With SwingWorker in Java SE 6(John O'Conner著、2007/01)
Swingのスレッド(Event Dispatch Thread)の注意点と、SwingWorkerの使い方(詳細)が解説されている。
[1] | Hans Muller. Swing Application Framework JSR-296. JavaPolis, 2006. |
[2] | Hans Muller. A Simple Framework for Desktop Applications. Sun Microsystems Inc.(JavaOne 2006), 2006. |
[3] | Andrew Deitsch; David Czarnecki; 訳=風間一洋. JAVA国際化プログラミング. オライリー・ジャパン, 2002. |