[ Topページへ戻る ]
NetBeans 6.0
Desktop Applicationの作成
高橋 徹
目次
NetBeans 6.0では、Java SEでGUIを持つアプリケーションを作成するためのプロジェクト構成「Java Desktop Application」が追加されました。このJava Desktop Application構成は、Java SE 標準のSwing GUIライブラリに加えて、以下の機能を使用します。
これらを使用することで、小・中規模の典型的なGUIアプリケーションを簡単に作成できるようになります。
そこで、本記事では、NetBeans 6.0のJava Desktop Application構成でデジタル時計(ストップウォッチ機能付き)を作成してみます。
2008年1月18日現在、本記事で使用しているNetBeans 6.0は、バージョンは6.0.1 Dev 20080117000(日本語開発版)です。
なお、本記事とは別にJSR-296についての詳しい解説記事を書いています。
まず最初に、NetBeans 6.0で追加されたプロジェクト構成Desktop Applicationの雛形について見ていきます。雛形で生成されるクラス、画面、プロパティ・ファイルについて明らかにしていきます。
新規プロジェクトを生成します。NetBeans 6.0の[ファイル]メニュー→[新規プロジェクト]を選択し、以下の「新規プロジェクトダイアログ」で、"Java Desktop Application"を選択します。
図2-1 新規プロジェクト・ダイアログのプロジェクト種類選択
続いて、プロジェクト名、プロジェクトを作成するディレクトリ、クラス名(パッケージ名+クラス名)を入力します。
デフォルトでは、パッケージ名が”プロジェクト名(すべて小文字)”となり、クラス名は”プロジェクト名(の小文字).プロジェクト名App”となりますが、あまりよい命名でないので、以下のとおり修正します。
パッケージ名は一見階層化されていないように見えますが、文字入力でピリオドを入れると、それが階層と解釈されます。
図2-2 新規(プロジェクト)Desktop Applicationダイアログのプロジェクト設定
これで、以下のソースファイル、プロパティファイルから構成されるプロジェクトが生成されました。
図2-3 Desktop Applicationプロジェクト作成時に自動で生成されるソース・ファイルとプロパティ・ファイル
3つのJavaソースファイルと、3つのプロパティファイルが生成されています。
依存するライブラリが以下となります。
Java Desktop Applicationのプロジェクトを生成すると、以下の画面が生成されます。
図2-4 Desktop Application雛形のウィンドウ・デザイン画面
メニューバーとステータスバー、およびメインのパネルから構成される雛形の画面となっています。
生成したプロジェクト(DigitalClockAutonomous)をビルドします。
ビルド結果は、プロジェクト・ディレクトリの中の dist ディレクトリ下に実行可能JARファイルとして生成されます。
C:\Users\torutk\work\DigitalClockAutonomous +--- dist +--- DigitalClockAutonomous.jar +--- README.txt +--- lib +--- appframework-1.0.3jar +--- swing-layout-1.0.3.jar +--- swing-worker-1.1.jar +--- beansbinding-1.2.1.jar
実行可能JARファイル(DigitalClockAutonomous.jar)の他に、実行時に必要な追加ライブラリがlibディレクトリ下に収められています。NetBeans以外から実行するときは、このdistディレクトリ以下を丸ごとコピーすればOKです。
libディレクトリ下のJARファイル | 内容 |
---|---|
appframework-1.0.3jar |
JSR-296 Swing Application FrameworkのJAR。 |
swing-layout-1.0.2.jar |
GroupLayoutのJAR。JDK 6で標準のGroupLayoutを使用するなら不要。 |
swing-worker.jar |
SwingWorkerのJAR。JSR-296が内部で使用。今後JSR-296がJDK 6の標準SwingWorkerを使うようになれば不要になる。 |
beansbinding-1.2.1.jar |
Beansオブジェクトのプロパティ間を連携し値の変更を同期する。GUIデザイン画面上でバインディング設定をするとこのライブラリが追加される。 |
参考までに、実行可能JARファイルの中のマニフェストファイル(META-INF/MANIFEST.MF)を開くと以下のようになっています。
Manifest-Version: 1.0 Ant-Version: Apache Ant 1.7.0 Created-By: 1.6.0_02-b05 (Sun Microsystems Inc.) Main-Class: digitalclock.autonomous.ClockApp Class-Path: lib/appframework-1.0.jar lib/swing-worker.jar lib/swing-layout-1.0.2.jar X-COMMENT: Main-Class will be added automatically by build
デフォルトでは、Java2 SE 5.0でも動作するためにJDK 6でSwingに追加されたjavax.swing.GroupLayoutクラスではなく、外部ライブラリのorg.jdesktop.layout.GroupLayoutクラスを使用するコードが生成されます。
JDK 6標準のGroupLayoutを使用するには、画面(フォーム)を持つNetBeansで生成したクラスの設定を変更します。本記事の例では、画面(フォーム)を持つクラスはClockViewとClockAboutBoxなので、この両者のコード生成プロパティの「レイアウト生成スタイル」を、デフォルトの「Swingレイアウト拡張統合」から「標準Java 6コード」に変更します。
図2-5 コード生成でGroupLayoutをJDK 6標準か外部か選択
NetBeans上から実行するか、コマンドプロンプトからjavaコマンドでJARファイルを指定して実行するか、エクスプローラ等からJARファイルを直接ダブルクリックして実行すると、以下のウィンドウが現れます。
図2-6 Desktop Application雛形を実行したときの画面
[Help]メニューの[About..]を選択すると、以下のAboutダイアログが表われます。
図2-7 Desktop Application雛形で[Help]メニュー→[About...]を選択したときに表示されるダイアログ
Desktop Application雛形によって生成されるウィンドウのタイトルやAboutダイアログの文字は、いずれもプロパティ・ファイルに記述されています。
図2-8 Desktop Application雛形の画面とプロパティ・ファイル設定項目との対比
ClockApp.propertiesファイルの設定項目は以下となります。
# Application global resources Application.name = DigitalClockMVP Application.title = Basic Application Example Application.version = 1.0 Application.vendor = Sun Microsystems, Inc. Application.homepage = http\://appframework.dev.java.net Application.description = A simple java desktop application based on Swing Application Framework Application.vendorId = Sun Application.id = DigitalClockMVP Application.lookAndFeel = system
ClockView.propertiesファイルの設定項目は以下となります。(抜粋)
# Resources for the ShellFrame class # top-level menus fileMenu.text = File helpMenu.text = Help # @Action resources showAboutBox.Action.text = &About... showAboutBox.Action.shortDescription = Show the application's information dialog
Fileメニューの中には[Exit]があるのですが、このExitについてはプロパティ・ファイルで設定はできず、直接ハードコーディングされるコードになっています。本来はHelpメニューの[About]と同じくプロパティ・ファイルに記述すべきでしょう。
図2-9 Desktop Application雛形のAboutダイアログとプロパティ・ファイル設定項目との対比
ClockAboutBox.propertiesファイルの設定項目は以下となります。
title = About: ${Application.title} ${Application.version} closeAboutBox.Action.text = &Close appDescLabel.text=<html>${Application.description} versionLabel.text=Product Version\: vendorLabel.text=Vendor\: homepageLabel.text=Homepage\: #NOI18N imageLabel.icon=about.png
まず、ClockApp.propertiesで定義されているApplication.titleの項目を日本語します。NetBeans 6.0のプロジェクト表示でClockApp.propertiesを右クリックし、ポップアップされるメニューの[追加ロケール...]を選択します。
図2-10 プロパティファイルの追加ロケールメニュー
新規ロケールダイアログで日本語ロケールを選択します。
図2-11 新規ロケールダイアログで事前定義ロケールからja_JPを選択
NetBeans 6.0のプロジェクト表示でClockApp.propertiesを展開すると、デフォルト言語に加えて日本語が追加されています。右側のプロパティファイル編集エディタで日本語を記述することができます。
図2-12 日本語ロケールを追加しプロパティを編集
(日本語環境で)実行すると以下の画面が表示されます。ウィンドウのタイトルおよびAboutダイアログのタイトルが日本語となっています。
図2-13 Application.titleプロパティを日本語ロケールで日本語記述し実行
ところで、Javaの場合リソースファイルに非ASCII文字を記述するときは、ユニコード・エスケープ形式でなくてはなりません。NetBeans上では図11のように日本語を直接記述していますが大丈夫でしょうか。別なエディタで日本語ロケールのプロパティファイル ClockApp_ja_JP.propertiesを開いてみると以下のようにユニコード・エスケープ形式となっていることが分かります。
# Application global resources Application.name = DigitalClockAutonomous Application.title = \u30c7\u30b8\u30bf\u30eb\u6642\u8a08 Application.version = 1.0 Application.vendor = Sun Microsystems, Inc. Application.homepage = http\://appframework.dev.java.net Application.description = A simple java desktop application based on Swing Application Framework. Application.vendorId = Sun Application.id = ${Application.name} Application.lookAndFeel = system
プロパティファイルを右クリックしポップアップされるメニュー(図2-10)で[開く]を選択すると、以下の画面が表示されます。
図2-14 プロパティの編集(ロケール横並び)
ここから、デジタル時計の作成に入ります。
オブジェクト指向プログラミングにおいて、GUIプログラムの作成は大きく2つに大別されます。
1.は、単一目的の小さなGUIプログラムや、ダイアログによく適用される方法です。とりあえず作りだすには楽ですが、表示項目やデータ項目、制御ロジックが増えると一気に複雑度が増し破綻してしまいがちです。
2.は、分離の仕方によってさらにいくつかの実現方法に分かれていきます。現時点ではまだ明確なパターンとして固まるところにいませんので、筆者の考えで以下に分類してみました。
マーチン・ファウラー氏が氏のWebサイト上でGUIにまつわるパターンについて「GUI Architectures」というページを先頭に議論を展開しています。
「制御(コントロール)」という言葉の用法ですが、この言葉には、表示・ユーザ操作に対する振る舞いの制御の意味と、データ操作の制御の意味が含まれているように感じています。GUIの世界においては、ユーザ・インタフェースにおける制御の意味で制御を使用し、データ操作についてはロジック(ビジネスロジック)、処理、という言葉を使うのがよいのではと考えます。
まず、簡単に作り始めるため、渾然一体のAutonomouse View方式でデジタル時計を作成します。とはいえ、JSR-296 Swing Application Frameworkを使うことにより、表示(View)と制御(ActionやTask)とが多少分離されます。
次に、表示とモデルが分離したPresentation Separation方式でデジタル時計を作成します。JSR-295 Beans Bindingを使って時計のモデルと時計の表示とを結合する実装を行います。
まずは、画面レイアウトを作成します。レイアウトは、GUI構造の方式によらず同じです。
現在時刻を表示するためと、経過時間(ストップウォッチ)を表示するため、JLabelとJTextFieldの組み合わせををそれぞれ用にフォームに貼ります。
図3-1 デジタル時計のGUIレイアウトデザイン
各GUIパーツの変数名は、クラスのフィールドとして定義されます。デフォルトではjLabel1, jLabel2, ...のように意味と結びつかないフィールド名となるので、ひとつひとつ意味のある名前に修正します。
今回は、jLabel1, jLabel2, jTextField1, jTextField2と自動で振られた変数名を、上記図のようにcurrentTimeLabel, elapsedTimeLabel, currentTimeTextField, elapsedTimeTextFieldと変更しています。
時刻を表示するJTextFieldは、画面上に貼った後で、背景色(background)、フォント(font)、水平方向の配置(horizontalAlignment、表示するテキスト(text)を変更しています。上記図右下の elapsedTimeTextFieldのプロパティで太字になっている項目が修正箇所です。
Java Desktop Application(JSR-296)ではなく、通常のJavaアプリケーションでNetBeansのGUI機能でSwing部品をレイアウトした場合、部品のプロパティはJavaコード中に生成されます。しかし、JSR-296では部品のプロパティをリソースファイルに定義します。上記レイアウト後に、ClockView.propertiesファイルの中を見ると、以下が追加されています。
currentTimeLabel.text=Current Time: currentTimeTextField.text=00:00:00 #NOI18N currentTimeTextField.font=Monospaced-Plain-18 #NOI18N currentTimeTextField.background=250, 240, 230 elapsedTimeLabel.text=Elapsed Time: elapsedTimeTextField.text=00:00:00 #NOI18N elapsedTimeTextField.font=Monospaced-Plain-18 #NOI18N elapsedTimeTextField.background=250, 240, 230
ClockView.propertiesファイルを右クリックして[追加ロケール]で"ja_JP - 日本語 / 日本"を追加してから、ClockView.propertiesファイルを右クリックし[開く]でtext属性に日本語文字列を設定しておきます。
現在時刻の表示を行うために、一定周期でシステムクロックを取得しcurrentTimeTextFieldに時刻を設定します。周期処理を行うためには、スレッドを使用します。スレッドを使うにはいくつかの方法があります。JSR-296では、別スレッドで処理を実行するためのTaskクラスが提供されています。Java SE 6から搭載されたSwingWorkerやjavax.swing.Timer、またはThread API、Concurrent APIなどを利用する方法があります。
ここでは、JSR-296に合わせてTaskを使用した現在時刻の取得と表示を作成します。
まず、Taskクラスを継承したClockTaskを ClockViewクラスの内部クラスとして作成します。内部クラスにする理由は、Taskの中で画面表示部品(ClockViewクラスのフィールドcurrentTimeTextField)にアクセスする必要があるためです。
public class ClockView extends FrameView { :(中略) class ClockTask extends Task<Void, Void> { ClockTask(final long aPeriod) { super(Application.getInstance()); period = aPeriod; formatter = new SimpleDateFormat("HH:mm:ss"); } @Override protected Void doInBackground() throws Exception { while (!isCancelled()) { Thread.sleep(period); publish((Void)null); } return (Void)null; } @Override protected void process(List<Void> ignored) { Date now = new Date(); String currentTimeText = formatter.format(now); currentTimeTextField.setText(currentTimeText); } private long period; private DateFormat formatter; } :(中略) }
今回作成するClockTaskは、デジタル時計プログラムの実行中は常に一定周期で動き続けるものです。そこで、Taskが要求する2つの型パラメータ(結果を返す型、途中結果を渡す型)は使用しないためどちらもVoidとしています。
Taskを継承する場合のポイントは、doInBackgroundメソッドをオーバーライドする点です。このメソッドがAWTイベント・ディスパッチ・スレッドとは別スレッドで実行されます。ここではTaskがキャンセルされていない間はずっとperiod[ミリ秒]経過毎にpublishメソッドを呼び出します。
publishメソッドは、Taskにおいて中間結果を送信する際に使用するメソッドです。publishメソッドで通知すると、イベント・ディスパッチ・スレッドからprocessメソッドが呼び出されます。そこで、このprocessメソッドにGUIを更新するような処理を記述します。したがって、一定間隔(period[ミリ秒])でpublishを呼び出すと、イベント・ディスパッチ・スレッド上でprocessメソッドが一定間隔(period[ミリ秒]に近い周期)で呼ばれるわけです。
processメソッドでは、Dateインスタンスを生成することによりシステムクロックに基づく現在時刻を取得し、これをSimpleDateFormatで時分秒の書式化を行い、GUIの現在時刻表示領域(currentTimeTextField)にセットしています。
ClockTaskを生成して稼動させる必要があります。Taskを稼動させるタイミングは、GUI生成の初期化が終わった直後がよいでしょう。このタイミングは、アプリケーションのライフサイクルを管理するClockAppに追記します。ライフサイクル上は、GUIの生成が終わった後に呼び出されるready()メソッドが適当でしょう。ready()メソッドはApplicationクラスで定義されます。NetBeansで自動生成されるApplicationを継承したClockAppクラスにはオーバーライド定義がありません。そこで、ClockAppクラスでreadyメソッドをオーバーライドします。
ClockTaskは、ClockViewクラスの内部クラスとして定義しているので、ClockTaskのインスタンス生成にはClockViewクラスのインスタンスが必要となります。しかし、雛形として生成されたClockAppクラスではClockViewのインスタンス生成が以下のようになっており、そのままではreadyメソッドで参照することができません。
雛形として生成されたClockAppのstartupメソッド内部でClockViewがインスタンス化
@Override protected void startup() { show(new ClockView(this)); }
そこで、(ClockApp)を、ClockView型のフィールドを定義しそれにClockViewインスタンスを保持するようstartupメソッドを修正しています。ClockAppクラスのフィールドにClockViewを保持するように修正します。
public class ClockApp extends SingleFrameApplication { :(中略) @Override protected void startup() { clockView = new ClockView(this); show(clockView); } @Override protected void ready() { getContext().getTaskService().execute(clockView.new ClockTask(500L)); } private ClockView clockView; }
readyメソッドをオーバーライドし、その中でClockTaskを生成・発動しています。
この時点でプロジェクトを実行すると、現在時刻が1秒間隔で更新される時計ができあがっています。
いよいよ、経過時間を表示する、いわゆるストップウォッチ機能を盛り込みます。仕様としては、ボタンを押すと経過時間を表示開始し、次にボタンを押すと経過時間を停止するものとします。ボタンの文字も開始・終了と押すごとに変わるようにします。
まず、ボタンを経過時間表示領域の隣に配置します。
図3-2 デジタル時計のGUIレイアウトデザイン(2) ボタンの追加
このボタンを押したときの処理を、ClockViewクラスのメソッドとして定義します。JSR-296の場合、メソッドに@Actionアノテーションを付けるだけで、SwingのActionListenerやActionを実装したクラスを定義せずともイベントリスナーを作成できるようになっています。
まず、GUIデザイン画面上でボタンを右クリックし、ポップアップメニューを表示させます。
図3-3 デジタル時計のGUIレイアウトデザイン(3) ボタンのポップアップメニュー
ポップアップメニューから[Set Action...]を選択すると、Action設定ダイアログが表示されます。以下は、ダイアログで[Action to edit:]欄を[Create New Action...]に変更し、空欄に記述したあとの画面です。
図3-4 デジタル時計のGUIレイアウトデザイン(4) ボタンのアクションを設定
[アクションのクラス:]欄で指定したクラス(ここではClockViewクラス)に、[アクションのメソッド:]欄に記述した名前のメソッド(ここではcontrolElapsedTime)を@Anotation付与して生成します。以下に生成されたcontrolElapsedTimeメソッドを示します。
@org.jdesktop.application.Action public void controlElapsedTime() { // put your action code here }
ダイアログの属性:欄の各項目に設定・記述した内容は、ClockViewプロパティ・ファイルに追加されます。
controlElapsedTime.Action.text=Elapse controlElapsedTime.Action.accelerator=ctrl pressed E controlElapsedTime.Action.shortDescription=Start or Stop Elapsed Time
経過時間の開始・終了は、ボタンを押すごとに切り替えるものとし、ClockViewクラスにboolean型のisElapsedフィールドを設けてそこに保存します。また、経過時間の開始操作時の時刻をClockViewクラスにDate型のelapsedTimeフィールドを設けて保持します。
public class DigitalclockView extends FrameView { :(中略) @org.jdesktop.application.Action public void controlElapsedTime() { if (isElapsed) { elapsedTime = null; elapsedTimeButton.setText( getResourceMap().getString("elapsedTimeButton.startText") ); isElapsed = false; } else { elapsedTime = new Date(); elapsedTimeButton.setText( getResourceMap().getString("elapsedTimeButton.stopText") ); isElapsed = true; } } private boolean isElapsed; private Date elapsedTime; }
ボタンを押すごとにボタンの文字を変更します。文字はリソースファイル(ClockView.properties)から読み込むものとし、リソースファイルで定義するキーは以下の2つです。
FrameViewクラスを継承しているDigitalclockViewクラスでは、リソースファイルの内容を読み取るのは簡単です。getResourceMap()メソッドでリソースマップの参照を取得し、getStringメソッドでキーとなる文字列を指定します。
次に経過時間を表示するために、ClockTaskクラスを改良します。
まず、経過時間の文字列を生成するためのDateFormatを追加します。経過時間は時刻帯(TimeZone)を持たないので、ここではGMTとなるTimeZoneを設定しています。
class ClockTask extends Task<Void, Void> { ClockTask(final long aPeriod) { super(Application.getInstance()); period = aPeriod; currentTimeFormatter = new SimpleDateFormat("HH:mm:ss"); elapsedTimeFormatter = new SimpleDateFormat("HH:mm:ss"); elapsedTimeFormatter.setTimeZone(TimeZone.getTimeZone("Etc/GMT0")); } :(中略) private DateFormat elapsedTimeFormatter; }
次に、経過時間計測中のときは、周期的に経過時間の表示も更新します。
class ClockTask extends Task<Void, Void> { :(中略) @Override protected void process(List<Void> ignored) { Date now = new Date(); String currentTimeText = currentTimeFormatter.format(now); currentTimeTextField.setText(currentTimeText); if (isElapsed) { Date elapse = new Date(now.getTime() - elapsedTime.getTime()); String periodTimeText = elapsedTimeFormatter.format(elapse); elapsedTimeTextField.setText(periodTimeText); } }
ソースコード |
ClockApp.java |
ClockView.java |
|
ClockAboutBox.java |
|
リソースファイル |
ClockApp.properties |
ClockView.properties |
|
ClockAboutBox.properties |
|
プロジェクトアーカイブ |
DigitalClockAutonomous.zip |
最初の例では、一つのビュークラス(フォーム)ClockViewだけで、画面表示、時刻の駆動とほとんど全ての責務を実現していました。小規模なプログラムならこれでも作りきれるのですが、本格的なプログラムを作る場合、ビューとロジックを分離して複雑性を下げるように設計します。
そこで、デジタル時計のビューと時計のモデル(時計を駆動するロジック)を別々のクラスで実現し、ロジックとビューの間を「バインディング」で結び付けます。
Beans Bindingは、その名から推測できるとおり、Java Beans仕様のクラスが保持するプロパティ同士を結び付けます。そのため、バインディングするクラスはJava Beans仕様に従って定義することが必要です。Swing/AWTの部品群は、いずれもJava Beans仕様に沿って作られているため、これらは容易にバインディングすることができます。ここに、自作のクラス(例えばモデルを表現するクラス)を入れるには、自作クラスをJava Beans仕様に合わせる必要があります。とはいえ、Beans Bindingでは、全てのJavaBeans仕様を実現している必要はなく、最低限以下の項目さえ満たしていればよいようです。
public String getName() { ... } public void setName(String arg) { ... }
import org.jdesktop.application.AbstractBean; // Beans仕様のバウンド・プロパティ"name"を持つクラス。 public class Person extends AbstractBean { // バウンド・プロパティ"name"の取得メソッド public String getName() { return nickname; } // バウンド・プロパティ"name"の設定メソッド public void setName(String aName) { String oldName = nickname; nickname = aName; firePropertyChange("name", oldName, nickname); // 値の変更を通知するために呼ぶ } private String nickname; // プロパティを実際に保持するフィールド(プロパティ名とは無関係) }注意) クラス内の他の処理において、nicknameフィールドを直接変更すると、setNameメソッド内のfirePropertyChangeが呼ばれません。クラス内であってもnicknameフィールドを直接変更せずに、必ずsetNameメソッドを介して変更することをコーディング規約にするべきでしょう。
今回の時計のモデルは、「現在時刻」と「経過時間」の2つのプロパティを持つ必要があることはすぐに分かります。その他、経過時間は経過時間の表示あり/なしと状態があるので、「経過時間有効性」もプロパティとして持つように設計します。
まずは、AbstractBeanを継承し、現在時刻(文字列)、経過時間(文字列)、経過時間有効性(真偽値)の3つをプロパティとして持つクラスの雛形を記述します。
// AbstractBeanを継承するClockModel public class ClockModel extends AbstractBean { // 現在時刻プロパティ(読み出し) public String getCurrentTime() { } // 経過時間プロパティ(読み出し) public String getElapsedTime() { } // 経過時間有効性プロパティ(読み出し) public boolean isElapsed() { } // 経過時間有効性プロパティ(書き込み) public void setElapsed(boolean anElapse) { } }
プロパティの情報を保持するフィールドを記述し、最低限のメソッドの実装を記述します。
現在時刻、経過時間はフィールドとしてはDate型で保持します。JavaBeans仕様では、プロパティの型はあくまでsetter/getterメソッドの型ですので、このようにフィールドの型とメソッドの型が異なっていても構いません。Date型からString型への変換は、java.text.SimpleDateFormatクラスを利用することにします。
コンストラクタでは、参照型のフィールドの初期化を行っています。時刻表記は24時間制での時分秒とするため、SimpleDateFormatクラスの初期化時に、"HH:mm:ss"を指定しています。elapsedTimeFormatterは持続時間(2つの時刻の間の長さ)を表現するので、文字列化に際してタイムゾーンのオフセットが加減されないようオフセット0のゾーンをセットします。
// AbstractBeanを継承するClockModel public class ClockModel extends AbstractBean { // コンストラクタ public ClockModel() { currentTimeFormatter = new SimpleDateFormat("HH:mm:ss"); elapsedTimeFormatter = new SimpleDateFormat("HH:mm:ss"); elapsedTimeFormatter.setTimeZone(TimeZone.getTimeZone("Etc/GMT0")); currentTime = new Date(); elapsedTime = new Date(0L); } // 現在時刻プロパティ(読み出し) public String getCurrentTime() { return currentTimeFormatter.format(currentTime); } // 経過時間プロパティ(読み出し) public String getElapsedTime() { return elapsedTimeFormatter.format(elapsedTime); } // 経過時間有効性プロパティ(読み出し) public boolean isElapsed() { return isElapsed; } // 経過時間有効性プロパティ(書き込み) public void setElapsed(boolean anElapse) { isElapsed = anElapse; } private Date currentTime; private Date elapsedTime; private boolean isElapsed; private DateFormat currentTimeFormatter; private DateFormat elapsedTimeFormatter; }
コンパイルが通り、プロパティの取り出しができるようになりましたが、まだバウンド・プロパティ(プロパティの値が変更されたら通知する)が実装されていません。なお、現在時刻、経過時間のプロパティは、tickメソッド内で変更するものとし、外部から書き込み可能なsetterメソッドは用意していません。
経過時間有効性プロパティの変更を通知する
// 経過時間有効性プロパティ(書き込み) public void setElapsed(boolean anElapse) { boolean oldIsElapsed = isElapsed; isElapsed = anElapse; firePropertyChange("elapsed", oldIsElapsed, isElapsed); }
現在時刻、経過時間のプロパティの変更を通知する
private void tick() { currentTime.setTime(System.currentTimeMillis()); firePropertyChange("currentTime", null, getCurrentTime()); if (isElapsed()) { // TODO: elapsedTimeの更新処理を記述 firePropertyChange("elapsedTime", null, getElapsedTime()); } }
tickは周期的に呼び出します。周期呼び出しは、java.util.concurrentパッケージを使って別スレッドによって実現します。とりあえずstartメソッドにその処理を記述しました。
周期処理を実現するので、scheduleAtFixedRateで実行します。tick呼び出しは1つのスレッドで十分なので、newSingleThreadScheduledExecutorで周期実行Executorを生成します。周期実行する処理はRunnableインタフェースを実装したrunメソッド内に記述します。このrunメソッド内で前述のtickメソッドを呼び出します。
周期の指定をフィールドperiodの値で行います。
public void start() { ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(new Runnable() { public void run() { tick(); } }, 3000, period, TimeUnit.MILLISECONDS); }
周期の設定は、容易に変更できるようにリソースファイルに記述することにします。リソースファイルから反映するにはいくつか方法がありますが、ここではアノテーション@Resourceを付与したフィールドにインジェクションする方法を取ります。
フィールド・インジェクションは、Applicationクラス経由でインジェクション対象クラスのリソースマップを取得し、injectFieldsメソッドで行います。
private void injectFields() { Application.getInstance().getContext().getResourceMap(ClockModel.class).injectFields(this); } @Resource private long period;
リソースファイル"ClockModel.properties"にフィールドperiodにインジェクションする値を記述します。クラス名.フィールド名で指定します。
ClockModel.period = 1000
第3段階までで積み残してあった経過時間の処理などを追加すればClockModelの実装は完了です。
GUIレイアウトに関しては、経過時間有効性(true/false)を表わすためにJToggleButtonを使ったこと以外は前回と同じです。
図3-5 デジタル時計(Presentation分離)のGUIレイアウトデザイン(1)
次に、バインディングを使って、現在時刻表示テキストフィールド、経過時間表示テキストフィールド、経過時間有効性変更トグルボタンをClockModelのプロパティに対応付けます。
ClockViewクラス |
バインドの更新モード |
ClockModelクラス |
|
---|---|---|---|
currentTimeTextFieldのtextプロパティ |
読み取り専用 |
currentTimeプロパティ |
|
elapsedTimeTextFieldのtextプロパティ |
読み取り専用 |
elapsedTimeプロパティ |
|
ElapsedToggleButtonのselectedプロパティ |
読み取り/書き込み |
elapsedプロパティ |
NetBeans 6のGUIデザイン画面上で、バインディングの設定をするためには、バインド対象クラスがGUIデザイン機能の管理可能なコンポーネントとして対象フォーム上に置かれている必要があります。今回のClockModelは表示を持たないコンポーネントですが、GUIデザイン画面上でフォームにドラッグ&ドロップすることで、フォーム(ClockViewクラス)のフィールドとして生成され、他のコンポーネントと同様GUIデザイン画面上で操作対象となります。
図3-6 デジタル時計(Presentation分離)のGUIレイアウトデザイン(2) モデルをドラッグ&ドロップ
ClockModel.ajvaをGUIデザイン画面上にドラッグ&ドロップすると、フォームのフィールドにClockModelが追加されます。
図3-7 デジタル時計(Presentation分離)のGUIレイアウトデザイン(3) フォーム(ClockView)のフィールドにモデル(ClockModel)が追加された
注)ドラッグ&ドロップするまえに、対象ソースファイルはコンパイルしておく必要があります。また、コンパイルされたクラスファイルを削除している状況でNetBeans上でViewファイルを開くと、コンポーネントが見つからないと警告ダイアログが表示されます。そのときは、Viewファイルをいったん閉じて、ドラッグ&ドロップしたクラスを再度コンパイルしてからViewファイルを開きます。
ソースコード上は、GUIデザイン機能によってClockViewクラスのinitComponentメソッドの中に自動生成されます。
public class ClockView extends FrameView { : public ClockView(SingleFrameApplication app) { super(app); initComponents(); : } private void initComponents() { : clockModel = new digitalclock.pm.ClockModel(); : } : private digitalclock.pm.ClockModel clockModel; : }
ClockModelのプロパティの値を画面に表示するため、各プロパティと表示するコンポーネントのプロパティとをバインドします。
現在時刻を表示するコンポーネント(currentTimeTextField)を選択し、右クリックで表われるポップアップメニューの[バインド]→[text]を選択します。
図3-8 デジタル時計(Presentation分離)のGUIレイアウトデザイン(4) バインドの設定(1)
すると、バインドダイアログが表示されます。currentTimeTextFieldのtextプロパティにバインドするコンポーネントを[ソースをバインド]欄で選択します。ここでは、clockModelを選択します。
図3-9 デジタル時計(Presentation分離)のGUIレイアウトデザイン(5) バインドの設定(2)
次に選択したコンポーネントのどのプロパティとバインドするかを[式をバインド]欄で選択します。ここでは、currentTimeを選択します。
図3-10 デジタル時計(Presentation分離)のGUIレイアウトデザイン(6) バインドの設定(3)
バインドするソースと式を選択した後、[詳細]タブをクリックします。
図3-11 デジタル時計(Presentation分離)のGUIレイアウトデザイン(7) バインドの設定(4)
「プロパティーを更新」の[モードを更新]欄は、現在時刻の表示の場合は片方向(ソース:ClockModelから読み取り)なので、図3-11のように「ソースから読み取りのみ(読み取り専用)」を選択しています。
初期値(ソースがnullを返したとき)として、00:00:00 を表示するように、「代替値」の[ソースの値がNULL]欄に指定しています。
上記の「現在時刻(currentTime)のバインド」と設定はほとんど同様です。違いは、バインドダイアログでの[式をバインド]欄で選択する項目です。図3-10で、"elapsedTime"をここでは選択します。
上記の「現在時刻(currentTime)のバインド」と設定はほとんど同様です。違いは、バインドダイアログでの[式をバインド]欄で選択する項目が"elapsed"となる点(図3-10参照)、バインドダイアログの詳細タブで[モードを更新]欄で選択する項目が"常に同期(読み取り/書き込み)"となる点です。
さて、最後に一つ残っている課題が、ClockModelの周期処理を誰がいつ開始させるかです。
ClockModelの周期処理は、startメソッドを呼ぶことで開始します。ClockModelはClockViewのinitComponentsメソッド内で生成されます。簡単なのは、コンストラクタの後でstartメソッドを呼ぶことです。ClockViewクラスのコンストラクタで、initComponents()呼び出しの次の行でよいでしょう。今回はこの方法で実装しました。
public ClockView(SingleFrameApplication app) { super(app); initComponents(); clockModel.start(); :
GUIの設計方針の観点で見ると、ビューがモデルを生成し制御するのはあまりよい構造ではないので、ClockAppクラスで行うのがよいと思います。その場合、ClockAppクラス内でClockModelをnewし、ClockViewに渡す処理を実装することになります。
ソースコード |
ClockApp.java |
ClockView.java |
|
ClockAboutBox.java |
|
ClockModel.java |
|
リソースファイル |
ClockApp.properties |
ClockView.properties |
|
ClockAboutBox.properties |
|
ClockModel.properties |
|
プロジェクトアーカイブ |
DigitalClockPM.zip |
SwingのJTable部品を使ったGUIが、バインディング機能で簡単に作成できるようになりました。
表はある決まった項目を持つデータが複数行並ぶという構造のため、表示項目に対応するプロパティを持つクラスが複数格納されたコレクションクラスから表の内容を定義するという方法です。従来のSwingでのプログラミングでは、TableModelインタフェースを実装する表専用データ構造をプログラミングする必要がありましたが、バインディングを使うことで、汎用データ構造であるJava
Collection API(特にListインタフェース実装系)をそのままJTableの表示に使うことができるようになりました。
まずは、簡単な例として、あらかじめデータがコレクション型(この例ではjava.util.Listインタフェース実装クラス)に用意されており、それを画面に表示するプログラムを作成します。
続いて、コレクション型のデータが途中で更新される場合のプログラムを作成します。
ここでは、機関車クラス(Locomotive)が複数詰められたList型データを表に示すプログラムを作成します。
まずは、NetBeans 6の新規プロジェクト作成で、「Javaデスクトップアプリケーション」を選択します。自動生成される雛形クラスを含めて、以下のクラスを作成します。
GUIデザインに移りたくなるのを堪えて、まずはデータ作りから始めます。
名前、機関車番号(ID)、塗装色の3つのプロパティを持つクラスを定義します。
public class Locomotive { public Color getColor() { return color; } public Locomotive setColor(Color aColor) { color = aColor; return this; } public int getId() { return id; } public Locomotive setId(int anId) { id = anId; return this; } public String getName() { return name; } public Locomotive setName(String aName) { name = aName; return this; } private String name; private int id; private Color color; }
注記) 各setterメソッドで、戻り値がvoidではなく自身のクラスLocomotiveとしています。これは、複数のプロパティをまとめて指定する際に役立つテクニックです。以下に例を示します。
Locomotive one = new Locomotive().setName("Thomas").setId(1).setColor(Color.BLUE);
Locomotive(機関車)を所有するRailwayCompany(鉄道会社)を表現します。プロパティとして複数の機関車を要素に持つListインタフェース実装型のオブジェクトを保持します。
なお、保有機関車はRailwayCompanyクラスのコンストラクタで事前に作っています。
public class RailwayCompany { public RailwayCompany() { locomotives = new ArrayList<Locomotive>(); locomotives.add(createLocomotive("Thomas", 1, Color.BLUE)); locomotives.add(createLocomotive("Gordon", 4, Color.BLUE)); locomotives.add(createLocomotive("James", 5, Color.RED)); locomotives.add(createLocomotive("Percy", 6, Color.GREEN)); } public List<Locomotive> getLocomotives() { return locomotives; } private Locomotive createLocomotive(String aName, int anId, Color aColor) { return new Locomotive().setName(aName).setId(anId).setColor(aColor); } private List<Locomotive> locomotives; }
次のGUIデザインに移る前に、RailwayCompanyクラスをコンパイルしておきます。
Javaデスクトップアプリケーションのプロジェクト生成時にデフォルトで生成されるこのViewクラスに、表題のJLabelと表のJTableを貼り付けます。
図4-1 機関車のGUIレイアウトデザイン(1) 表題ラベルと表の貼り付け直後
表題のJLabelの変数名を"jLabel1"から"locomotiveTableLabel"に変更し、textプロパティを"Locomotive Table"に変更します。
表のJTableの変数名を"jTable1"から"locomotiveTable"に変更し、表を囲むJScrollPaneの変数名を"jScrollPane1"から"locomotiveTableScrollPane"に変更します。
プロジェクト・ツリー上のRailwayCompany.javaを、LocomotiveTableViewのGUIデザイン画面上にドラッグ&ドロップします。
JTableを選択し右クリックでポップアップするメニューの[バインド]→[elements]を選択します。以下の図に示します。
図4-2 機関車のGUIレイアウトデザイン(2) JTableのバインド設定(1)
バインドのダイアログが表示されるので、[ソースをバインド]欄は機関車一覧プロパティを持つrailwayCompanyを選択し、[式をバインド]欄は、Listインタフェース型のlocomotivesを選択します。以下の図4-3にその画面を示します。
図4-3 機関車のGUIレイアウトデザイン(3) JTableのバインド設定(2)
Listインタフェース型のプロパティを指定すると、表に表示する項目(列)とその並びを指定するデザイン画面が表示されます。以下の図4-4に示します。
図4-4 機関車のGUIレイアウトデザイン(4) JTableのバインド設定(3)
表に表示する列とその並びの選択を終え、[了解]ボタンを押すと、GUIデザイン画面上の表の見え方が反映されます。以下の図4-5にGUIデザイン画面を示します。
図4-5 機関車のGUIレイアウトデザイン(5) JTableのバインド設定(4)
これで最低限の設定は完了しました。プロジェクトを実行します。
4.1.1 のプログラムでは、起動後にListの内容を変更しても、表には反映されません。そこで、Listの変更に応じて表のGUIが更新されるようにするには、org.jdesktop.observablecollectionsパッケージのObservableListを使用します。
RailwayCompanyがビューに渡す機関車一覧の型を、List<Locomotive>からObservableList<Locomotive>に変更します。
public class RailwayCompany { public RailwayCompany() { locomotives = ObservableCollections.observableList(new ArrayList<Locomotive>()); locomotives.add(createLocomotive("Thomas", 1, Color.BLUE)); locomotives.add(createLocomotive("Edward", 2, Color.BLUE)); locomotives.add(createLocomotive("Henry", 3, Color.GREEN)); locomotives.add(createLocomotive("Gordon", 4, Color.BLUE)); locomotives.add(createLocomotive("James", 5, Color.RED)); locomotives.add(createLocomotive("Percy", 6, Color.GREEN)); } public ObservableList<Locomotive> getLocomotives() { return locomotives; } private Locomotive createLocomotive(String aName, int anId, Color aColor) { return new Locomotive().setName(aName).setId(anId).setColor(aColor); } private String name; private ObservableList<Locomotive> locomotives; }
簡易にデータを追加するため、画面上にボタンを1つ追加して、RailwayCompanyのgetLocomotivesメソッドで取得するリストに1つLocomotiveデータを追加する処理を記述します。
図4-6 機関車のGUIレイアウトデザイン(6) JTableデータ追加操作ボタン
ボタンを右クリックしてポップアップするメニューから、[アクションを開く]を選択して「アクションを設定」ダイアログを開きます。ポップアップメニューは図3-3を、ダイアログは図3-4を参照してください。
ダイアログで、アクション欄[新規アクションを作成]で、このLocomotiveTableViewクラスにメソッド"insertNewItem"を作成します。
@Action public void insertNewItem() { Locomotive one = new Locomotive().setName("Toby").setId(7).setColor(new Color(153, 76, 0)); railwayCompany.getLocomotives().add(one); }
ボタンが押されると、新しいLocomotiveインスタンスを生成し、RailwayCompanyインスタンスが管理するリストに追加します。
これでデータ更新に対応した表画面が完了しました。プロジェクトを実行します。
Color列に表示される文字は、"java.awt.Color[r=0, g=255,b=0]"のように、java.awt.ColorクラスのtoString()メソッドが生成する文字列です。のtoStringメソッドは、オブジェクトの内容をユーザーインタフェースに表現するというよりは開発者がデバッグ等するとき向けのものです。
表示文字列を改善するには、Colorクラスのサブクラスを定義してtoStringメソッドをオーバーライドする方法もありますが、画面表示文字列のためだけにGUIとは直接関係ないクラスを定義するのは責務上好ましい設計ではありません。
JTableではなく、単一のデータを表示するコンポーネントの場合、バインディングの設定で「型変換」(コンバータ)を指定することで、そのデータ固有の型からString型へ変換させることができます。しかし、JTableの場合、型変換はテーブルとバインドするデータ(全体)に対する型変換となります。各列固有の変換処理としては使えそうにありません。
そこで、他の手段を探してみたところ、どうやらTableCellRendererという仕組みを使うのがよさそうです。TableCellRendererは、JTableの各列毎に指定可能で、それぞれの列のセルを表示するComponent派生型を定義します。
ここでは、Color型のデータから、その色で塗りつぶし表示を行い、その横にRGB値を数値で表示するセルを作成します。
インタフェースTableCellRendererを実装するか、既存のクラスDefaultTableCellRendererを継承するかの実装方針が想定されます。ここでは、色の塗りつぶしをIconにより、RGB値はテキストで行うJLabelの表示機能が適当と判断し、JLabelでセルを描画するDefaultTableCellRendererを継承することにしました。
public class ColorTableCellRenderer extends DefaultTableCellRenderer { public ColorTableCellRenderer() { icon = new RectIcon(12, 12); setIcon(icon); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); Color item = (Color)value; icon.setColor(item); String text = "#" + Integer.toHexString(item.getRGB()); setText(text); return this; } private RectIcon icon; }
アイコンサイズは標準的な文字サイズ12ptに合わせて12ピクセルとしています。セルの高さに応じて変えるといった高度な処理をするのが本来かもしれません。
塗りつぶしIconはIconインタフェースを独自に実装したRectIconとして定義します。Iconクラスは以外と簡単でした。
class RectIcon implements Icon { private Color color; private int width; private int height; RectIcon(int aWidth, int aHeight) { width = aWidth; height = aHeight; } public void setColor(Color aColor) { color = aColor; } public void paintIcon(Component c, Graphics g, int x, int y) { g.setColor(color); g.fillRect(x, y, width, height); } public int getIconWidth() { return width; } public int getIconHeight() { return height; } }
GUIデザイン画面上でテーブルを選択した状態で右クリックすると、ポップアップメニューが表示されます(図4-2参照)。メニューから、[表の内容]を選択します。カスタマイザダイアログが表示されるので、列タブを選択します。
表に表示される各列の設定がリストで出てくるので、今回レンダリングを設定するColor列を選択します。
レンダラ欄の右端のボタン[...]をクリックします。すると、rendererダイアログが表示されるので、rendererプロパティーを設定欄のリストボックスから[カスタムコード]を選択します。プロパティーコード欄が空白となっているので、上述で作成したColorTableCellRendererクラスのインスタンスを生成する式を記入します。この設定画面を以下の図4-7に示します。
図4-7 機関車のGUIレイアウトデザイン(7) JTable列設定画面からレンダラを設定
では、Color列を改良したLocomotiveTableViewを実行してみます。
図4-8 機関車のGUI実行画面