kanizaのブログ

コンピュータ、ソフトウェア、映画、音楽関連や家族のことなど、思いついたことを書きます。

JGoodies Binding入門

日本語でJGoodies Bindingを扱った記事というのはなかなかないので、ちょっとだけ書いてみる。僕自身の経験と理解をもとにしているので間違えてたらごめんなさい。

JGoodies Bindingって何?

JGoodies Bindingは、あるオブジェクトが持つ値(典型的にはJavaBeanのプロパティ)と、SwingによるGUIをカンタンに同期させるためのフレームワークだ。Karsten Lentzsch氏がJGoodies Swing Suiteの一部として開発して、BSDライセンスで公開している。

たとえば、あるJavaBeanにint型のプロパティがあって、JTextFieldにその値を表示させるとしよう。JGoodies Bindingを用いて同期させれば、JTextFieldの値を編集した場合にオブジェクトのプロパティが自動的に更新されるし、別の要因(Undoなど)でオブジェクトのプロパティが変更された場合にはJTextFieldが自動的に更新される。

通常はこれらの同期のためにいろいろとコードを書かなければならなくて、結果としてバグが入り込んでしまったりする。JGoodies Bindingは、そういった問題解決を使いやすいAPIとよくテストされた実装でサポートしてくれる。

ここでは、JGoodies Bindingの基本概念であるValue Model および Presentation Modelと、それらを用いたサンプルコードを紹介して、簡単な使い方を理解してもらえればと思っている。前提知識としてMVCの基本的な考え方を用いているので「何それ?」という人は調べてから読んでほしい。

なお、Value Model や Presentation Model は Smalltalk 処理系であるVisualWorksに由来する考え方なので、興味がある人はそれら由来についても調べてみてほしい(僕もよくは知らないのだけど)。

ValueModel

JGoodies BindingはValue Modelという考え方をもとに構築されている。JGoodies BindingにおけるValue Model は、次のようなインタフェースによって表現されている。

public interface ValueModel {
    Object getValue();
    void setValue(Object value);
    void addValueChangeListener(PropertyChangeListener l);
    void removeValueChangeListener(PropertyChangeListener l);
}

つまり、値の設定/取得/監視ができるオブジェクトということになる。

たとえばJavaBeanの特定のプロパティに対する ValueModel を作れば、setValue(Object)すればそのプロパティが更新され、getValue()すればそのプロパティ値が取れるようになる。

+--------+ setValue(v)  +------------+          +----------+
|        |------------->|            |          |          |
| Client |              | ValueModel |<-------->| JavaBean |
|        |<-------------|            |          |          |
+--------+ getValue()   +------------+          +----------+

これによって、Clientからは相手がJavaBeanなのか何なのか気にすることなく、単に「値」に着目して動作できるようになる。JGoodies Binding は、各種データを ValueModel という形で抽象化して、その値をSwingコンポーネントと同期させる機能を提供している。

このValueModelにはいろいろな実装があって、たとえばある ValueModel への変更を一時的に保留しておくBufferedValueModelや、別の ValueModel の値を変換して返せる各種 Converter などが組み込まれている。自分でも簡単に独自実装を作れる。

Value Model は強力な概念で、他にもいろいろと使い道がある。どんなものがあるか考えたり調べたりしてみると面白いと思う。

Presentation Model

Value Model が単一の値へのアクセスを提供するのに対して、Presentation Model は1つの画面を構成するような複数の値へのアクセスを提供する。具体的には、JavaBean の複数プロパティに対する Value Model の生成/管理機能を主な役割として持ち、GUIに都合の良い形でモデルを表現する。

通常は、ある画面に表示する一連のデータを保持するモデルとして JavaBean を作っておいて、Presentation Model を通じてその JavaBean の各プロパティの ValueModel を取得し、GUIと同期させる。実際にGUIと接続するのはビュー側の役割となる。ビュー側は PresentationModel を受け取って、ValueModel と同期したJTextFieldとかを作って配置することになる。

PresentationModel には、ValueModel への変更をいったんバッファしておいて後から一括で JavaBean に反映させる機能も組み込まれているから、たとえば「OKボタンを押したら反映させる」というようなGUIも簡単に作れる。

PresentationModel はサブクラス化してより使いやすい API にできるし、独自のロジックを追加することもできる。ビューに関するロジックは PresentationModel に持たせてビューからはできるだけロジックを排除し、データモデルにはビューとは関係ないコアなロジックを実装するのが良いと思う。依存関係としてはビュー->PresentationModel->データモデルとなる。

サンプル

簡単なサンプルを用意した。起動するとJTextFieldとJSpinnerとJSliderを配置した小さなウィンドウが開く。JSpinnerの値とJSliderの値は同じデータと同期していて、どちらかを変更するともう一方も動く。

OKまたはCancelボタンを押すと、結果としてデータモデルがどのような値になったかを表示して終了する。

なお、このサンプルにはJGoodies Binding 1.3.1とJGoodies Forms 1.0.7を使っているので(もっと前のバージョンでも動くとは思うけど)試してみる場合には別途入手してくだされ。

Mainクラス

JFrameにビュークラスから取得したJComponentを貼り付けている。ボタンが押されたらモデルのtoString()を表示して終わる。

package bindingsample;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

public class Main {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                final JFrame frame = new JFrame("BindingSample");
                frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        
                final BSPresentationModel model = new BSPresentationModel(new BSModel());
                BSView view = new BSView(model);
                frame.add(view.getViewComponent());
                frame.getRootPane().setDefaultButton(view.defaultButton());
                model.getTriggerChannel().addValueChangeListener(new PropertyChangeListener() {
                    public void propertyChange(PropertyChangeEvent ev) {
                        JOptionPane.showMessageDialog(frame, model.getBean().toString());
                        frame.dispose();
                    }
                });
                frame.pack();
                frame.setVisible(true);
            }
        });
    }
}
モデルクラス

この画面で編集したいデータを単純なJavaBeanとして表現している。各プロパティの変更時にはPropertyChangeEventを出す。

package bindingsample;

import com.jgoodies.binding.beans.Model;

public class BSModel extends Model {

    public static final String PROPERTYNAME_LABEL = "label";
    public static final String PROPERTYNAME_VALUE = "value";
    public static final int MAX_VALUE = 100;
    public static final int MIN_VALUE = -100;
    public static final int DEFAULT_VALUE = 0;
    
    private String label = "BindingSample";
    private int value = DEFAULT_VALUE;
    
    public BSModel() {
    }
    
    public String getLabel() {
        return label;
    }

    public void setLabel(String label) {
        String oldLabel = this.label;
        this.label = label;
        firePropertyChange(PROPERTYNAME_LABEL, oldLabel, label);
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        int oldValue = this.value;
        this.value = value;
        firePropertyChange(PROPERTYNAME_VALUE, oldValue, value);
    }
    
    public String toString() {
        return "label:" + label + " value: " + value;
    }
}
PresentationModelクラス

PresentationModelを拡張して、JSpinnerやJSlider用のモデルへのアクセスを提供している。

package bindingsample;

import javax.swing.BoundedRangeModel;
import javax.swing.SpinnerModel;

import com.jgoodies.binding.PresentationModel;
import com.jgoodies.binding.adapter.BoundedRangeAdapter;
import com.jgoodies.binding.adapter.SpinnerAdapterFactory;
import com.jgoodies.binding.value.ValueModel;

public class BSPresentationModel extends PresentationModel {
    private SpinnerModel spinnerModel = null;
    private BoundedRangeModel rangeModel = null;
    
    public BSPresentationModel(BSModel model) {
        super(model);
    }
    
    public ValueModel getLabelModel() {
        return getBufferedModel(BSModel.PROPERTYNAME_LABEL);
    }
    
    public SpinnerModel getValueSpinnerModel() {
        if (spinnerModel == null) {
            spinnerModel = SpinnerAdapterFactory.createNumberAdapter(getBufferedModel(BSModel.PROPERTYNAME_VALUE), BSModel.DEFAULT_VALUE, BSModel.MIN_VALUE, BSModel.MAX_VALUE, 1);
        }
        return spinnerModel;
    }
    
    public BoundedRangeModel getValueRangeModel() {
        if (rangeModel == null) {
            rangeModel = new BoundedRangeAdapter(getBufferedModel(BSModel.PROPERTYNAME_VALUE), 0, BSModel.MIN_VALUE, BSModel.MAX_VALUE);
        }
        return rangeModel;
    }
}
ビュークラス

BSPresentationModelをもとに、GUI部品を生成、配置している。このクラスがJComponentを継承していないのは議論の余地があるかと思う。人によってはJPanelのサブクラスとかにして、直接GUIに配置できるようにしても良いかと。

レイアウトにはJGoodies Formsを利用している。ここでは詳細は割愛。

package bindingsample;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JTextField;

import com.jgoodies.binding.adapter.BasicComponentFactory;
import com.jgoodies.forms.builder.PanelBuilder;
import com.jgoodies.forms.factories.ButtonBarFactory;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;

public class BSView {

    private BSPresentationModel model;
    private JComponent viewComponent = null;
    private JButton ok = null;

    public BSView(BSPresentationModel model) {
        this.model=model;
    }
    
    public JComponent getViewComponent() {
        if (viewComponent == null) {
            viewComponent = createViewComponent();
        }
        return viewComponent;
    }
    
    public JButton defaultButton() {
        return ok;
    }
    
    private JComponent createViewComponent() {
        JTextField label = BasicComponentFactory.createTextField(model.getLabelModel());
        JSpinner spinner = new JSpinner(model.getValueSpinnerModel());
        JSlider slider = new JSlider(model.getValueRangeModel());
        ok = new JButton("OK");
        ok.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent ev) {
                model.triggerCommit();
            }
        });

        JButton cancel = new JButton("Cancel");
        cancel.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent ev) {
                model.triggerFlush();
            }
        });
        JPanel buttonBar = ButtonBarFactory.buildOKCancelBar(ok, cancel);
        
        FormLayout layout = new FormLayout("right:pref, 4dlu, fill:pref:grow", "pref, 4dlu, pref, 4dlu, pref, 4dlu, pref");
        PanelBuilder builder = new PanelBuilder(layout);
        CellConstraints cc = new CellConstraints();
        
        builder.setDefaultDialogBorder();
        
        builder.addLabel("label", cc.xy(1, 1));
        builder.add(label, cc.xy(3, 1));
        builder.addLabel("value", cc.xy(1, 3));
        builder.add(spinner, cc.xy(3, 3));
        builder.add(slider, cc.xyw(1, 5, 3));
        builder.add(buttonBar, cc.xyw(1, 7, 3));
        return builder.getPanel();
    }
}

まとめ

JGoodies Bindingを使うと、GUIとデータの同期が簡単にできるようになる。データモデルとビュー、PresentationModelの役割分担が進んで、システムの本来のロジックとGUI用のロジックを分離でき、メンテナンス性も向上する。Swingアプリケーション開発のツールとして手元に置いておくのがおすすめ。

JGoodies Bindingを使ったSwingアプリケーションの開発についてはまだ書きたいことがあるけど今回はとりあえずこのへんで。

参考リンク