[Indexへ戻る]
Project Kanday
2004.6.1よりアクセス
Hello Againプロジェクトによる最初の開発成果であるDesktop編では、Java 2 Standard Edition(以降J2SE)の範疇において、Hello Worldを表示するプログラムを作成します。ただし、単にコンソールへ文字列を出力するだけの従来のHello Worldプログラムを作っても、J2SEの機能のほんの一部しか触りません。そこで、本プロジェクトがおくるHello Again Desktop編としては、GUIの表示、メニュー・ツールバーの操作、国際化対応、ユーザ設定保存などを盛り込んでいます。
そのため、従来のHello Worldプログラムとは比べ物にならない複雑なプログラムになっていましたがご容赦の程。
Java 2 Standard Edition, v.1.4以上
リリース種類 | ファイル名 | 日付 | 備考 |
---|---|---|---|
バイナリ・リリース |
kanday-1.0.2.zip |
2004.7.4 | 暫定リリース |
ソース・リリース |
kanday-1.0.2-src.zip |
2004.7.4 | 暫定リリース |
まだリリースに必要な体裁(ライセンスファイル、README、その他ドキュメント)が含まれていません。
バイナリ・リリースを解凍し、中のkanday-1.0.1.jarを実行します。
C:\work\kanday-1.0.2> java -jar kanday-1.0.2.jar :
ソース・リリースを解凍・展開します。
ビルドには、ApacheのMavenを使用します。
リリース バージョン |
日付 | 備考 |
---|---|---|
1.0.2 | 2004.7.4 | KandayAboutDialogの文字列センタリング バージョン情報にJARファイルのMANIFEST.MFを使用 |
1.0.1 | 2004.6.13 | |
1.0 | 2004.6.7 |
C:\work\kanday-1.0.2> maven jar :
targetディレクトリの中に、kanday-1.0.1.jarが生成されます。
ソフトウェア開発をいくつか経験してきて感じたことは、ソフトウェアに開発コードをつけることの重要性です。そこで、Hello Again Desktop編のプログラムを”Kanday"[*1]とコード名を付けます。
要求定義と分析については、箇条書きのリスト(次項)で管理することにします。要求は番号を付けて管理します。
設計については、今回はプログラム構造を把握するためのコミュニケーション用途としてUMLの図を一部使用します。ツールは、Judeを使用します。ただし、主にプログラム構造を把握する目的としているため、クラス図のみを作成します。クラスの詳細(メソッドやフィールド)は構造の把握しやすさを阻害するので省略した形を載せています。
ソースコードおよびビルドについては、Apacheプロジェクトのツール"Maven"を使用します。ディレクトリ構成は、Mavenの標準ディレクトリ構成に従います。Mavenの利用により、ビルドの設定で記述した量を数行程度にすますことができました。ビルド設定は、以外と大変な作業なので、この効果は大きいと言えます。
バージョン管理は、初回リリース以降のリリース管理に適用します。リリースに付与するバージョン番号の取り方にはいろいろな流儀があります。本プロジェクトでは、SunのJARパッケージ・バージョン仕様に従います。このバージョン仕様では、3つの数値でバージョンを表現します。
バージョン番号: X.Y.Z X:メジャー番号 Y:マイナー番号 Z:マイクロ番号 |
JARパッケージ・バージョン仕様については、本サイトのパッケージ・バージョンのページで紹介しています。簡単に述べると、メジャー番号は、機能変更などの大きな変更(仕様が変更された)時に増やし、マイナー番号は互換性を保持した小さな変更(機能追加)時に増やし、マイクロ番号はバグ修正等の内部的な変更時に増やします。
テストについては非適用とします。
HelloAgain Desktop編における要求定義を記述します。
受注ソフトウェア開発では、ソフトウェア発注者側の要求(願望と言った方が近い)をもとにして曖昧な点を解消したり矛盾する要求の整合性を取ったりして、まず開発するソフトウェアの仕様を定義します。この種の作業を要求定義とか要件定義とか仕様化と表現します。
パッケージ製品等のソフトウェア開発では、製品仕様を決める作業に相当します。
要求定義にあたり、実現範囲を具体的に特定します。
日本語ロケールで実行した場合の画面例です。メニューバー、ツールバー、およびメッセージを表示する領域から構成されます。
英語ロケールで実行した場合の画面例です。
Javaの標準GUIライブラリであるSwing(Java Look and Feel)を用いたときの画面設計については、ガイドラインがSunから提示されています。本プログラムもこのガイドラインになるべく準拠するようにしています。
現在のプログラムにおいて、要求定義事項の達成状況を記述します。
要求 定義 No. |
要求定義名 | 動作確認OS | |
---|---|---|---|
Windows 2000 |
Linux | ||
1 | コマンドラインから起動 | ∨ | ∨ |
2 | OSメニューから起動 | ||
3 | デスクトップから起動 | ||
4 | ファイル管理ツールから起動 | ∨ | |
5 | 起動時キャプション画面表示 | ∨ | ∨ |
6 | アプリケーション画面表示 | ∨ | ∨ |
7 | 前回のアプリケーション画面位置再現 | ∨ | ∨ |
8 | メニュー/ツールバー、メッセージ表示 | ∨ | ∨ |
9 | メニュー文字列のロケール対応 | ∨ | |
10 | ツールチップのロケール対応 | ∨ | |
11 | メッセージのロケール対応 | ∨ | |
12 | メッセージの前景色/背景色変更 | ∨ | |
13 | プログラム情報画面の表示 | ∨ | ∨ |
14 | 終了メニューからプログラム終了 | ∨ | ∨ |
15 | ショートカットキーでプログラム終了 | ∨ | ∨ |
要求定義には項目化されていないものの、仕様として問題と考えられるものについてリストアップし対処します。
問題 No. |
問題内容 | 原因 | 対処方針 | 状況 |
---|---|---|---|---|
1 | 前景色・背景色変更ダイアログがロケール対応していない | 要求定義漏れ | 現状でリリース。今後ロケール対応する。 | 1.0.1 |
2 | Aboutダイアログの文字列が左寄りになっている。中央表示が美しい。 | 品質向上 | 次のリリース時に反映する | 1.0.2 |
3 | 何回も起動すると複数起動される | 仕様未確定 | 仕様上二重起動を許すか否か決定し、決定によって必要が生じたら修正する。 | 未 |
4 | ソースコードがバージョン管理されていない | 管理上の問題 | 速やかにバージョン管理を実施する | 未 |
意見
要求定義時点でプログラムの振る舞いについて詳細に規定しておくことが重要です。しかし、往々にして要求定義では方針レベルの曖昧な項目だけが挙げられ、詳細は設計以降に先送りされてしまいます。この結果、開発工程の進捗に伴い問題(疑問)が次から次へと挙げられてきます。これを問題が挙がった順に個別に対処を決めてしまうことにより、全体として仕様項目間での不整合や一貫性の欠如が発生してしまいます。後で挙がった問題の対処のために、前に挙がった問題の対処を変更しなくてはならないこともしばしばです。これによって、仕様がころころ変わるという事態に陥ります。顧客が仕様を変えている訳ではないのですけれど。。。
要件を分析して分析モデルを作成するのが一般的な開発のアプローチになると思われますが、今回のように規模が小さなプログラムでは要件から一気に設計を行ってしまいます。
意見
規模の大きなソフトウェアになると、分析からして大変な作業となり設計なんて複雑怪奇な代物になります。これは、ソフトウェアの分割統治がうまくないことの表われです。大きなソフトウェアも、分割すれば小さなプログラムの集合として構成することができます。小さなプログラムであれば、分析も設計もかなり容易な作業となります。
まず、画面構成からぱっぱと図を作成しました。クラス設計のアプローチはいろいろありますが、ここでは大まかなクラス構成を挙げた程度のクラス図を出発点とします。
クラス構成を最初に考えておくと、クラスに単一の責務を割り当てることが容易になります。開発途中で機能を増やすことになった場合、その機能を持つに適した責務を持つクラスがあれば、そのクラスにメソッドとして機能を追加します。もし適する責務がなければ、新たな責務としてクラスを新規に追加します。
クラス構成のないままプログラムのコーディングに入ると、「木を見て森を見ず」の状態に陥ってしまい、適切なクラス/メソッド分割ができず、クラスやメソッドが肥大化してしまいがちです。
※ このUMLの図は、Jude 竹1.3バージョンを使用しています。
GUIは、Swingを使用します。開発対象クラスは色つきで示しています。継承等でクラス図に記述する必要のあるJavaの標準クラスは色なしとしています。メソッドやシーケンス等は実装時に決めながら作成します。
(パッケージの中の設計まで律儀にメソッド・シーケンスを作成してからコーディングに入る人はえらいと思うが真似できない。コードで考えないとぐちゃぐちゃになってしまう。)
mainメソッドは独立したクラスKandayMainに持たせます。今回は、プログラム起動時のスプラッシュ画面表示(KandaySplashクラス)の制御、KandayPreferecesの生成、KandayFrameの生成を行っています。
GUIの詳細は、Creatorパターン(GRASPパターンの1つ)に従い全てKandayFrameクラスに生成・管理させています。表示に必要な情報は、KandayPreferencesに一括して持たせています。GUI側はこのKandayPreferencesから情報を取り出します。ただし、GUIの末端の部品まですべてKandayPreferencesを共有すると部品の結合度が高くなり、部品の独立性が損なわれてしまうので、なるべく上位のクラスだけにKandayPreferencesクラスへの依存性を限定するよう留意しています。
GUIの各部品は、Swingの部品を継承する形で設計しています。継承によるプログラミングは差分プログラミングが適用できるので、コードを書く手間が少なくなります。しかし、継承は親クラスと子クラスの間の結合度が極めて高い、つまり部品への依存性が強大なものとなります。その結果、親クラス(部品)の変更に弱い設計となります。Swingは十分枯れてきたAPIなので今後の変更は少ないと判断し、今回は継承を中心とした設計でよしとしています。通常は継承を避けてコンポジションを使用するようにします。継承とコンポジションの使い分けについては、書籍「UMLによるJavaオブジェクト設計」(Peter Coad著)がよい参考になります。
ロケールの切替については、リソースバンドルAPIを使用します。リソースバンドルの利用はすべてKandayPreferencesクラスで隠蔽し、他のクラスではこのAPIを使用しません。また、値の保存については、Preferences APIを使用し、それもKandayPreferencesに隠蔽します。表示に必要なデータを保持しているという点では、モデル・ビューの関係を設計に取り入れていると考えることも出来ます。ただし、モデル側となるKandayPreferencesクラスはデータの意味に基づくモデリングを行わず、単一クラスに各種の値を持たせています。
プログラムの挙動を把握するために、ロギングを使用します。ロギングには、Java
2 Standard Edition ver.1.4から導入されたLogging
APIを使用します。ロギングAPIの使用にあたっての共通指針を決めておく必要があります。指針として決めるべき内容としては、ロギングの制御単位(パッケージまでにするかクラス単位までにするか)、ロギングのレベルの規定(プログラム内でログのレベルを共通にする)、メッセージ内容、などです。
今回は、ロギングの制御単位はクラス単位とし、Loggerインスタンスは各クラスでクラス変数として保持することにします。レベルについては、メソッドの実行状況をトレースする際はFINER、設定情報の取り込み結果などはCONFIG、エラーについては、プログラム実行を継続できるものはWARNING、継続できないものはSEVERE、プログラム使用者に知らせたい情報はINFO、細かなデバッグ情報はFINESTを使用します。
helloagain.kanday
今回は、クラス数が10個以内におさまりそうなのと、階層的にも1つしかないので、一つのパッケージとします。パッケージ名については、DNSドメイン名に基づく正規化された命名を使用することが推奨されています。しかし今回は再利用を目的としないアプリケーションの開発ですので、簡略化した命名を採用しています。
設計で抽出したクラスに対応するソースコードを作成します。
現在、順次実装中。
クラス名 | ファイル名 | 備考 |
---|---|---|
KandayMain |
KandayMain.java |
mainメソッド提供。起動時のスプラッシュ画面表示と続くアプリケーション画面の表示を行う。 |
KandayPreferences |
KandayPreferences.java |
リソースバンドルおよびPreferences APIを使用してプログラムの各種パラメータを定義・保持する。 |
KandaySplash |
KandaySplash.java |
プログラム起動時のスプラッシュ画面。 |
KandayFrame |
KandayFrame.java |
プログラムのウィンドウ(枠) |
KandayMenuBar |
KandayMenuBar.java |
ウィンドウのメニューバー |
KandayToolBar |
KandayToolBar.java |
ウィンドウのツールバー |
KandayPane |
KandayPane.java |
ウィンドウの主コンテンツでメッセージを表示する。文字列はコンテンツの中央に表示されるよう位置を調整する。 |
KandayAboutDialog |
KandayAboutDialog.java |
Help→About で表示される、製品名・バージョン等を表示するダイアログ |
KandayPreferencesDialog |
KandayPreferencesDialog.java |
File→Preferencesで表示される、前景色・背景色変更設定ダイアログ |
プログラムを実行するにあたり、いく種類かの設定ファイルを使用します。ハードコーディングは「悪い習慣」なので、最初のサンプルから「よい習慣」である設定情報の外部化を扱うことは重要です。今回は、アプリケーションの設定とロギング設定の2つの設定ファイルを使用します。
また、プログラム終了時にウィンドウの位置・大きさを保存し、次回起動時にはこの保存した値を優先して設定として使用します。この保存に際して、Java以外のプログラム開発では、ファイルを使って値を保存することが多いのですが、ファイルI/Oを使用してしまうとJavaとはいえプラットフォームに依存しがちなコーディングに陥りがちです。そこで、Javaのプラットフォーム非依存性を維持するために、Java Preferences APIを使用します。
ウィンドウ表示メッセージ、ウィンドウ初期位置・サイズなどを設定ファイルから読み取って使用します。この設定ファイルを実現する方法として、プロパティ・リソースバンドルを使用します。リソースバンドルは、主に国際化対応プログラムにおいて使用されます。プログラムにおいて使用する文字列を、ロケール(言語)ごとに定義しておいて、実行した時のロケールから対応する文字列を選ぶ仕組みです。
今回は、デフォルトのロケールにおける設定と、jaロケールにおける設定の2つを用意します。
ファイル名 | 実行ロケール | 備考 |
---|---|---|
KandayResource.properties |
デフォルト | |
KandayResource_ja.properties |
ja |
KandayResource.propertiesには、ウィンドウのタイトルバーに表示するメッセージとして、リソースバンドル用プロパティファイルに以下のように指定しています。書式は、「キー = 値」 です。キーの文字列には、しばしば'.'(ピリオド)で区切った階層的な命名を使用します。これは人間の目から見てデータの分類が分かりやすくなるためのものです。プロパティの仕組みとしては、階層はなく'.'も単なる文字の一つにしか過ぎません。
frame.title = HelloAgain Desktop Program frame.width = 640 |
KandayResource_ja.propertiesには以下のように日本語メッセージ(ユニコード・エスケープ)を記述しています。
frame.title = \u3053\u3093\u306b\u3061\u306f\u3001\u3082\u3046\u4e00\u5ea6 |
直接ユニコードエスケープを入力するのは困難なので、別途Shift JIS等のエンコーディングで記述しておきます。例えば、SJISコードで記述したファイルを作成します。
frame.title = こんにちは、もう一度 |
このファイルを、JDKに付属のツールnative2asciiコマンドでユニコード・エスケープに変換します。
C:\work> native2ascii KandayResource_ja.properties.sjis > KandayResource_ja.properties
プログラム中でリソースバンドルから値を取り出すには、リソースバンドルのAPIを使用して以下のように記述します。
ResourceBundle resource = ResourceBundle.getBundle("helloagain.kanday.KandayResource"); String param = resource.getString("frame.title"); |
プログラムをjaロケールで実行すれば、日本語のタイトルが、それ以外のロケールで実行すれば、英語のタイトルがウィンドウのタイトルバーに表示されます。ロケールの切替はUNIX系では環境変数LANGで簡単にできますが、Windows 2000の場合はコントロールパネルの地域の設定で変更しなくてはならず、システム全体が変わってしまいます。そこで、日本語環境のWindowsOS上で英語ロケールを実行するために、java実行時にJavaVMオプションで-Duser.language=en のように指定して変更します。
パッケージ名を'helloagain.kanday'と指定しているので、プロパティファイルはクラスパス上でhelloagain.kandayパッケージに属するクラスが置かれているのと同じ場所に置かなくてはなりません。
ファイルに各種パラメータ値を記述することによって、プログラムを修正しなくても設定をカスタマイズすることが可能になります。本格的なプログラムではほぼ必ずといっていいほどパラメータファイルが使用されています。しかし、その実現方法については複数あります。
プログラム実行中に発生したデータを次回プログラム実行時に反映させるために、何らかの場所にデータを保存する仕組み(いわゆる永続化)として使用します。本プログラムでは、ウィンドウの位置・大きさ、色(未実装)を保持するのに使用します。以下に、Preferences APIを使って4つの属性をロードおよびセーブするメソッド例を示します。
この例では、OSにログインするユーザ個別に設定を行えるようにユーザ・プリファレンスに値を格納して読み出しています。一方、ユーザ個別ではなくそのマシンで共通の設定を保存する場合は、userNodeForPackageメソッドではなくsystemNodeForPackageメソッドを使用します。
プリファレンスから設定値を読み出す例
public void load() { Preferences prefs = Preferences.userNodeForPackage(getClass()); frameWidth = prefs.getInt(KEY_FRAME_WIDTH, frameWidth); frameHeight = prefs.getInt(KEY_FRAME_HEIGHT, frameHeight); framePositionX = prefs.getInt(KEY_FRAME_POSITION_X, framePositionX); framePositionY = prefs.getInt(KEY_FRAME_POSITION_Y, framePositionY); } |
プリファレンスへ設定を保存する例
public void save() { Preferences prefs = Preferences.userNodeForPackage(getClass()); prefs.putInt(KEY_FRAME_WIDTH, frameWidth); prefs.putInt(KEY_FRAME_HEIGHT, frameHeight); prefs.putInt(KEY_FRAME_POSITION_X, framePositionX); prefs.putInt(KEY_FRAME_POSITION_Y, framePositionY); } |
Windows OSの場合はプリファレンスへ保存すると、レジストリに設定が書き込まれます。
HKEY_CURRENT_USER | +- Software | +- JavaSoft | +- Prefs | +- helloagain | +- kanday |
(標準) REG_SZ (値の設定なし) frame.height REG_SZ 232 frame.position_x REG_SZ 164 frame.position_y REG_SZ 394 frame.width REG_SZ 408 message.background.color REG_SZ -986896 message.foreground.color REG_SZ -16777216 |
Linuxの場合はプリファレンスへ保存すると、ユーザのホームディレクトリ下の
.java/.userPrefs/helloagain/kanday/prefs.xml
に設定が書き込まれます。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE map SYSTEM "http://java.sun.com/dtd/preferences.dtd"> <map MAP_XML_VERSION="1.0"> <entry key="frame.height" value="480"/> <entry key="frame.position_x" value="352"/> <entry key="frame.position_y" value="284"/> <entry key="frame.width" value="640"/> <entry key="message.background.color" value="-16737997"/> <entry key="message.foreground.color" value="-52"/> </map> |
JARファイルの情報にアクセスするためのAPIを用いて、"Implementation-Version"を取得します。Kandayプログラムでは、プログラム情報ダイアログの表示に、プロパティファイルに記述したバージョン識別文字列と、JARファイルのMANIFEST.MFのImplementation-Versionに記載されたバージョン識別子の両方を表示します。
aboutVersion = readString(KEY_ABOUT_VERSION, "Version: Souchong") + "(" + getClass().getPackage().getImplementationVersion() +")"; |
SwingのJDialogクラスを継承しています。
日本語ロケールでのダイアログ
英語ロケールでのダイアログ
JDialogを継承して独自のダイアログを作成する方法は、JFrameを継承して独自のウィンドウを作成するのとほぼ同一です。貼り付ける部品は、JDialogのgetContentPaneメソッドで取得したContainerに対してaddメソッドで追加します。
色を表示している部品はJTextFieldを使用しています。backgroundで色を指定しています。
Swingの場合、いわゆるダイアログのような部品レイアウトにぴったりフィットするレイアウトマネージャがデフォルトで提供されていないのが問題です。そして多くの場合、GridBagLayoutを駆使して望むレイアウトに近づける方法を採ります。ところがGridBagLayoutはかなり難解で、試行錯誤をすることになります。
そのようなときはどうすればよいのでしょうか?
一つには、NetBeansやJBuilderといったGUIデザイン機能をもった統合開発環境を使用する方法があります。GUIデザイン画面上で部品を望む位置にぺたぺたと貼り、一通り終わった段階でGridBagLayoutに変換します。すると、コードを書かなくてもGridBagLayoutを用いて望むレイアウトを実現してくれるプログラムが一丁上がりと出来ています。
今回は、NetBeansもJBuilderも使用しないし、特定のツールによって自動生成されたコードがあるのも気に入らないし、ということで、J2SE 1.4から新たに加わったレイアウトマネージャjavax.swing.SpringLayoutを使用しています。SpringLayoutは、指定は多少面倒ですが、細かな位置指定ができるので、GridBagLayoutより使いやすいのではないかと思います。もっとも座標指定がかなり面倒なので、ツールによって生成するのが本筋でしょう。
上記のように、水平方向の位置を指定する場合のコード例は、以下のようになります。
(1)は、foregroundLabel(前景色:)の左端(WEST)と、contentPaneの左端(WEST)との間隔を10pixelに設定しているコードです。部品の上下左右は、SpringLayoutの定数 NORTH, SOUTH, WEST, EASTで表現します。
(1) (2) (3) (4) (5) (6) (7) (8) |
SpringLayout layout = new SpringLayout(); Container contentPane = getContentPane(); : layout.putConstraint(SpringLayout.WEST, foregroundLabel, 10, SpringLayout.WEST, contentPane); layout.putConstraint(SpringLayout.WEST, foregroundField, 10, SpringLayout.EAST, foregroundLabel); layout.putConstraint(SpringLayout.WEST, foregroundChangeButton, 10, SpringLayout.EAST, foregroundField); layout.putConstraint(SpringLayout.WEST, backgroundLabel, 10, SpringLayout.WEST, contentPane); layout.putConstraint(SpringLayout.WEST, backgroundField, 10, SpringLayout.EAST, foregroundLabel); layout.putConstraint(SpringLayout.WEST, backgroundChangeButton, 10, SpringLayout.EAST, foregroundField); layout.putConstraint(SpringLayout.WEST, applyButton, 10, SpringLayout.EAST, backgroundLabel); layout.putConstraint(SpringLayout.WEST, cancelButton, 25, SpringLayout.EAST, applyButton); |
上記のように、垂直方向の位置を指定する場合のコード例は、以下のようになります。
(1) (2) (3) (4) (5) (6) (7) (8) |
layout.putConstraint(SpringLayout.NORTH, foregroundLabel, 25, SpringLayout.NORTH, contentPane); layout.putConstraint(SpringLayout.NORTH, foregroundField, 25, SpringLayout.NORTH, contentPane); layout.putConstraint(SpringLayout.NORTH, foregroundChangeButton, 25, SpringLayout.NORTH, contentPane); layout.putConstraint(SpringLayout.NORTH, backgroundLabel, 20, SpringLayout.SOUTH, foregroundLabel); layout.putConstraint(SpringLayout.NORTH, backgroundField, 20, SpringLayout.SOUTH, foregroundLabel); layout.putConstraint(SpringLayout.NORTH, backgroundChangeButton, 20, SpringLayout.SOUTH, foregroundLabel); layout.putConstraint(SpringLayout.SOUTH, applyButton, -10, SpringLayout.SOUTH, contentPane); layout.putConstraint(SpringLayout.SOUTH, cancelButton, -10, SpringLayout.SOUTH, contentPane); |
(7)は、applyButton(適用)の下端(SOUTH)と、contentPaneの下端(SOUTH)との間隔を10pixelに設定しているコードです。指定している数値がマイナスの値になっています。これはcontentPaneの下端からの距離が正方向だと下側(つまりダイアログ画面の外)になるからです。
SpringLayoutは、contentPaneの大きさに対して内部の部品の位置を決めます。そのため、packメソッドでサイズを決めることができないので、SpringLayoutを使用した場合は、明示的にダイアログやフレームのサイズを指定します。
swingのJMenuBarを継承して作成します。今回作成したKandayプログラムでは、以下のメニュー構成です。
![]() |
![]() |
![]() |
![]() |
メニューが選択されたときのアクションを定義する2つの方法を、プログラム終了メニューを題材に紹介します。いずれも、以下の設定を行います。
日本語ロケールでは、ニーモニックとして指定したアルファベット(例えば'X')がメニュー文字列(例えば”終了”)に含まれません。ニモニックにアンダーラインが引かれないので、日本語ロケールにおける文字列では、”終了(X)”のようにニモニックに指定したアルファベットを括弧付きで付加して補足しています。
ActionListenerを実装するクラス(内部クラスや匿名クラスでも可)を定義して、JMenuItemのaddActionListenerで登録します。また、ショートカットキー、ニモニックキー、アイコンなどは、JMenuItemの属性として設定できるので、設定メソッドを個々呼んでいきます。
JMenuItem exitItem = new JMenuItem("Exit"); exitItem.addActionListener(new ExitAction()); exitItem.setAccelerator(KeyStroke.getKeyStroke( KeyEvent.VK_Q, ActionEvent.CTRL_MASK) ); exitItem.setMnemonic('X'); exitItem.setIcon(new ImageIcon("stop16.gif")); : class ExitAction implements ActionListener { public void actionPerformed(ActionEvent ev) { System.exit(0) } } |
1.の方法の欠点は、後にツールバーのプログラム終了ボタンに対しても同じような処理を記述しなくてはならない点にあります。コードの冗長化を発生させ、結果として修正漏れのバグを呼び込む原因になります。例えば、アイコン画像を変更する際に、ツールバーのボタンだけ変更してメニューの方を忘れる等。
メニュー項目を選択したときに実行させたい処理(アクション)と、ツールバーのアイコンボタンによる処理は同じアクションになります。そこで、アクションを一つのクラスとして定義し、そのアクションをメニューとツールバーに加えるという方法を使うのがよいプログラミングと言えます。
JMenu fileMenu = new JMenu(File); fileMenu.add(new ExitAction("Exit", new ImageIcon("stop16.gif")); : class ExitAction extends AbstractAction { ExitAction(String aText) { init(aText, null); } ExitAction(String aText, Icon anIcon) { init(aText, anIcon); } final void init(String aText, Icon anIcon) { putValue(NAME, aText); putValue(SMALL_ICON, anIcon); putValue(MNEMONIC_KEY, new Integer(KeyEvent.VK_X)); putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke( KeyEvent.VK_Q, ActionEvent.CTRL_MASK) ); putValue(SHORT_DESCRIPTION, "Exit this program"); } public void actionPerformed(ActionEvent ev) { System.exit(0); } } |
AbstractActionは、内部にキーと値のペアで属性を管理する機構を持っています。そこにメニューやツールバーで使用する設定は、通常のクラスの属性ではなくこの汎用的な属性管理機構を用います。変更の多発する画面系の設計においては有効なのかもしれません。
キー | 値 | 用途 |
---|---|---|
NAME | String | メニューに表示される文字列 |
SMALL_ICON | Icon | メニューやツールバーに表示されるアイコン画像 |
MNEMONIC_KEY | Integer | ニモニックキー(メニュー選択時にマウスの代わりにキー入力で選択する仕組み) |
ACCELERATOR_KEY | KeyStroke | ショートカットキー |
SHORT_DESCRIPTION | String | ツールチップで表示される文字列 |
LONG_DESCRIPTION | String | コンテクスト・ヘルプ等で使用を想定する長い説明 |
ツールバーは、主要なメニュー機能を一操作で簡単に実行できるようグラフィカルなアイコンボタンを並べたものです。
メニューやツールバー等で使用可能な本格的なアイコン画像が、SunのJava look and feel Graphics Repositoryで提供されています。今回は、このリポジトリの中にあるアイコンを使用します。
アイコン画像 | 用途 |
---|---|
![]() |
プログラムの終了 |
![]() |
プログラム情報の表示 |
![]() |
プログラム設定 |
先のメニューバーの作成において、Actionクラスによってメニューへの応答コードを実装しているので、ここでは単にKandayToolBarに表示順にActionオブジェクトをaddするだけの簡単な実装となります。
アプリケーションの主コンテンツとなる、挨拶メッセージを表示する領域です。メッセージ文字列を、表示領域の中央にグラフィックスとして描画します。ウィンドウの大きさ、および表示するメッセージ文字列が可変であるため、表示位置は実行時に計算しています。
文字列を単に表示するだけであれば、JLabelクラスを使うのが簡単です。しかし、ここではグラフィックス描画の基本を知っておくことを目的にしています。
Swingでは、グラフィックスを描画するための専用部品は提供されていません。しかし、Swing各部品の基底クラスとなっているJComponentを直接継承して簡単にグラフィックスを描画するクラスを定義することができます。
JComponentクラスを継承し、paintComponentメソッドをオーバーライドしてグラフィックス描画処理を記述します。paintメソッドではない点に注意します。
public final void paintComponent(final Graphics graphics) { int x = (getWidth() / 2) - (messageWidth / 2); int y = (getHeight() / 2); graphics.setFont(font); graphics.drawString(message, x, y); } |
文字列をウィンドウの中央に表示するためには、ウィンドウの大きさと文字列の大きさの両方から算出します。
ウィンドウの大きさは、JComponentクラスのgetWidth、getHeightメソッドで取得できます。しかし、これらのメソッドをウィンドウがまだ表示されていないときに呼び出すと、ウィンドウの大きさが確定していないため0を返します。そこで、コンストラクタの中ではなく、paintComponentメソッド中で大きさを取得しています。
文字列を描画したときの大きさは、描画するフォントの種類と大きさによって異なります。FontMetricsクラスのstringWidthメソッドで算出します。
Font font = new Font("Serif", Font.PLAIN, 18); FontMetrics metrics = getFontMetrics(font); int messageWidth = metrics.stringWidth("こんにちは、Swing"); |
実際のコードでは、paintComponentメソッドで文字列の描画領域の大きさを毎回計算する必要がないので、メッセージ文字列が確定した段階で計算しておきます。
SwingのJDialogクラスを継承し、画像、表示メッセージ、OKボタン等を貼っています。
画面のレイアウトには、BorderLayoutを使用しています。
画像ファイル (BorderLayout.NORTH) |
||
文字列 (BorderLayout.CENTER) |
||
ボタン (BorderLayout.SOUTH) |
JLabelを使用するとき、文字列の表示位置をセンタリングする場合、コンストラクタの引数にSwingConstants.CENTERを追加します。または、setHorizontalAlignmentメソッドに引数SwingConstants.CENTERを指定します。
今回は、swingのJDialogクラスを継承して自前でダイアログのクラスを作成しました。しかし、この程度のダイアログであれば、swing標準のダイアログで作成した方が簡単だったかもしれません。Swingでは、HTMLによる表示制御が多少できるため、この程度のダイアログならばHTMLで記述した方が楽だったかもしれません。
URL img = getClass().getResource("splash.png"); String message = "<html><img src=\"" + img + "\"><br><center>" + "Desktop example program by Hello again project.<br>" + "(C) TAKAHASHI,Toru 2004<br>Version: Souchong</center></html>"; JOptionPane.showMessageDialog(kandayFrame, message); |
SwingのJFrameを継承しています。JFrameは、トップレベルのウィンドウとなるクラスです。
初期のSwingのサンプルコードでは、GUI部品に持たせる処理(アクション)を匿名クラスなどで生成して対応付けしていました。しかし、メニュー項目とツールバーのボタンと双方から同じアクションを呼び出します。そこで、複数のGUI部品と対応付けられるアクションは、クラスとして定義します。今回は、KandayFrameクラスの内部クラスとしてアクションを定義します。
ウィンドウを閉じたとき(Windowsなら右上の[X]ボタンを押した場合)の処理を選択することができます。JFrameのsetDefaultCloseOperationメソッドで指定します。
引数 | 振る舞い |
---|---|
JFrame.DO_NOTHING_ON_CLOSE | 何もしない |
JFrame.HIDE_ON_CLOSE | ウィンドウを非表示にする |
JFrame.DISPOSE_ON_CLOSE | ウィンドウを破棄する |
JFrame.EXIT_ON_CLOSE |
JavaVMを終了される |
以下のSplashウィンドウをプログラム起動時に表示します。
ウィンドウの枠もなく、操作もできないスプラッシュ画面は、JWindowを継承して作成します。
ウィンドウの枠を黒の線で囲っています。
contentPane.setBorder(BorderFactory.createLineBorder(Color.BLACK)); |
イメージファイルは、"splash.png"としており、クラスローダによってリソースとして読み込ませる方法を使用します。JARファイルにアーカイブしたときに、一緒にイメージファイルを含めるためです。パスは、本クラスと同じパッケージ(helloagain.kanday)に対応する場所です。
InputStream inStream = getClass().getResourceAsStream("splash.png"); if (inStream != null) try { BufferedImage image = ImageIO.read(inStream); Icon icon = new ImageIcon(image); } catch (IOException e) { // 画像は使用しない } } |
getResourceAsStreamに指定するファイル名は、絶対パスと相対パスの2つの記述ができます。上記例では相対パスを使用しています。
イメージファイル名を可変にしていない(ハードコーディングされている)のは、画像ファイルを入れ替える場合は、ファイル名を変更するのではなくイメージファイルそのものを差し替えるからです。ファイル名を可変にすることによる保守上のメリットがあまりないのです。
プログラムのエントリポイントであるmainメソッドを持つクラスです。このクラスは、インスタンス化せずに使用します。以下の機能を果たします。
単純に文字列をif文で比較しています。注意点は、文字列の比較には、== ではなくequalsメソッドを使うことです。
駄目な例 |
if ("-nologo" == args[i]) { isSplash = false; } else if ("-notoolbar" == args[i]) { // no implement } |
---|---|
良い例 |
if ("-nologo".equals(args[i])) { isSplash = false; } else if ("-notoolbar".equals(args[i])) { // no implement } |
スプラッシュ画面を一定時間だけ表示してからプログラムのウィンドウを表示します。これは単に製品アピールの意味だけでなく、プログラムの初期化時間を隠蔽する意味があります。プログラムの起動操作を実行してから何かしらのリアクションが出るまでに何秒もかかってしまうと、問題となります。例えば何度も起動操作をしてしまったりとかが発生し得ます。そこで、プログラムの起動操作後直ちに表示を行うという目的でスプラッシュ画面を表示します。
OSネイティブなアプリケーションでは、この目的のためにマウスカーソルを変更する方法を取っています。Javaの場合、残念ながらOSに依存する処理を行う場合はネイティブコードを作成してJNI経由で呼び出す必要があります。
プログラムを終了する時点でのウィンドウの位置と大きさを保存する必要があります。プログラムを終了する個所が確定できるなら、そこに保存メソッドを呼び出すコードを入れればよいのですが、あちこちに散在しているため、シャットダウンフックを使用します。
Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { savePreferences(pref, frame); } }); |
同じプログラムを複数実行できないようにすることが必要な場合、JavaではソケットのAPIを使用してポート番号を指定してopenする方法を取ります。他のアプリケーションで使用されていないポート番号を1つ選ぶ必要があります。ソケットAPIでは、同じホスト上で複数のプログラムが同じポート番号を使用することはできないので、多重起動防止策に利用することができます。
ポート番号以外にも、OS上のユニークなリソースであれば多重起動防止策として利用できると思います。ただし、ファイルの場合、プログラムが異常終了したときの対処がやや面倒なため、あまりお勧めできません。
ただし、異なるユーザが同じホスト上にログインしていて同じプログラムを実行する場合、これもポート番号が重なってしまい、多重起動防止機構に引っ掛ってしまいます。これを防止するには、さらにユーザ毎に固有のポート番号を使う必要があるかもしれません。
今回のプログラムでは、多重起動防止策は組み込んでいません。
各クラスにおいて、クラス変数としてLoggerを保持します。getLoggerの引数には、クラス名をFQCN(パッケージ名付き)で指定します。コーディングミスを防ぐために、文字列をベタにコーディングせずにClassクラスのgetNameメソッドで文字列を取得しています。また、クラス名の文字列は、ロギング用メソッドにおいて引数に指定することが何度もあるため、あらかじめクラス変数に置いています。
public class KandayMain { : private static final String MYCLASS = KandayMain.class.getName(); private static Logger logger = Logger.getLogger(myClass); } |
private static void showSplash() { logger.entering(MYCLASS, "showSplash");
logger.exiting(MYCLASS, "showSplash"); }
意見 このように、メソッドの出入りにログを吐くように記述するのは単調で面倒な作業です。機械的にできればいいのですが、その方法としてアスペクト指向プログラミングを適用することができます。Javaプログラミングで利用できるアスペクト指向ツールとしては、AspectJがあります。これを使えば、ロギングのコーディングが大幅に省略できるようになります。 |
プログラムの実行中の状況をロギングします。ロギングの設定は、デフォルトでは<Java Runtime Environmentディレクトリ>/lib/logging.properteisの内容が適用されます。このファイルを直接変更してもよいのですが、そのJREで実行される全てのプログラムに変更内容が波及します。そこで、プログラム毎に独自のロギング用プロパティファイルを作成し、実行時にシステムプロパティ "java.util.logging.config.file"で指定するようにします。
ちなみにデフォルトのlogging.propertiesの内容が適用された場合、ロギングは次の状態です。
ログのレベル設定は、開発のどの工程で使用するかによって大きく異なります。
handlers = java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.level = ALL java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter helloagain.kanday.level = ALL |
Java言語では、ソースコードに記述したコメントからドキュメントを自動生成することが標準として決められています。
ソースディレクトリの各パッケージのディレクトリに、package.htmlというファイル名でパッケージコメントを記述しておくと、Javadocによってパッケージの説明としてパッケージコメントが抜き出されます。
KandayPreferencesクラスの肥大化があるように思われます。項目が増えてくると、フィールドやアクセッサの定義がどんどん増えてしまうので、もっと汎用的な仕組み(HashMap等)で対応した方がよかったかもしれません。
以下のツールを使用しています。
Mavenツールを使ってプロジェクトディレクトリを生成します。
work$ mkdir kanday work$ cd kanday kanday$ maven genapp __ __ | \/ |__ _Apache__ ___ | |\/| / _` \ V / -_) ' \ ~ intelligent projects ~ |_| |_\__,_|\_/\___|_||_| v. 1.0-rc2 Enter a project template to use: [default] Please specify an id for your application: [app] kanday Please specify a name for your application: [Example Application] HelloAgain Desktop Program Please specify the package for your application: [default.example.app] helloagain.kanday build:start: genapp: [copy] Copying 1 file to E:\work\helloagain\kanday\src\java\helloagain\kanday [copy] Copying 3 files to E:\work\helloagain\kanday\src\test\helloagain\kanday [copy] Copying 1 file to E:\work\helloagain\kanday [copy] Copying 2 files to E:\work\helloagain\kanday BUILD SUCCESSFUL Total time: 8 minutes 46 seconds Finished at: Wed May 26 11:38:04 JST 2004 kanday$ |
← ここは[Enter]キーのみ入力 ← IDにはコード名を入力 ← プログラム名を入力 ← パッケージ名を入力 |
ソースファイルは、<プロジェクトルート>/src/java/helloagain/kandayディレクトリの中に置きます。
ビルドには、Mavenを使用しています。
ファイル名 | 備考 |
---|---|
project.xml |
|
project.properties |
|
maven.xml |
ゴールの定義 |
mavenのjava:compileターゲットを実行すると、ソースファイルをコンパイルします。
kanday$ maven genapp __ __ | \/ |__ _Apache__ ___ | |\/| / _` \ V / -_) ' \ ~ intelligent projects ~ |_| |_\__,_|\_/\___|_||_| v. 1.0-rc2 build:start: java:prepare-filesystem: [mkdir] Created dir: E:\work\helloagain\kanday\target\class es java:compile: [echo] Compiling to E:\work\helloagain\kanday/target/classe s [javac] Compiling 9 source files to E:\work\helloagain\kanday\target\classes BUILD SUCCESSFUL Total time: 4 seconds Finished at: Thu Jun 03 05:40:48 JST 2004 kanday$ |
target/classesディレクトリ以下に、コンパイルされたクラスファイルが生成されます。
リソースバンドルのプロパティファイルと、画像ファイルがリソースとなります。これらのファイルをsrcディレクトリの下からビルド時にtarget/classesディレクトリ以下にコピーする必要があります。
mavenのjava:jar-resourcesターゲットを実行すると、リソースファイルがコピーされます。ただし、デフォルトのプロジェクト設定では、src/conf直下の*.propertiesファイルだけが対象となっているので、サブディレクトリ以下の*.propertiesファイルがコピーされません。そこで、src/conf以下のサブディレクトリも含めたディレクトリにある*.propertiesファイルとsrc/data以下のサブディレクトリを含めたディレクトリにある画像ファイル(*.png、*.gif)をコピーするように修正します。
修正前 |
<resources> <resource> <directory>src/conf</directory> <includes> <include>*.properties</include> </includes> </resource> </resources> |
修正後 |
<resources> <resource> <directory>src/conf</directory> <includes> <include>**/*.properties</include> </includes> </resource> <resource> <directory>src/data</directory> <includes> <include>**/*.png</include> <include>**/*.gif</include> </includes> </resource> </resources> |
プロパティファイルとして有効な文字エンコーディングは、ISO 8859-1のみです。日本語等を使用する場合は、ユニコードエスケープ(\uXXXX)しなくてはなりません。しかしながら、このユニコードエスケープは人間が読み書きすることが困難です。そこで、人が読み書きできる文字エンコーディングで記述した後にJava 2 SDKのコマンドnative2asciiでユニコードエスケープ化します。
例えば、KandayResource_ja.properties ファイルの場合、まずShift JISエンコーディングで記述したファイル KandayResource_ja.properties.sjisを作成します。次に、native2asciiコマンドで、KandayResource_ja.properties.sjisを入力し、KandayResource_ja.propertiesを出力します。
kanday$ native2ascii src/conf/helloagain/kanday/KandayResource_ja.properties.sjis > src/conf/helloagain/kanday/KandayResource_ja.properties kanday$ |
せっかくMavenを使っているのに、このユニコードエスケープ処理を手作業で実行しないといけないというのは問題があります。手作業は必ず忘れてしまい、修正したプロパティ値が反映されない事態が生じます。そこで、Mavenの手順で自動的にこの処理が実行されるように設定を追加します。
まず、プロパティファイル等のリソースをsrcディレクトリ以下からtargetディレクトリへコピーしているゴールは、java:jar-resourcesです。このゴールに手順を追加します。ユニコードエスケープ処理はコピー前に実行しなくてはならないので、preGoalを使用します。このような修正は、プロジェクトディレクトリにmaven.xmlというファイルを作成して記述します。
<?xml version="1.0" encoding="Windows-31J"?> <project xmlns:j="jelly:core" xmlns:ant="jelly:ant"> <!-- プロパティファイルをコピーする前に、native2asciiを実行する --> <preGoal name="java:jar-resources"> <j:if test="${!pom.build.resources.isEmpty()}"> <j:forEach var="resource" items="${pom.build.resources}"> <j:set var="directory" value="${resource.directory}"/> <ant:echo message="directory=${directory}"/> <ant:native2ascii encoding="Windows-31J" src="${directory}" dest="${directory}" includes="**/*.properties.sjis" ext=""/> <ant:native2ascii encoding="EUC-JP" src="${directory}" dest="${directory}" includes="**/*.properties.euc" ext=""/> </j:forEach> </j:if> </preGoal> </project> |
preGoalは、Mavenのゴールが実行される場合、実行前に呼び出される手続きを定義します。この場合、java:jar-resourcesのゴールが起動されるときに、その内容を実行する前にpreGoalで定義した内容が先に実行されます。なお、ゴールが実行された後に呼び出したい手続きは、postGoalで定義可能です。
次にリソースが定義されているかをj:ifで判定しています。リソースが定義されていなければ何もせずpreGoalは終了します。このリソースは、project.xmlファイルにおいて project -> build -> resourcesで指定したものです。
リソースは、複数指定できるので、それぞれをj:forEachで毎挙しています。そして、各リソースのディレクトリに対してant:native2asciiを実行しています。ここでは、リソースのディレクトリ以下をパターンマッチング**/*.properties.sjisでマッチしたファイルに対してnative2asciiを実行しています。今回は、Linux環境も考慮して、sjisおよびeucの両拡張子での対応を行っています。
preGoalの設定を追加した後のjava:jar-resourcesを実行すると、native2asciiが実行されるようになります。
kanday$ maven java:jar-resources __ __ | \/ |__ _Apache__ ___ | |\/| / _` \ V / -_) ' \ ~ intelligent projects ~ |_| |_\__,_|\_/\___|_||_| v. 1.0-rc2 build:start: java:jar-resources: [echo] directory=E:\work\helloagain\kanday\src\conf [native2ascii] Converting 1 file from E:\work\helloagain\kanday\src\conf to E:\work\helloagain\kanday\src\conf [echo] directory=E:\work\helloagain\kanday\src\data Copying 2 files to E:\work\helloagain\kanday\target\classes Copying 3 files to E:\work\helloagain\kanday\target\classes BUILD SUCCESSFUL Total time: 2 seconds Finished at: Mon Jun 07 12:32:38 JST 2004 kanday$ |
Mavenのproject.propertiesファイルに、実行可能JARファイルにおいてMANIFEST.MFに記載するMain-classの名前を記述します。実行可能JARファイルは以下のように実行することができます。
C:\work\kanday> java -jar target/kanday-1.0.1.jar
また、エクスプローラ上からJARファイルをダブルクリックして実行(Windows OSの場合)することができます。
maven.jar.mainclass = helloagain.kanday.KandayMain |
Mavenのjarターゲット(省略形です。正式にはjar:jar)を実行します。
kkanday$ maven jar __ __ | \/ |__ _Apache__ ___ | |\/| / _` \ V / -_) ' \ ~ intelligent projects ~ |_| |_\__,_|\_/\___|_||_| v. 1.0-rc2 build:start: java:prepare-filesystem: java:compile: [echo] Compiling to E:\work\helloagain\kanday/target/classes java:jar-resources: test:prepare-filesystem: test:test-resources: test:compile: test:test: [junit] Running helloagain.kanday.AppTest [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0.016 sec [junit] Running helloagain.kanday.KandayPreferencesTest [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0.016 sec jar:jar: [jar] Building jar: E:\work\helloagain\kanday\target\kanday-1.0.jar BUILD SUCCESSFUL Total time: 3 seconds Finished at: Thu Jun 03 05:52:41 JST 2004 kanday$ |
Mavenのdistターゲット(省略形です。正式にはdist:build)を実行します。
TAKAHASHI, Toru