[Java How To Programming] [Home on 246net] [Home on Alles net]
Powered by SmartDoc

Swing Application Framework (JSR-296)

TAKAHASHI, Toru
torutk@alles.or.jp

目次

Swing Application Framework概要

はじめに

Swing Application Frameworkとは

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の登場によってようやく変化が生じることになります。

JSR-296スペックリードHans Muller氏のインタビュー記事より

2006年10月25日付けの下記URLで公開されているHans Muller氏へのインタビュー記事より、Muller氏が語るSwing Application Frameworkの目的、範囲などを簡単にまとめてみました。

  1. JSR-277:Java Module System
  2. JSR-291:Dynamic Component Support for Java SE
  3. JSR-294:Improved Modularity Support
  4. JSR-295:Beans Binding

Swing Applicatin Frameworkの入手

2007年10月現在、JSR-296 Swing Application Frameworkは、java.netコミュニティのプロジェクトの1つとして開発が進められています。appframeworkプロジェクトのページから、ソースを入手可能です。サイズも小さく簡単にビルドできます。また、バイナリも用意されています。

  1. Downloadsからソースアーカイブをダウンロード
  2. Subversionリポジトリからソースコードをチェックアウト
  3. Downloadsからバイナリアーカイブをダウンロード

1.ソースアーカイブのダウンロード

appframeworkプロジェクトのページにある"Downloads,Docs,and Feedback"見出しの下のSource code: AppFramework-1.01-src.zip(1.01の部分はバージョン番号なので変化します)のリンクをクリックしダウンロードします。

2.リポジトリからチェックアウト

appframeworkプロジェクトのページの左側メニューにある"Subversion"のリンクを辿ると、Subversionリポジトリの閲覧ページになります。「ソースコードリポジトリへのアクセス」の説明にしたがってSubversionコマンドなりTortoiseSVNで(Windowsな人はこれが便利)チェックアウトします。

3.バイナリアーカイブのダウンロード

appframeworkプロジェクトのページの左側メニューにある"ドキュメント&ファイル"のリンクを辿り、"releases(0)"をクリックし、"1.01(3)"をクリックします。ダウンロードの画面を以下に示します。

図1-1 AppFrameworkのバイナリアーカイウ・ダウンロード

SwingApplicationFrameworkのビルド

一番簡単なのは、NetBeansでビルドしてしまう方法です。ソースアーカイブを展開すると、ディレクトリの中にnbprojectディレクトリがあるので、これをNetBeansで開きます(5)

また、コマンドライン環境でJDKでビルドすることも簡単です。本文書では、コマンドライン環境でのビルドを紹介します。

  1. NetBeansでの手順は本文書では省略します。なお、NetBeans 6.0では、標準でJSR-296の実装が付いています。

ビルドの前提条件

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. JavaSE 6単独でビルドできるように、SwingWorkerのimport文をJavaSE 6の標準パッケージ名へ修正する
  2. J2SE 5.0でもJavaSE 6でもビルドできるようjdesktopのSwingWorkerライブラリを使ってビルドする

本文書では、1.のJDK 6単体でビルドする手順を紹介します。

ビルド実行

1. JavaSE6単独でコマンドライン環境でビルドする

Java SE 6単独でビルドできるよう以下2つのファイルでimportしているSwingWorkerのパッケージ名を修正します。

Task.java(オリジナル)
import org.jdesktop.swingworker.SwingWorker;

から以下へ修正します。

Task.java(修正後)
import javax.swing.SwingWorker;

同様に、

TaskMonitor.java(オリジナル)
import org.jdesktop.swingworker.SwingWorker.StateValue;

から以下へ修正します。

TaskMonitor.java(修正後)
import javax.swing.SwingWorker.StateValue;
Swing Application Frameworkのjavacによるコンパイル
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ファイルにまとめます。

Swing Application Frameworkのjarによるアーカイブ
D:\work\AppFramework-1.01> jar cvf AppFramework-1.01.jar -C \
    classes .
    :
D:\work\AppFramework-1.01>

この手順で作成したJDK 6単独用フレームワークのJARファイルは、ダウンロード・コーナーに置いております。

Swing Application Framework最初の一歩

本節では、単純なラベル1つだけのウィンドウを表示するGUIプログラムを作成します。目標として以下に示すウィンドウを表示させます。

ソースコード(HelloApplication.java)の記述

ソースコードを以下に示します。public static voidなmainメソッドを持つ1つの小さなクラスです。

HelloApplication.java
/*
 * 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);
    }
}

以下に作成手順を示します。

  1. 作成するアプリケーションの継承元クラスを選択する

    フレーム(OSの画面上に表示するウィンドウのこと)が1つしかないアプリケーションを作成するときは、SingleFrameApplicationクラスを継承します。

  2. startupメソッドをオーバーライドする

    startupメソッドをオーバーライドして、そこにアプリケーションの画面初期化コードを記述します。この単純なHelloApplicationプログラムでは、フレームにJLabelを1つだけ貼った画面構成となります。通常のSwingアプリケーション同様にJLabelを生成します。なお、今回はラベルの文字列はコード中に固定で記述しています。

    (補足)Swing Application Frameworkに含まれるサンプルコードによっては、このstartupメソッドがpublicであったりprotectedであったりします。継承元(SingleFrameworkApplicationクラスのさらに継承元)のApplicationクラスを見ると、startupメソッドはprotectedで定義されているので、ここはprotectedとすべきでしょう。

  3. showメソッドに表示させたいSwingコンポーネントを渡して呼び出す

    SingleFrameApplicationクラスを継承した場合、トップレベル・ウィンドウのJFrameは継承元のSingleFrameApplicationクラスで管理されるので、アプリケーション作成者が直接JFrameを触る必要はありません。JFrameに対する設定、例えば画面レイアウトの実行(packまたはsetSize)、画面の具現化(setVisible)といった今までSwingアプリケーションを作成するときにコーディングしていたお決まりの記述は、継承元のSingleFrameApplicationクラスのshowメソッドを呼び出した中にあるため、記述量が大分少なくなりました。

  4. mainメソッドからは、Applicationクラスのlaunchメソッドを呼び出す

    mainメソッドからは、アプリケーション派生クラス(今回は、HelloApplication)を引数に渡してlaunchメソッドを呼び出します。コマンドライン引数をアプリケーション派生クラスに渡すために、第二引数に指定します。このコマンドライン引数は必要ならアプリケーション派生クラスでinitializeメソッドをオーバーライドして受け取ることができます。

コンパイルと実行

先程作成したSwing Application Frameworkのライブラリファイルをjavacのコマンドラインでクラスパス・オプションに設定しておきます。

HelloApplicationのコンパイル
D:\work\appframework> javac -cp D:\work\AppFramework-1.01.jar;. \
    HelloApplication.java
D:\work\appframework>

Swing Application Frameworkをjavaコマンドのコマンドラインでクラスパス・オプションに指定して実行します。

HelloApplicationの実行
D:\work\appframework> java -cp D:\work\AppFramework-1.01.jar;. \
    HelloApplication

さて、実行結果は以下のようになりました。最初の目標画面と少し違います。JLabelに設定した文字列("Hello World")は表示されていますが、ウィンドウのタイトル文字列が空になっています。

この問題には2つの解決方法があります。方法1は少し邪道で方法2がSwing Application Frameworkの思想に沿った解決です。

  1. SingleFrameApplicationが管理するJFrameの参照を取り出しタイトル文字列を設定する
    startupメソッド内に追加
        getMainFrame().setTitle("Hello JSR-296");
    
  2. リソースファイルでタイトル文字列を記述する
    resources/HelloApplication.properties
    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つの属性をリソースファイルに明示的に記述します。

resources/HelloApplication.properties
Application.title = Hello JSR-296
Application.id = HelloApplication
Application.vendorId = torutk

ロケール対応リソースファイルの定義

上述2.で定義したリソースファイルを国際化(ロケール)対応することにします。リソースファイルをロケール別に作成しておき、実行時のロケールに対応したリソースファイルの内容を反映させるというものです。

Swing Application Frameworkで使用するリソースファイルは、Javaの標準機能であるプロパティ・ファイルによるリソースバンドルを使用しています。リソースバンドルは国際化対応されているので、リソースバンドルのルールに従ってロケール別のリソース定義ファイル(プロパティファイル)を記述するだけです。

以下、日本語ロケール用のリソースファイルを記述し、日本語ロケールで実行します。

resources/HelloApplication_ja.properties.sjis
Application.title = こんにちは、JSR-296

ただし、Javaの規約でリソースバンドル・プロパティは非ASCII文字(日本語文字等)をUnicodeエスケープしたASCII文字で記述することが要求されます。

そこで、上記ファイルを一旦Shift_JISコードで保存し、次のJDK付属コマンドであるnative2asciiコマンドを使ってUnicodeエスケープに変換します。

リソースファイルの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エスケープした日本語ロケール用プロパティファイルの中身は以下となります。

resources/HelloApplication_ja.properties
Application.title = \u3053\u3093\u306b\u3061\u306f\u3001JSR-296

これを日本語ロケール環境で実行すると以下のように日本語でタイトルが表示されます。

日本語環境のOS上で実行すると、デフォルトで日本語ロケールになりますが、他のロケールで実行させたい場合、javaのコマンドライン・オプション(例:-Duser.language=en)で指定することができます。

ここで、試しに独語ロケールのプロパティファイルを用意して実行してみることにしましょう。

resources/HelloApplication_de.properties
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"となります。

WindowsVistaにおけるセッション情報格納先
C:\Users\torutk\AppData\Roaming\
torutk\ [Application.vendorID]で指定した名前
HelloApplication\ [Application.id]で指定した名前
mainFrame.session.xml

リソースの定義を行う

Javaには標準でリソースバンドル・プロパティ・ファイルという仕組みがあり、テキストデータをリソースファイルに記述して実行時にプログラムから読み込むことができます。しかし、テキストデータをプログラムで使用する各型へ変換し、所定のオブジェクトへ設定するのはアプリケーションで記述する必要があります。

JSR-296では、プログラムで扱う様々なデータをリソースファイルから取り込み所定のオブジェクトへ設定する処理を随分と面倒見てくれるようになっています。

GUI部品のプロパティをリソースファイルで定義

最初の一歩の節では、ウィンドウのタイトル文字をリソースファイルで定義しました。この節では、ウィンドウに貼っているラベル(javax.swing.JLabel)の以下のプロパティ(属性)をリソースファイルで定義する方法を見ていきます。

ソースコード(LabelApplication.java)の記述

ソースコードを作成します。前の節で作成したHelloApplication.javaとほとんど同じですが、JLabelのインスタンス生成箇所に違いがあります。ここでは、引数無しのコンストラクタでJLabelインスタンスを生成し、setNameメソッドで名前"myLabel"を指定しています。この名前が後程リソースファイルで使用されることになります。

LabelApplication.java
/*
 * 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仕様のプロパティ名を指定して値を記述しています。

LabelApplication.properties
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 表示するアイコン
boolean型のプロパティ
true|false|True|Falseのいずれかで指定します。
Color型のプロパティ
Red, Green, Blue, Alphaの4種類の値の組み合わせで指定します。Alphaは省略可能です。指定方法は、各色0〜255の10進数または、00〜FFの16進数です。10進数の場合は、R,G,B,Aと4つの値をカンマで区切って指定し、最後のAは省略可能です。16進数の場合は、#RRGGBBまたは#AARRGGBBで、桁数で判定するため0は省略不可能(ゼロサプレスなし)で記述します。
フォント
文字列で、「フォント名-形体-サイズ」を記述します。
アイコン
画像ファイル名を指定します。

今回使用した画像ファイル(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_ja.properties.sjis(Unicodeエスケープ前)
# LabelApplication.propertiesから変更する部分のみ記述すればよい
Application.title = ラベル・リソース・アプリケーション
myLabel.text = リソース定義

日本語のファイルはnative2asciiコマンドで変換(Unicodeエスケープ文字に変換)します。

リソースファイルの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_ja.properties
# \
    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コンポーネントのプロパティ以外にも、プログラム外部にデータを記述しておき実行時に取り込むことができます。

この節では、オブジェクトのフィールドに設定したい値をリソースファイルに定義する方法を見ていきます。

ソースコード(FieldInjectApplication.java)の記述

リソースファイルから値を設定するフィールドを@Resourceアノテーション付きで定義します。以下のサンプルコードは、Ver.1.0現在サポートされている型を全て挙げています。

@Resourceを付けたフィールド定義例
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メソッドの引数には、値を設定する対象オブジェクトを指定します。

以下にソースコード全体を示します。

FieldInjectApplication.java
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

以下にリソースファイル全体を示します。

FieldInjectApplication.properties
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メソッドの引数で指定するクラスに対応づけられるものとなります。従って、以下のコードでインジェクションする場合は、

引数なしのgetResourceMap
    MyModel model = new MyModel();
    Application.getInstance().getContext().getResourceMap().injectField(model);

アプリケーション・クラスのリソースファイルとなります。

一方、getResourceMapメソッドの引数でインジェクション対象クラスを指定すると、リソースファイルはそのインジェクション対象クラスのものが使用されます。

引数ありの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でのアクションの定義(1)〜ActionListener

Swingでは、ユーザ操作等のイベントはイベント発生源のGUIコンポーネントに登録されたActionListenerオブジェクトに通知されます。

例えば、ボタンがクリックされたときに実行したい処理は、ActionListenerインタフェースを実装したクラスを定義し、そのクラスのインスタンスを生成し、ボタン・オブジェクトのaddActionListenerメソッドを呼び登録しておきます。ボタンがクリックされると、登録したActionListener実装クラスのactionPerformedメソッドが呼び出されます。

ソースコードで具体的に見ていきます。まず、以下のようにActionListenerインタフェースを実装したアクションクラスを定義します。

ActionListenerによるアクション定義例
    // アクションを定義したクラス
    class MyButtonAction implements ActionListener {
        public void actionPerformed(ActionEvent ev) {
            // イベント処理の記述
        }
    }

次にこのアクションクラスのインスタンスを生成し、イベントと結び付けたいGUIコンポーネント(ここではJButton)に登録します。

ActionListenerクラスをGUIコンポーネントへ登録する例
    JButton myButton = new JButton("押してね");
    myButton.addActionListener(new MyButtonAction());

これで、[押してね]ボタンをクリックすると、MyButtonActionクラスのactionPerformedメソッドが呼び出されるようになります。

Swingでのアクション定義(2)〜Action

ActionListenerインタフェースを実装したアクション定義クラスを使用する方法では、イベント発生時の処理記述メソッド以外を共通的に定義していないため、同じアクションを複数のGUIコンポーネントに共通して設定したい場合に似たようなコードがあちこちに散在し汚ないソースコードとなりがちです。例えば、データを保存するアクションを、画面上のボタン、メニューバーの中の保存メニュー、ツールバー上の保存アイコンの3箇所に結び付けたい場合がそうです。(よくあることですね)

Swingでは、このような用途に便利なActionインタフェース(と雛形の実装を定義したAbstractActionクラス)を提供しています。Actionインタフェースは、ActionListenerの役割に加えて、アクションで使用するプロパティをいくつか保有する仕組みを供えています。

以下に、AbstractActionクラスを利用したアクション定義のコード例を示します。

Actionによるアクション定義例
    // アクションを定義したクラス
    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アノテーションを提供しています。

ソースコード(ButtonApplication.java)の記述

まず、アクション(イベント処理)を記述するメソッドを@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メソッドでボタンに設定します。

以下に、このサンプルのソースコード全体のサンプルを示します。

ButtonApplication.java
/*
 * 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()
    );

}

リソースファイルの記述(ButtonApplication.properties)

以下に、このサンプルで使用するリソースファイルを示します。

ButtonApplication.properties
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

ButtonApplicationのコンパイルと実行

ButtonApplicationのコンパイル
D:\work\appframework> javac -cp D:\work\AppFramework-1.01.jar;. \
    ButtonApplication.java
D:\work\appframework>
ButtonApplicationの実行
D:\work\appframework> java -cp D:\work\AppFramework-1.01.jar;. \
    ButtonApplication

ButtonApplicationプログラムを実行すると、以下の画面が表示されます。

ボタンにはアクションを結び付けていますので、ボタンを押すと画面の色が変化します。

時間のかかる処理を行うアクション(1)(簡単なTask)

Swingはシングルスレッドで動作するように設計されているライブラリです。そのスレッドはイベント・ディスパッチ・スレッドと呼ばれます。したがって、イベント・ディスパッチ・スレッドの上で時間のかかる処理を行うと、画面の更新や他の操作が止まってしまいます。そのため、イベント・ディスパッチ・スレッドとは別にスレッドを起こして、そこで時間のかかる処理を行う必要があります。

JDK 5以前は、別のスレッドで行う処理をJavaのスレッドAPIを使ってSwingとは別にアプリケーション開発者が一から記述する必要がありました。JDK 6からは、SwingWorkerというクラスが導入され、Swingライブラリの一部として別スレッドでの処理を記述するフレームワークが用意されました。

JSR-296では、このSwingWorkerをさらに簡単に使うための仕組みが用意されています。

この節では、単純に一定時間スリープするだけの処理をアクションとして定義します。先のサンプル同様にラベルとボタンからなる簡単なウィンドウを表示するプログラム"SleepyTaskApplication"を作成します。起動すると以下の画面を表示します。

ソースコード(SleepyTaskApplication.java)の記述

JSR-296の普通のアプリケーション作成のとおり、SingleFrameApplicationを継承したアプリケーション・クラスを定義します。画面構成としてラベルとボタンを定義しています。

前のサンプルとの違いは、アクション定義の部分です。@Actionアノテーションを付与したメソッドの定義を見ると、戻り値の型がTaskになっています。JSR-296では、Task型を返すメソッドをアクションとして定義すると、メソッドから返却されるTask型インスタンスのdoInBackgroundメソッドの処理がSwingWorkerの管理する別スレッド(ワーカー・スレッド)で実行されます。

今回のサンプルは、アクションが実行されると、リソースファイルから文字列とアイコンを読み出し、それらをラベルのテキストとアイコンにセットしてから、SleepyTaskのインスタンスを生成します。SleepyTask生成時にスリープ時間はリソースファイルから整数値として読み出し指定ています。

ちなみに、リソースファイルには任意のキー文字列とその値を記述することができ、それを読み込むときは、ResourceMapを使用します。

SleepyTaskApplication.java
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の引数でアプリケーション・クラスを指定します。

SleepyTaskのコード
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;
}

リソースファイルの記述(SleepyTaskApplication.properties)

以下に、このサンプルで使用するリソースファイルを示します。

SleepyTaskApplication.resources
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

SleepyTaskApplicationのコンパイルと実行

SleepyTaskApplicationのコンパイル
D:\work\appframework> javac -cp D:\work\AppFramework-1.01.jar;. \
    SleepyTaskApplication.java

D:\work\appframework>
SleepyTaskApplicationの実行
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メソッドではラベルのアイコンと文字列を変更します。この結果、以下の画面となります。

時間のかかる処理を行うアクション(2)(ファイル検索)

単にSleepするだけのタスクにはあまり意義がありません。実際アプリケーションで必要とするのは、時間のかかる処理を実行し、その結果を取得することが必要です。

そこで、次は指定したディレクトリ以下のディレクトリにあるファイルを検索するプログラムを作成していきます。

指定したファイル名に一致するファイルを検索し、見つけた場所を表示するプログラムです。

ファイル検索プログラムの画面は以下のようになります。

クラス構成

ファイル検索プログラムの全体構造は、以下の3つのクラスから成ります。

  1. FileSearchApplicationクラス

    GUIアプリケーションを定義する

  2. FileSearchTaskクラス

    時間のかかるファイル検索を別スレッドで行う

  3. FileSearchTaskListenerクラス

    タスクの状態変化をイベント通知され対応する処理を行う

この3つのクラスは、いずれもSwing Application Frameworkが提供する基底クラスを継承しています。

  1. FileSearchApplicationクラスは、SingleFrameApplicationクラスを継承
  2. FileSearchTaskクラスは、Task<List<File>, Void>クラスを継承
  3. FileSearchTaskListener(インナー)クラスは、TaskListenerインタフェースを実装するTaskListener.Adapter<List<File>, Void>クラスを継承

ソースコード(FileSearchApplicationクラス)の記述

まずFileSearchApplicationクラスでは、以下のメソッド、インナークラスを定義しています。

createUIメソッドが長いですが、ここは大半がGUIのレイアウトを設定しているコードですので、レイアウトに興味がない場合は最後数行のアクションの設定の部分まで読み飛ばしても構いません。なお、GUIレイアウトには、JDK 6で新規追加されたGroupLayoutを使っています。

mainメソッド

FileSearchApplicationクラスのmainメソッド
    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フィールドで定義した変数です。

loggerの宣言
    private static final Logger LOGGER = Logger.getLogger(
        FileSearchApplication.class.getName()
    );

startupメソッド

FileSearchApplicationのコード
    @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に貼って表示させます。

createUIメソッド

FileSearchApplicationのコード
    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で構成した際のレイアウトの階層構造を水平方向、垂直方向について図示します。

getActionメソッド

ボタンやメニューを増やすと、何箇所にも同じアクションの取得処理を記述しなくてはなりません。そこで、ユーティリティメソッドgetActionを定義し、これを利用するようにします。

アクションの取得メソッド(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行です。

このメソッドを使ってボタンにアクションを設定します。

2つのボタン、1つのテキストフィールドにアクションを設定
        searchButton.setAction(getAction("search"));
        nameField.setAction(getAction("search"));
        terminateButton.setAction(getAction("terminate"));

アクションは次の2つがあります。

  1. "search":検索を開始するアクション
  2. "terminate":プログラムを終了するアクション

検索アクション("search")は、検索ボタン(searchButton)をクリックしたとき、およびテキストフィールド(nameField)で[Enter]キーが押されたときに実行します。そこで、searchButtonとnameFieldの両方にsetActionメソッドで"search"アクションを設定しています。

終了アクション("terminate")は、終了ボタン(terminateButton)をクリックしたときに実行します。そこで、terminateButtonにsetActionメソッドで"terminate"アクションを設定しています。

searchメソッド

検索アクションの定義(searchメソッド)
    @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インスタンスに登録しています。

terminateメソッド

終了アクションの定義(terminateメソッド)
    @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クラス)の記述

FileSearchTaskListenerは、Taskの状態遷移をイベントとして受け取るためのリスナーです。Swing Application Frameworkでは、TaskListenerインタフェースとして定義されています。TaskListenerインタフェースが受け取ることのできるTaskの状態遷移は以下です。

cancelled
Taskがキャンセルされた
doInBackground
Taskが処理をこれから実行する
failed
Taskが完了前に失敗した
finished
Taskが完了した
interrupted
Taskが割り込まれた
process
Taskが途中経過を発行した
succeeded
Taskが完了に成功した

アプリケーション側で必要とするイベントはこれらの中の一部(恐らくはsucceededやfinished)だけなのに、これらすべてのメソッドを実装するのはとても面倒です。そこで、TaskListener.Adapter<T, V>が用意されています。これは、TaskListenerインタフェースの各メソッドを空実装しているだけのアダプタ・クラスですが、アプリケーション側の労力を大きく軽減してくれます。

interfaceの中にネストしたstaticなclassが定義されているというのも興味深いJavaプログラミング例です。

TaskListener.Adapterの継承
    class FileSearchTaskListener extends TaskListener.Adapter<List<File>, File> {

タスク実行開始前に処理すること(doInBackground)

FileSearchTaskListenerのコード
        @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メソッドを呼び出して取得します。

Taskの実行途中でpublishされた時に実行すること(process)

FileSearchTaskListenerのコード
        @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コンポーネントのモデルに追加します。

Taskの実行が成功したときに実行すること(succeeded)

FileSearchTaskListenerのコード
        @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メソッドを呼び出して取得します。

タスク完了後に処理すること(finished)

FileSearchTaskListenerのコード
        @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クラス)の記述

FileSearchTaskクラスには、指定されたディレクトリ以下から指定された名前のファイルが存在するかを再帰的に検索するsearchFileメソッド、それをバックグラウンド・スレッドで起動するdoInBackgroundメソッド、検索終了後に実行されるfinishedメソッドが定義されています。

FileSearchTaskのコード
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つを記述します。

FileSearchApplication.properties
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.
FileSearchTask.properties
startMessage = Now searching %s ...
finishedMessage = Find file : %s

FileSearchApplicationのコンパイルと実行

FileSearchApplicationのコンパイル
D:\work\appframework> javac -cp D:\work\AppFramework-1.01.jar;. \
    FileSearchApplication.java

D:\work\appframework>
FileSearchApplicationの実行
D:\work\appframework> java -cp D:\work\AppFramework-1.01.jar;. \
    FileSearchApplication

別スレッドでの検索処理を実行している画面を以下に示します。

検索結果を表示している画面を以下に示します。

アプリケーション全体の設定

Look And Feelの設定

Look And Feelの設定は、Applicationクラスのリソースファイルに記述します。

SampleApplication.properties
Application.lookAndFeel = sun.swing.plaf.nimbus.NimbusLookAndFeel

リンク情報

ダウンロード

本資料で使用したプログラムのダウンロードを以下に用意しました。

フレームワークのバイナリ・ファイル
バージョン 1.01 SwingWorker JDK6対応修正版
作成日 2007/10/08
ダウンロード AppFramework-1.01.jar
HelloApplicationサンプル・ソースコード
ソースコード HelloApplication.java
リソースファイル HelloApplication.properties
HelloApplication_ja.properties
HelloApplication_de.properties
日本語リソースファイル元ネタ HelloApplication_ja.properties.sjis
LabelApplicationサンプル・ソースコード
ソースコード LabelApplication.java
リソースファイル LabelApplication.properties
LabelApplication_ja.properties
日本語リソースファイル元ネタ LabelApplication_ja.properties.sjis
使用した画像ファイル penduke-transparent.gif
FieldInjectApplicationのサンプル・ソースコード
ソースコード FieldInjectApplication.java
リソースファイル FieldInjectApplication.properties
ButtonApplicationのサンプル・ソースコード
ソースコード ButtonApplication.java
リソースファイル ButtonApplication.properties
SleepyTaskApplicationのサンプル・ソースコード
ソースコード SleepyTaskApplication.java
リソースファイル SleepyTaskApplication.properties
使用した画像ファイル snoozeDuke.gif
PensiveDuke.gif
FileSearchApplicationのサンプル・ソースコード
ソースコード FileSearchApplication.java
リソースファイル FileSearchApplication.properties
FileSearchTask.properties

SwingApplicationFrameworkのリリース履歴

SwingApplicationFramework実装のリリース
バージョン リリース日 備考
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

参考文献(Web)

JSR-296の簡単な紹介

開発ツールとJSR-296

NetBeansでのJSR-296開発

Swingの参考

参考文献

[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.