Sorry, English version is not available at this time.

Javaデスクトップアプリケーション

最終更新日:2008/6/7


変更履歴

2008/6/7 JJUG資料への言及を追加。


jsr295, jsr296で、Swingを用いたデスクトップアプリケーションの開発生産性の強化が予定されています。そこで、ここでは、その中の幾つかの機能を試してみたいと思います。

なお、「書きもの」トップページの方に、JJUGでこれらについて発表した内容があります。こちらには、操作を動画として録画したものと、NetBeans用のプロジェクトも置いてあるので、参考にしてください。

準備

Java SE 6を導入しておきます。http://java.sun.comのダウンロードページから入手できます。

Swingアプリケーションの開発と言えば、もちろんNetBeansです。今回は6.1betaを使用しました。NetBeans自体のインストールは簡単なので、ここでは詳細は省略します。

NetBeans自体にはjsr295, jsr296の参照実装が含まれており、JavaDocも添付されています。しかしまだまだドキュメントは整備されていないので、ソースを読まないと、細かなところは分かりません。ソースも入手しておくことをお勧めします。

jsr295https://beansbinding.dev.java.net/
jsr296https://appframework.dev.java.net/
Swing Workerhttps://swingworker.dev.java.net/

ソースをダウンロードしたら、NetBeansに登録しておきましょう。Tools => Librariesを選んで、Library Managerを表示します。Swing Application Frameworkを選んでから、Sourceタブをクリック。Add JAR/Folder...ボタンを押して、jsr296とSwing Workerのソースを展開した場所を指定します。

同様にjsr295のソースを、Beans Bindingに登録します。

これで、NetBeansの中からデバッガで追うことが可能になります。

サンプル1(テキストフィールドによる値編集)

最初に最も簡単な例として、テキストフィールドで値を編集する例を見てみましょう。

NetBeansを立ち上げ、File -> New Projectを選びます。

Nextボタンを押し、プロジェクトの名前を指定します。ここではPersonにしています。

あとは、デフォルトのままでFinishボタンを押します。すると自動的にアプリケーションの雛形が作成されます。

ProjectsビューのPersonを右クリックして、Propertiesを選んでプロジェクトのプロパティを開き、まずApplicationの項目を修正します。

ここは、ご自分の好きなように入力してみてください。Splash Screenを指定すると、アプリケーションが起動するまでの間に指定されたグラフィックを表示させることができます。

次にDesktop Appの項目を編集します。

ここも、好きなように入力して構いません。ただしLinuxで実行する場合、Look & FeelはSun Defaultのままだと、NullPointerExceptionが発生して立ち上がらなくなってしまうので、Java Defaultに変更しておきます。編集し終ったらOKボタンを押します。

試しにF6を押して、実行してみましょう。

FileメニューにはExitが、HelpメニューにはAboutが登録されています。試しにAboutをクリックすると、先ほど登録しておいた内容が表示されますね。しかし左側のグラフィックは何なのでしょう? ちょっとこれを変更してみましょうか。

今回は、これに差し替えてみることにします。

プロジェクトを作成したディレクトリの下の、src/person/resources/の下に、このファイル(about.jpg)を格納します。NetBeansのPrjectsビューの中で、Source Packages/person.resourceを展開すると、PersonAboutBox.propertiesがあるので、これをダブルクリックします。imageLabel.iconの行に、about.pngが指定されているので、そこをコメントアウトし(行の先頭に#を入れればコメントになります)、かわりにabout.jpgを指定します。

F11を押してコンパイルしたら、F6で実行し、Aboutを表示させてみましょう。

うまくいきましたね。

これだけだと、なんなので仕組みを簡単に解説しておきましょう。Projectsビューから、PersonAboutBox.javaをダブルクリックします(ソースコードが表示された場合は、エディタ上部のDesignボタンを押して、デザインモードに切り替えます)。グラフィックの部分をクリックしてPropertiesビューのiconを見ると、about.jpgが設定されていますね。

すぐ横にある...というボタンを押してみましょう。

Image Within Projectは、クラスパスの中からイメージを読み込むことを指定しています。Define as a Resourceにチェックしておくと、フレームワークが、リソースファイルから、設定を読み出してくれます。リソースファイルというのは、先ほど編集したプロパティファイルのことです。そしてKey、Valueのところに、先ほど変更した行の内容が表示されているのが分かります。

imageLabel.icon=about.jpg
このようにNetBeansがリソースを管理してくれるため、リソースファイルのみを変更することで、表示内容を変更することができたわけです。

それでは、リソースファイルを使って、表示を日本語化してみましょう。もう一度PersonAboutBox.propertiesをダブルクリックします。この内容を日本語にすれば良いわけですが、今回はJavaのリソースバンドルの仕組みを使って、デフォルト(英語)と日本語の2つを用意してみます。こうしておくことで、アプリケーションの国際化が可能になります。Projectsビューから、PersonAboutBox.propertiesを右クリックし、Add Localeを選びます。

下のリストから、日本語を選びます。

日本語のロカールが追加されました。

ja_JPの方をダブルクリックして開きます。

早速日本語に修正します。

修正したら、実行してみましょう。

うまく反映されました。

同様にPersonApp.propertiesやPersonView.propertiesに日本語ロカールを追加して修正すれば、アプリケーション全体を日本語化できます(今回は、省略します)。

次にPersonクラスを作りましょう。src/person/の下にPerson.javaというファイルを作成します。私は単純なJavaのクラスはエディタで作る方が好きなので、エディタから打ち込みましたが、NetBeansでFile => New Fileメニューから作成しても、もちろんokです。

package person;

public class Person {
    String name;
    int age;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

単なるJavaBeansですね。

ファイルが出来たらF11でコンパイルしましょう。コンパイルしたら、Projectsビューから、Person.javaを右クリックし、Tools => Add to Palette...を選びます。これでパレット(右上のボタンなどのGUIパーツが並んでいるところです)に、自分で作成したJavaBeansを登録することができます。今回は、Beansの下に登録しましょう。

これで、パレットのBeansの下にPersonが登録されたはずです。

これをアプリケーションに登録しましょう。Projectsビューの中のPersonView.javaをダブルクリックします。下のフォームビューからForm PersonViewを右クリックし、Add From Palette => Beans => Personを選択します。これで、Personがアプリケーションに追加されました。

それでは、まずアプリケーションのメインウィンドウに表示してみることにしましょう。まずLabelをPaletteでクリックしてから、アプリケーションの上に置きます。この時、以下のように補助線が表示される場所でクリックします。

もう1つラベルを、右横に置きます。以下のような補助線が出る場所にします。

ラベルの右端をマウスでドラッグして、ウィンドウの右端まで拡げます。やはり補助線が表示されるので、それに合わせます。

これが名前の表示領域です。

次に年齢の表示領域を作成します。同様にラベルを2つ配置します。

まず左側のラベルを設定しましょう。jLabel1をクリックし、Propertiesビューの中からtextを探します。

右端の...ボタンを押します。ValueのところにNameと入力してEnterキーを押してから、OKを押します。

同様にjLabel3にAgeを設定します。

次にProjectビューのperson.resourceの中から、PersonView.propertiesをダブルクリックしてみてください。

今入力した内容が、自動的にリソースファイルに保存されていることが分かりますね。

jLabel1という名前は分かりにくいので、修正しましょう。Projectsビューから、PersonView.javaをダブルクリックします。インスペクタビューで、jLabel1を右クリックし、Change Variable Nameを選べば、名前を変更できます。今回は以下のように変更しました。

もう一度PersonView.propertiesを開いてみましょう。

ちゃんとNetBeansが、名前の変更をリソースファイルの中にまで適用してくれているのが分かりますね。リソースファイルを日本語化する方法は既に解説しました。PersonView.propertiesを右クリックして、Add Locale...を選び、日本語を追加し、日本語のラベルを設定してください。

一度保存して、F6で実行してみましょう。

ちゃんと表示されていますね。

次にPersonオブジェクトの内容を、右側に表示します。これはバインドという機能を使用します。既に、上でPersonオブジェクトをPersonViewに追加済みですから、これをラベルにバインドしてやります。PersonView.javaをダブルクリックして開いたら、フォームビューからnameValueLabelを右クリックし、Bind => textを選びます。Binding Sourceにはperson1を、Binding Expressionにはnameをドロップダウンリストから選択してやります。

同じように、ageValueLabelには、Bindin Sourceにperson1を、Binding Expersisonにageをバインドしてやります。設定はこれだけです。保存して実行してみましょう。

Personオブジェクトの初期値が表示されているのが分かりますね。これだけではつまらないので、Personの編集画面を作りましょう。

Projectsビューで、personパッケージを右クリックし、New => Other...を選びます。Swing GUI Formsの名からJDialogを選びNextをクリックします。

名前はEditPersonDialogにしました。

Finishボタンを押すと、ダイアログが作成されます。

上で作成したメインウィンドウには、ラベルを4つ配置しましたが、今度は編集なので、右側はテキストフィールドにします。前と同様にして部品を並べてください。

名前を付けてやります。やり方は前と同じです。右クリックしてChange Variable Nameでしたね。

ボタンを2つ追加します。これも補助線を頼りに、ウィンドウの右下に配置してください。名前は、cancelButtonとokButtonにしました。

前と同様にラベルを設定しましょう。プロパティビューのtextを選び、...ボタンで設定です。ボタンもラベルと同じようにして設定できます。

ニモニックを設定します。これはAlt+英字キーのショートカットの設定です。今回はOKボタンにOを、CancelボタンにCを割り当てました。

設定すると、下線が描画されます。

Nameラベルをクリックし、labelForプロパティを設定します。これは、このラベルが、どのコンポーネントのラベルとして使用されているかを設定します。もちろん、今回はnameValueTextFieldを指定します。

同様にAgeラベルには、ageValueTextFieldを設定します。

NameラベルのdisplayedMnemonicを設定します。今回はNameにはNを、AgeにはAを割り当てました。

同様にラベルにも下線がひかれます。

テストしてみましょう。エディタの上部にある目がついたアイコンでテストできます。Alt+N、Alt+A、Alt+O、Alt+Cを押してみて、上で設定したニモニックがちゃんと機能しているか確認しましょう。okならXアイコンで終了します。

今回は、このダイアログに、Personオブジェクトを渡したいので、メインウィンドウの時と同様にPersonオブジェクトを追加します。フォームビューでOther Componentsを右クリックし、Add From Palette => Beans => Personを選びます。

名前をperson1からpersonに変更しておきます。これはラベルやボタンの名前変更と同じです。インスペクタビューで、右クリックしてChange Variable Nameです。メインウィンドウ側もpersonに変えておいてください。さてメインウィンドウのPersonViewから、EditPersonDialogに編集対象のPersonオブジェクトを渡すため、コンストラクタを少々変更することにします。

    public EditPersonDialog(java.awt.Frame parent, Person person) {
        super(parent, true);
        if (person == null) throw new NullPointerException("person is null");
        this.person = person;
        initComponents();
    }

安全のため、引数のpersonはnullを許さないようにしてあります。

この変更によって、EditPersonDialogの中のmain()メソッドがエラーになります。このメソッドはテスト用にNetBeansが生成したものなので、今回は削除してしまいましょう。

さて、実は、これだけでは不十分です。というのもNetBeansが生成したコードは、initComponents()メソッドの中に以下のような行を含んでおり、せっかくコンストラクタで渡しても、上書きされてしまうのです。

        person = new person.Person();

更にもう1つ設計上の問題があります。EditPersonDialogは、ユーザが入力域に入力すると、その都度Personオブジェクトを変更してしまいます。このため、PersonViewからもらったPersonオブジェクトを直接EditPersonDialog側に設定してしまうと、キャンセルボタンを押しても、その時点では既にPersonオブジェクトが変更された後になってしまい、意味を成さなくなってしまうのです。このためもらったPersonオブジェクトのコピーを作り、OKボタンが押されたら、そこで初めて値をPersonView側に設定するようにしなければなりません。そこでPersonクラスをClonableにすることにします。

public class Person implements Cloneable {
    String name;
    int age;

    public Person clone() {
        try {
            return (Person)super.clone();
        }
        catch (CloneNotSupportedException ex) {
            throw new RuntimeException(ex);
        }
    }
...

そして、EditPersonDialogの、personのインスタンス生成コードを変更します。EditPersonDialog.javaをダブルクリックして、インスペクタビューの中のOther Componentsを展開し、personをクリックします。プロパティビューのCodeというボタンを押すと、Custom Create Codeという項目があります。ここにperson.clone();と入力します。


これでinitComponents()の中のコードは、以下のように変わります。

        person = person.clone();

では、バインドしましょう。エディタ上部のDesignボタンを押して、デザインモードに切り替えます。インスペクタビューからnameValueTextFieldを右クリックし、Bind => textを選びます。メインウィンドウの時と同様にして、personのnameプロパティを割り当てましょう。

同様にして、ageValueTextFieldにはageプロパティを割り当ててください。

それでは、メインウィンドウから、このダイアログを表示することにしましょう。すでにメインウィンドウには、Aboutダイアログを表示するロジックがあるので、これを真似すれば良いのです。まず編集のためのボタンを作りましょう。PersonView.javaをダブルクリックしてボタンを配置します。

名前をeditButtonに変更しておきます(もう慣れたでしょうから、やり方は省略します)。

次にボタンの動作を登録します。プロパティビューからactionを選び、...をクリックし、Actionのドロップダウンから、Create New Actionを選びます。

以下の図のように設定します。

これで、PersonViewクラスにeditPerson()というメソッドが用意され、ボタンを押すと、このメソッドが呼ばれるように設定されます。ソースを見ると、以下のようなメソッドが用意されているのが分かります。

    @Action
    public void editPerson() {
    }

Tool Tipに設定した文字列は、ボタンの上にマウスを乗せた状態にすると表示される、簡単なヘルプです。Acceleratorを設定しているので、Ctrl-Eを押すことでも、このボタンを押すことができます。

上で作ったPerson編集ダイアログを表示するコードを書きましょう。すぐ上にAboutダイアログを表示するメソッドがあるので、それを真似すれば良いのです。さて、上で述べた通り、EditPersonDialogは、受け取ったPersonオブジェクトのコピーを作って、その内容を更新します。従ってもしもCancelボタンが押されたら、単にその内容を捨て、OKボタンが押されたら変更されたPersonオブジェクトを取り込めば良いことになります。EditPersonDialogが持っているPersonオブジェクトにアクセスできるよう、まずはゲッタを作りましょう。EditPersonDialogのソースを表示し、ソース上で右クリック => Refactor => Encupslate Fields...を選びます。personフィールドのCreate Getterの所にチェックを入れて、Refactorボタンを押せばゲッタが生成されます。

EditPersonDialogのOKボタンとCancelボタンの処理を作成します。OKボタンをクリックし、上でEditボタンの処理を作成した時と同様に、actionプロパティの...ボタンを押してアクションを作ります。

今回は、OKボタンにokButtonPerformed()メソッドを、CancelボタンにcancelButtonPerformed()を割り当てました。とりあえずはメソッドの中身は空っぽにしておきます。

それではEditPersonDialogを表示する処理を見てみましょう。PersonView.javaのeditPerson()メソッドの中身を実装します。

    @Action
    public void editPerson() {
        EditPersonDialog editPersonDialog = new EditPersonDialog(getFrame(), person) { // (1)
            @Override public void okButtonPerformed() {
                Person oldPerson = PersonView.this.person;
                person = getPerson();
                for (Binding b : bindingGroup.getBindings()) { // (3)
                    if (b.getSourceObject() == oldPerson) { // (4)
                        b.unbind(); // (4)
                        b.setSourceObject(person); // (4)
                        b.bind(); // (4)
                    }
                }
                dispose();
            }
            
            @Override public void cancelButtonPerformed() {
                dispose(); // (2)
            }
        };
        editPersonDialog.setLocationRelativeTo(getFrame()); // (5)
        ((SingleFrameApplication)getApplication()).show(editPersonDialog); // (5)
    }

まず、EditPersonDialog()にPersonオブジェクトを渡してインスタンスを作りますが、この時、継承をしてokButtonPerformed()とcancelButtonPerformed()をオーバーライドします(1) 。cancelButtonPerformed()の場合は、単にダイアログを削除して終わりです(2)。OKボタンの処理ですが、jsr295, 6では、バインディングという仕組みでJavaBeansとコンポーネントを結び付けています。現在のバインディングは、bindingGroupのgetBindings()を呼び出せば一覧できます(3)。各バインディングが、Personオブジェクトに紐付いているかを調べ、もしそうならば一旦バインドを解除してから、新しいPersonオブジェクトにバインドしてやります。あとは、Aboutダイアログの時と同様のコードでダイアログを表示してやります。Person編集内容の反映には、今回の方法の他に、プロパティをそれぞれコピーしてやる方法が考えられます。この方法をとるには、Personクラスを正式なJavaBeansにする必要があるため、次章で解説します。それでは実行して、値を変更してみてください。

次に値のチェックを行えるようにしてみましょう。まずEditPersonDialogの右側にエラーを表示するため、場所を開けます。

エラー表示用のラベルを配置します。

エラー表示用のラベルのtextプロパティを削除しておきます。

コンポーネントの名前を変更します。今回はnameErrorLabel、ageErrorLabelとしておきました。

バインディングを区別したいので、バインディングに名前を付けておきます。EditPersonDialogのnameValueTextFieldのtextプロパティへのバインディングを開きます。Advancedのタブを開き、Identificationのところにperson.nameと入力します。

同様にageValueTextFieldのtextプロパティへのバインディングにperson.ageという名前を付けます。

バインディングの際のエラーを受け取るには、BindingListenerを登録します。EditPersonDialogのコンストラクタでリスナを登録して、バインディングのエラーを受け取れるようにしましょう。

    public EditPersonDialog(java.awt.Frame parent, Person person) {
        super(parent, true);
        if (person == null) throw new NullPointerException("person is null");
        this.person = person;
        initComponents();
        bindingGroup.addBindingListener(new BindingListener() {
            public void bindingBecameBound(Binding binding) {}
            public void bindingBecameUnbound(Binding binding) {}

            public void syncFailed(Binding binding, SyncFailure failure) { // (1)
                ResourceMap resourceMap =
                    Application.getInstance(PersonApp.class)
                    .getContext().getResourceMap(EditPersonDialog.class);
                if ("person.name".equals(binding.getName())) { // (2)
                    if (failure.getType() == SyncFailureType.VALIDATION_FAILED) { // (3)
                        nameErrorLabel.setText(resourceMap.getString("invalid.value.error")); // (5)
                    }
                }
                else if("person.age".equals(binding.getName())) { // (2)
                    if (failure.getType() == SyncFailureType.CONVERSION_FAILED) { // (3)
                        ageErrorLabel.setText(resourceMap.getString("number.format.error")); // (4)
                    }
                    else if (failure.getType() == SyncFailureType.VALIDATION_FAILED) { // (3)
                        ageErrorLabel.setText(resourceMap.getString("invalid.value.error")); // (5)
                    }
                }
            }

            public void synced(Binding binding) {
                nameErrorLabel.setText("");
                ageErrorLabel.setText("");
            }

            public void sourceChanged(Binding binding, PropertyStateEvent event) {}
            public void targetChanged(Binding binding, PropertyStateEvent event) {}
        });
    }

syncFailed()は、コンポーネントの値をJavaBeansに反映できなかった場合に呼び出されます(1)。バインディングに名前を付けておいたので、(2)のようにして、どのバインディングでエラーが発生したのかを識別することができます(名前を付けておかないと、binding.getName()はnullを返します)。SyncFailureオブジェクトのgetType()を呼び出すことで、エラーの種類を特定できます。コンポーネントに設定された値を、バインド先に反映する場合、型変換、バリデーションの順に処理が行われます。例えば、今回の年齢フィールドの場合、テキストフィールドなので、数字以外の文字列も入力できてしまいます。そうした場合にはCONVERSION_FAILEDというエラーになります。型変換に成功したとしても例えば年齢が500歳とかだったら、おかしいですね。このようなエラーはVALIDATION_FAILEDで通知されます。そこで、今回はCONVERSION_FAILEDなら、"number.format.error"というリソースを(4)、VALIDATION_FAILEDならば"invalid.value.error"というリソースの内容を(5)表示するようにしています。もちろん、このエラーメッセージは、EditPersonDialog.propertiesに設定します。そろそろ日本語のリソースも作成しておきましょう。方法は覚えていますか? EditPersonDialog.propertiesを右クリックし、Add Locale...を選んで、リストから日本語を選択します。日本語のリソースファイルを作成しましょう。

nameLabel.text=名前
ageLabel.text=年齢
okButton.text=OK
cancelButton.text=キャンセル(C)
okButtonPerformed.Action.text=OK
okButtonPerformed.Action.shortDescription=
cancelButtonPerformed.Action.shortDescription=
cancelButtonPerformed.Action.text=キャンセル(C)
nameErrorLabel.text=
ageErrorLabel.text=
number.format.error=数値を入力してください。
invalid.value.error=値が不適当です。

以下は編集画面で、年齢のところに"aaa"と入力した例です。

せっかくなので赤で表示するようにしておきましょう。これまでのプロパティ設定と同様にforegroundプロパティを設定してやるだけです。


さて、上でVALIDATION_FAILEDというエラーについて定義しましたが、まだバリデータを登録していないので、このエラーメッセージは出力されません。今度はバリデータを登録しましょう。次ようなチェックをすることにします。

フィールドチェック内容
名前長さが1以上
年齢0以上300以下

バリデータは、クラスとして作成します。次のようなバリデータを用意することにしましょう。

バリデータ クラスチェック内容
RequiredValidatornullでないこと。文字列型の場合は、更に長さが0で無いこと。
MinMaxValidator値指定された範囲であること。

最初にRequredValidatorを見てみましょう。

package person;

import org.jdesktop.beansbinding.Validator;

public class RequiredValidator extends Validator<Object> { // (1)
    enum ErrorCode {
        NULL_VALUE, LENGTH_ZERO;
    }

    public final Validator.Result NULL_VALUE // (3)
        = new Validator.Result(ErrorCode.NULL_VALUE, "Null is not permitted.");
    public final Validator.Result LENGTH_ZERO
        = new Validator.Result(ErrorCode.LENGTH_ZERO, "Length zero.");

    @Override
    public Validator.Result validate(Object value) { // (2)
        if (value == null) return NULL_VALUE;
        if (value instanceof CharSequence &&
            ((CharSequencevalue).length() == 0)) return LENGTH_ZERO;
        return null;
    }
}

バリデータは、Validatorクラスを継承して作成します。Validatorは型パラメータを持つことができますが、今回はあらゆる型のプロパティに利用可能バリデータなので、Objectを指定しています(1)。バリデータクラスで実装が必要なのは、validate()メソッドです(2)。validate()メソッドは、バリデーションでエラーが無ければnullを、そうでなければValidator.Resultクラスのインスタンスを返すようにします。今回は、nullのケースと、文字列で長さ0のケースで別のResultを用意しました。Validate.Resultはイミュータブルなので、使い回して構いません。ただし残念ながら、なぜかstaticなネステッドクラスになっていないので、ここでは非スタティックなfinalフィールドで宣言してあります(3)。あとはプロパティを調べて適切なResultを返すようにvalidate()メソッドを実装するだけです。

バリデータもJavaBeansとして登録します。せっかくなので、パレットにValidatorというカテゴリを新設しておくことにしましょう。PaletteのMiscカテゴリの上で、右クリックし、Create New Categoryを選択します。


Validatorと入力してValidatorカテゴリを作ります。

あとは、これまでと同様に、RequiredValidatorをValidatorカテゴリに登録し、EditPersonDialogに追加してください。

nameValueTextFieldに登録しましょう。プロパティからBindingを選んで、textプロパティの...ボタンを押します。Binding設定のAdvancedというタブを選ぶと、Validatorの設定があるので、ここにrequiredValidator1を設定してやります。さて、上で、VALIDATION_FAILEDの時には、invalid.value.errorというメッセージを表示するようにしましたね。

    if ("person.name".equals(binding.getName())) { // (2)
        if (failure.getType() == SyncFailureType.VALIDATION_FAILED) { // (3)
            nameErrorLabel.setText(resourceMap.getString("invalid.value.error")); // (5)
        }
    }

EditPersonDialogのリソースに日本語のロカールを追加して、メッセージを追加しましょう。

nameLabel.text=名前
ageLabel.text=年齢
...
number.format.error=数値を入力してください。
invalid.value.error=値が不適当です。

それではアプリケーションを起動して、Person編集画面を表示し、名前のフィールドに何か入力してから、全て削除してフィールドを空にしてみてください。


エラーメッセージが表示されましたね。ただ、このメッセージは、バリデーション失敗の時の一般的なメッセージとして用意したので、今ひとつ的確ではないですね。BindingListenerのsyncFailed()メソッドが呼び出された時に渡されるSyncFailureオブジェクトは、バリデータが返したResultオブジェクトを保持しているので、これを判別すれば、もっと適切なメッセージを出力することができます。RequredValidatorは長さ0のエラーの時、エラーコードとして、ErroCode.LENGTH_ZEROを渡していました。

    public final Validator.Result LENGTH_ZERO
        = new Validator.Result(ErrorCode.LENGTH_ZERO, "Length zero.");

これを使って判定しましょう。

    public void syncFailed(Binding binding, SyncFailure failure) {
        ResourceMap resourceMap =
            Application.getInstance(PersonApp.class)
            .getContext().getResourceMap(EditPersonDialog.class);
        if ("person.name".equals(binding.getName())) {
            if (failure.getType() == SyncFailureType.VALIDATION_FAILED) {
                if (failure.getValidationResult().getErrorCode() == // (1)
                    RequiredValidator.ErrorCode.LENGTH_ZERO)
                {
                    nameErrorLabel.setText(resourceMap.getString("length.zero.error")); // (2)
                }
                else {
                    nameErrorLabel.setText(resourceMap.getString("invalid.value.error"));
                }
            }
        }

リザルトコードをチェックして、長さ0のエラーを判定し(1)、エラーメッセージを設定しています(2)。あとはEditPersonDialogのリソースにエラーメッセージを追加するだけです。

length.zero.error=入力してください。

実行してみましょう。

うまく表示されました。次にMinMaxValidatorを作成してみましょう。

package person;

import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import org.jdesktop.beansbinding.Validator;

public class MinMaxValidator<T extends Number> extends Validator<T> {
    public enum ErrorCode {
        BELOW_MIN, ABOVE_MAX;
    }

    T min;
    T max;

    public void setMin(T min) {
        this.min = min;
    }

    public T getMin() {
        return min;
    }

    public void setMax(T max) {
        this.max = max;
    }

    public T getMax() {
        return max;
    }

    static final Map<Class<? extends Number>, Comparator<? extends Number>> comparatorTable
        = new HashMap<Class<? extends Number>, Comparator<? extends Number>>();
    static {
        comparatorTable.put(Byte.class, new Comparator<Byte>()
            {public int compare(Byte b1, Byte b2) {return b1.compareTo(b2);}});
        comparatorTable.put(Integer.class, new Comparator<Integer>()
            {public int compare(Integer i1, Integer i2) {return i1.compareTo(i2);}});
        comparatorTable.put(Double.class, new Comparator<Double>()
            {public int compare(Double d1, Double d2) {return d1.compareTo(d2);}});
        comparatorTable.put(Float.class, new Comparator<Float>()
            {public int compare(Float f1, Float f2) {return f1.compareTo(f2);}});
        comparatorTable.put(Long.class, new Comparator<Long>()
            {public int compare(Long l1, Long l2) {return l1.compareTo(l2);}});
        comparatorTable.put(Short.class, new Comparator<Short>()
            {public int compare(Short s1, Short s2) {return s1.compareTo(s2);}});
    }

    public final Validator.Result BELOW_MIN
        = new Validator.Result(ErrorCode.BELOW_MIN, "Value is less than minimum value.");
    public final Validator.Result ABOVE_MAX
        = new Validator.Result(ErrorCode.ABOVE_MAX, "Value exceeds maximum value.");

    @Override
    public Validator<T>.Result validate(T value) {
        if (value == null) return null;
        Comparator<T> cmp
            = (Comparator<T>)comparatorTable.get(value.getClass());
        if (cmp == null)
            throw new RuntimeException("Unsupported type:" + value.getClass());
        if (cmp.compare(value, min) < 0) return BELOW_MIN;
        if (cmp.compare(max, value) < 0) return ABOVE_MAX;
        return null;
    }
}

ちょっと欲張って、byte, short, integer, long, float, doubleの全てに対応しています。このため少々複雑になっていますが、ロジック自体はそれほど難しくは無いはずです。最小値と最大値は外から設定できるようにし、それとバリデータに渡された値とを比較して、最小値より小さければBELOW_MINを、最大値よりも大きければABOVE_MAXを返すようにしてあるだけです。RequiredValidatorの時と同様にパレットに追加して、EditPersonDialogに追加してください。このバリデータは最大値と最小値を保持するので、今回は名前をageValidatorに変えておきましょう。

今回のJavaBeansは型パラメータ付きなので、プロパティシートのCodeのページでTypeParametersを指定しておきます。年齢はintなので<Integer>を設定します。

残念ながら、現バージョンのNetBeansでは、型パラメータをIntegerにしてやっても、max, minのプロパティはIntegerとは認識されないようで、プロパティシートに直接値を設定することはできません。

今回は、...ボタンを押し、メニューからCustom codeを選んで、値を指定することにします。

minには0を、maxには300を指定します。それでは実行してみましょう。

範囲外の値を入力するとエラーになっているのが分かりますね。もちろん、これも前と同じようにもっと分かり易いメッセージにすることができます。もうやり方は想像できますね?

    else if("person.age".equals(binding.getName())) {
        if (failure.getType() == SyncFailureType.CONVERSION_FAILED) {
            ageErrorLabel.setText(resourceMap.getString("number.format.error"));
        }
        else if (failure.getType() == SyncFailureType.VALIDATION_FAILED) {
            if (failure.getValidationResult().getErrorCode() ==
                MinMaxValidator.ErrorCode.BELOW_MIN)
            {
                ageErrorLabel.setText
                    (String.format(resourceMap.getString("min.value.error"),
                                   ageValidator.getMin(),
                                   ageValidator.getMax()));
            }
            else if (failure.getValidationResult().getErrorCode() ==
                     MinMaxValidator.ErrorCode.ABOVE_MAX)
            {
                ageErrorLabel.setText
                    (String.format(resourceMap.getString("max.value.error"),
                                   ageValidator.getMin(),
                                   ageValidator.getMax()));
            }
            else {
                ageErrorLabel.setText(resourceMap.getString("invalid.value.error"));
            }
        }
    }

特に解説は必要無いかと思います。RequiredValidatorと異なるのは、メッセージの中に最小、最大値を出力したいので、String.format()を使って、メッセージを組み立てている点でしょう。リソースは以下のように設定しました。

min.value.error=値が小さ過ぎます。(%1$,d < 値 < %2$,d である必要があります)
max.value.error=値が大き過ぎます。(%1$,d < 値 < %2$,d である必要があります)

実行して範囲外の値を入力すると、以下のように表示されます。


Valid XHTML 1.0 Strict

 ご感想をお聞かせください(ruimo@ruimo.com)。なお、誠に勝手ながら、HTMLメールはサーバーで全て削除されますので、テキストメールでお願いいたします。

 トップページへ