Powered by SmartDoc

3 GUI領域での段階的プログラミング

3.1 MVCの特徴

MVCの特徴を挙げてみると、

がある。

3.2 MVCを使わないGUIカウンタ

最初にGUIカウンタのGUIインタフェースを決めることにする。図[カウンタのGUIインタフェース]のようにresetボタン、incrementボタン、decrementボタンと整数の値を表示するテキストボックスから構成される。

図 3.2.1 カウンタのGUIインタフェース

リスト[CountDemo.java]がGUIカウンタのメインプログラムである。

リスト 3.2.1 CountDemo.java
package nonmvc;

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JFrame;

import model.AbstaractCount;
import model.ICount;
import model.IntegerCount;

/**
 * CountDemo.java
 * 
 * @author Hiroshi TAKEMOTO
 * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $
 */
public class CountDemo {
	// メインプログラム
	public static void main(String[] args)
	{
		ICount model = new IntegerCount();	// IntegerCountモデルを生成

		CountView	view = new CountView(model);	// モデルを持つカウンタビューを生成する

		JFrame frame = new JFrame("CountDemo");	// テスト用のフレームを生成する
		frame.addWindowListener(					// フレームにウィンドウリスナーを追加する
			new WindowAdapter() {                 
				// フレームのクローズイベント処理のみを定義する
				public void windowClosing(WindowEvent evt) {
					System.exit(0); 				// プログラムを終了する
				}
			}
		);
		frame.getContentPane().add(view); // フレームにカウンタビューを追加する
		frame.pack();                     // パッキングして
		frame.show();                     // フレームを表示する
	}

}

IntegerCountのインスタンスをモデル(14)として作成し、それをCountViewに渡してビューを作成する。後は、おきまりのウィンドウを閉じるためのWindowAdapterを登録してフレームを表示する。次に、CountViewをリスト[CountView.java]に示す。

リスト 3.2.2 CountView.java
package nonmvc;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JTextField;

import model.AbstaractCount;
import model.ICount;
import model.IntegerCount;

/**
 * CountView.java
 * 
 * @author Hiroshi TAKEMOTO
 * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $
 */

public class CountView extends JPanel {
	protected	JButton 	resetButton;		// リセットボタン
	protected	JButton 	incrementButton;	// incrementボタン(カウントアップ)
	protected	JButton 	decrementButton;	// decrementボタン(カウントダウン)
	protected	JTextField	valueBox;			// 値表示用テキストボックス
	private	ICount		model_;				// カウンタモデル
	
	/**
	 * CountViewコンストラクタ
	 * @param model 整数カウンタモデル
	 */
	public CountView (ICount model) {	
		JPanel panel = new JPanel(); // パネルを生成する
		model_ = model;              // モデルをビュー内部に保持する
		// resetボタン用のアクション(コールバック)を生成する
		// この部分がJDK1.3で新しく導入された部分!
		Action	resetAction = new AbstractAction("reset") {
				// アクションを実行メソッド
			public void actionPerformed(ActionEvent evt)
			{
				// モデルをリセットし、その値をvalueBoxに表示する
				model_.reset();
				valueBox.setText(model_.toString());
			}
		};
		// アクションresetActionを持つリセットボタンを生成する
		// この部分がJDK1.3で新しく導入された部分!
		resetButton = new JButton(resetAction);
		// resetButtonをパネルに追加する
		panel.add(resetButton);

		// 同様にincrementボタンを生成
		Action	incrementAction = new AbstractAction("increment") {
			public void actionPerformed(ActionEvent evt)
			{
				model_.increment();
				valueBox.setText(model_.toString());
			}
		};
		incrementButton = new JButton(incrementAction);
		panel.add(incrementButton);

		// decrementボタンを生成
		Action	decrementAction = new AbstractAction("decrement") {
			public void actionPerformed(ActionEvent evt)
			{
				model_.decrement();
				valueBox.setText(model_.toString());
			}
		};
		decrementButton = new JButton(decrementAction);
		panel.add(decrementButton);

		// 値を表示するテキストボックスを生成する
		valueBox = new JTextField(model_.toString());
		// テキストボックスの値を編集できないよう属性を変更する
		valueBox.setEditable(false);

		// レイアウトをBorderLayoutに変更し、上部にボタンパネル、中央にテキストボックス
		// を配置する
		setLayout(new BorderLayout());
		add(valueBox, "Center");
		add(panel, "North");
	}
}// CountView

ボタンの作成のところで、

		Action	resetAction = new AbstractAction("reset") {
			public void actionPerformed(ActionEvent evt)
			{
				model_.reset();
				valueBox.setText(model_.toString());
			}
		};
		resetButton = new JButton(resetAction);
		

のようにAbstractActionを匿名クラスとして定義し、JButtonのコンストラクタに渡す。これで、ボタンを押したときの動作を登録する。これがJDK 1.3で新たに追加された機能である。この機能によってボタンのコールバックがとてもきれいに記述することができる。resetボタンが押されたときにモデルにresetをコールし、モデルの値をvalueBoxにセットする。しかし、本来リセットボタンの機能(15)としては、ボタンを押された時にモデルにresetコールを実行することであり、値の再表示をどのように行う かはビューの処理である。これがMVCを使っていないプログラムの欠点である。

  1. 今回model_をIntegerCountの変数と宣言するのではなく、インタフェースICountの変数とした。 これによって、モデルクラスの切り替えによるソースの変更を1カ所にすることができた。
  2. ここではコントローラとしてのリセットボタンの機能を言っている

3.3 Observer, Observableを使ったMVCカウンタ

モデルとビューにObserver, Observableを使ったMVCカウンタを作成する。最初にモデルであるAbstaractCountをObservableのサブクラスに変更する。

public abstract class AbstaractCount extends Observable
	implements ICount {			
		

次にsetValue_メソッドからObserverに変更を通知するために、setChanged, notifyObserversメソッド呼び出しを追加する。

		public void setValue_(Object value_) {
			this.value_ = value_;
			setChanged();
			notifyObservers();
		}
		

次にビューを修正する。比較のためにCountView.javaをコピーして、ObserverCountView.javaとして変更する。Observerインタフェースの追加を宣言し、updateメソッドを追加する

public class ObserverCountView extends JPanel 
	implements Observer {			
		
リスト 3.3.1 updateメソッド
		public void update(Observable o, Object arg) {
			if (o instanceof ICount)
				valueBox.setText(((ICount)o).toString());
			else
				valueBox.setText("");
		}			
		

MVCを使わないGUIカウンタで、行っていたsetTextをupdateで行うため、各ActionのactionPerformedからこの処理を削除する。

		Action	resetAction = new AbstractAction("reset") {
			public void actionPerformed(ActionEvent evt)
			{
				model_.reset();
			}
		};			
		

これで、コントローラはICountで定義した操作を実行するだけでよくなる。最後に、ビューをモデルのObserverとして登録する処理を行えば完了である。

		if (model instanceof Observable)
			((Observable)model).addObserver(this);
		

最終のソースは、リスト[AbstractCount.java]リスト[ObserverCountView.java]に示す。

3.4 独自のバリューモデルインタフェースを使ったGUIカウンタ

フレームワークを使った場合、Observableのサブクラスとしてモデルを実装することができない場合がある。このような場合には、

の2つの方法がある。ここでは、独自のバリューモデルインタフェースを定義する方法を使ってGUIカウンタを実装してみる。(16)

  1. JavaWorld 2000, jul., p110-119を参考にした

3.4.1 バリューモデルインタフェース

モデルは、インタフェースの定義とその実装に分かれる。値モデルのインタフェースをに示す。このモデルインタフェースの特徴は、

である。

リスト 3.4.1.1 IValueModel.java
package valuemodel;

import java.util.*;

/**
 * IValueModel.java
 *
 * MVCの検証のために、一番単純なValueModelを作成する
 * javaでは、interfaceを使ってうまくMVCを構築している。
 * 特に、モデルの発行するイベントオブジェクトの受け手が
 * 処理する部分をinterfaceに記述している点に注意。
 *
 * 参考文献 JavaWorld 2000, jul., p110-119
 *
 * Created: Tue Jan 09 00:10:09 2001
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version 1.00
 */

public interface IValueModel {
	// 内部イベントクラスを使って固有の情報をリスナーに渡せるようにしている。
	public static class Event extends EventObject {
		private Object value_;
		
		public Event (IValueModel model, Object value) {
			// イベントの発行元をセットするためにsuper(model)をコール。
			super (model);
			this.value_ = value;
		}
		public Object getValue_() {
			return value_;
		}
	}
	
	// この部分をリスナーで実装することに注意!
	public interface Listener extends EventListener {
		public void valueChanged(IValueModel.Event e);
	}

	// ValueModelの基本的なインタフェース
	public void setValue_(Object obj);
	public Object getValue_();

	// イベントリスナーの処理部分
	/**
	 * リスナーを追加する
	 * @param l	リスナー
	 */
	public void addListener(IValueModel.Listener l);
	/**
	 * リスナーを削除する
	 * @param l	リスナー
	 */
	public void removeListener(IValueModel.Listener l);

}// IValueModel

3.4.2 バリューモデルインタフェースの実装

次に値モデルのAbstractCountクラスを変更してモデルインタフェースを実装する。比較のために、AbstractCountクラスをAbstractValueCountクラスにコピーしてして作業をする。

AbstractCountへの修正は、

である。ソースの修正は、

public abstract class AbstaractValueCount
	implements ICount, IValueModel {
	protected Vector listeners_ = new Vector();

	public void addListener(IValueModel.Listener l) {
		listeners_.add(l);
	}

	public void removeListener(IValueModel.Listener l) {
		listeners_.removeElement(l);
	}

	protected void fireValueChanged (Object obj) {
		int size = listeners_.size();
		IValueModel.Event e = new IValueModel.Event(this, obj);

		for (int i = 0; i < size; i++) 
			((IValueModel.Listener)listeners_.elementAt(i)).valueChanged(e);
	}
			

リスト[AbstractValueCount.java]に完全なリスト示す。

比較のため、AbstractValueCountのサブクラスとしてIntegerValueCountを定義するが、内容はIntegerValueCountと同じです。

3.4.3 バリューモデルリスナーの実装

リスナーであるビューを修正する。比較のためにCountView.javaをコピーして、ValueCountView.javaとして変更する。

ビューの部分の変更は、バリューモデルのリスナーのアクションメソッドを定義する。モデル(model_)をアクションリスナーのvalueChangedメソッドから見えるようにprotectedに変更する。

	protected	ICount		model_;				// カウンタモデル
												// リスナーの生成
	protected	IValueModel.Listener modelListener_ =
		new IValueModel.Listener() {
			public void valueChanged (IValueModel.Event e) {
				valueBox.setText(e.getValue_().toString());
			}
		};
			

モデルの値が更新されてイベントがリスナーに届いたら、ここで定義したvalueChangedが呼び出される。最後に、ビューのインスタンス生成時にモデルのリスナーに自分自身を登録します。

		// リスナーの登録
		if (model instanceof IValueModel)
			((IValueModel)model).addListener(modelListener_);
			

後は、ObserverCountViewの場合と同様に、

			valueBox.setText(model_.toString());
			

を削除する。最終のソースは、リスト[ValueCountView.java]に示す。

以上のように、独自のバリューインタフェースを追加する方式もObserver, Observableを使った方式と比べ、ソースの修正量はそんなに多くならないことが分かる。Swingのコンポーネントには、MVCを使った部分が多々存在するのでこれらのモデルの実装について時間があれば検証してみたいと考えている。

3.5 最終のソース一覧

以下に、MVC対応のGUIへの完全なソースを示す。

リスト 3.5.1 AbstractCount.java
package model;

import java.util.Observable;

/**
 * カウントのアブストラクトクラス
 * 
 * @author Hiroshi TAKEMOTO
 * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $
 */
public abstract class AbstaractCount extends Observable 
	implements ICount {
	/**
	 * 現在の値
	 */
	private Object	value_ = null;
	/**
	 * リセット値
	 */
	private Object	resetValue_ = null;
	
	/**
	 * @see model.ICount#increment()
	 */
	public abstract void increment();

	/**
	 * @see model.ICount#decrement()
	 */
	public abstract void decrement();
	
	/**
	 * @see java.lang.Object#toString()
	 */
	public abstract String toString();
	
	/**
	 * @see model.ICount#reset()
	 */
	public void reset() {
		setValue_(getResetValue_());
	}
	/**
	 * Returns the resetValue_.
	 * @return Object
	 */
	protected Object getResetValue_() {
		return resetValue_;
	}

	/**
	 * Returns the value_.
	 * @return Object
	 */
	public Object getValue_() {
		return value_;
	}

	/**
	 * Sets the resetValue_.
	 * @param resetValue_ The resetValue_ to set
	 */
	protected void setResetValue_(Object resetValue_) {
		this.resetValue_ = resetValue_;
	}

	/**
	 * Sets the value_.
	 * @param value_ The value_ to set
	 */
	public void setValue_(Object value_) {
		this.value_ = value_;
		setChanged();
		notifyObservers();
	}
}
リスト 3.5.2 ObserverCountView.java
package observermvc;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.util.Observable;
import java.util.Observer;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JTextField;

import model.AbstaractCount;
import model.ICount;
import model.IntegerCount;

/**
 * CountView.java
 * 
 * @author Hiroshi TAKEMOTO
 * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $
 */

public class ObserverCountView extends JPanel 
	implements Observer {
	protected	JButton 	resetButton;		// リセットボタン
	protected	JButton 	incrementButton;	// incrementボタン(カウントアップ)
	protected	JButton 	decrementButton;	// decrementボタン(カウントダウン)
	protected	JTextField	valueBox;			// 値表示用テキストボックス
	private	ICount		model_;				// カウンタモデル
	
	/**
	 * CountViewコンストラクタ
	 * @param model 整数カウンタモデル
	 */
	public ObserverCountView (ICount model) {	
		JPanel panel = new JPanel(); // パネルを生成する
		model_ = model;              // モデルをビュー内部に保持する
		
		// リスナーの登録
		if (model instanceof Observable)
			((Observable)model).addObserver(this);
			
		// resetボタン用のアクション(コールバック)を生成する
		// この部分がJDK1.3で新しく導入された部分!
		Action	resetAction = new AbstractAction("reset") {
				// アクションを実行メソッド
			public void actionPerformed(ActionEvent evt)
			{
				// モデルをリセットし、その値をvalueBoxに表示する
				model_.reset();
			}
		};
		// アクションresetActionを持つリセットボタンを生成する
		// この部分がJDK1.3で新しく導入された部分!
		resetButton = new JButton(resetAction);
		// resetButtonをパネルに追加する
		panel.add(resetButton);

		// 同様にincrementボタンを生成
		Action	incrementAction = new AbstractAction("increment") {
			public void actionPerformed(ActionEvent evt)
			{
				model_.increment();
			}
		};
		incrementButton = new JButton(incrementAction);
		panel.add(incrementButton);

		// decrementボタンを生成
		Action	decrementAction = new AbstractAction("decrement") {
			public void actionPerformed(ActionEvent evt)
			{
				model_.decrement();
			}
		};
		decrementButton = new JButton(decrementAction);
		panel.add(decrementButton);

		// 値を表示するテキストボックスを生成する
		valueBox = new JTextField(model_.toString());
		// テキストボックスの値を編集できないよう属性を変更する
		valueBox.setEditable(false);

		// レイアウトをBorderLayoutに変更し、上部にボタンパネル、中央にテキストボックス
		// を配置する
		setLayout(new BorderLayout());
		add(valueBox, "Center");
		add(panel, "North");
	}
	
	/**
	 * @see java.util.Observer#update(Observable, Object)
	 */
	public void update(Observable o, Object arg) {
		if (o instanceof ICount)
			valueBox.setText(((ICount)o).toString());
		else
			valueBox.setText("");
	}
}// CountView
リスト 3.5.3 AbstractValueCount.java
package valuemodel;

import java.util.Observable;
import java.util.Vector;

import model.ICount;

/**
 * カウントのアブストラクトクラス
 * 
 * @author Hiroshi TAKEMOTO
 * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $
 */
public abstract class AbstaractValueCount
	implements ICount, IValueModel {
	/**
	 * リスナーを保持するためベクター
	 */
	protected Vector listeners_ = new Vector();
		
	/**
	 * 現在の値
	 */
	private Object	value_ = null;
	/**
	 * リセット値
	 */
	private Object	resetValue_ = null;

	/**
	 * @see valuemodel.IValueModel#addListener(Listener)
	 */
	public void addListener(IValueModel.Listener l) {
		listeners_.add(l);
	}

	/**
	 * @see valuemodel.IValueModel#removeListener(Listener)
	 */
	public void removeListener(IValueModel.Listener l) {
		listeners_.removeElement(l);
	}

	/**
	 * リスナーにモデルの値が変更したことを通知する
	 * @param obj	変更した値
	 */
	protected void fireValueChanged (Object obj) {
		int size = listeners_.size();
		IValueModel.Event e = new IValueModel.Event(this, obj);

		for (int i = 0; i < size; i++) 
			((IValueModel.Listener)listeners_.elementAt(i)).valueChanged(e);
	}
	
	/**
	 * @see model.ICount#increment()
	 */
	public abstract void increment();

	/**
	 * @see model.ICount#decrement()
	 */
	public abstract void decrement();
	
	/**
	 * @see java.lang.Object#toString()
	 */
	public abstract String toString();
	
	/**
	 * @see model.ICount#reset()
	 */
	public void reset() {
		setValue_(getResetValue_());
	}
	/**
	 * Returns the resetValue_.
	 * @return Object
	 */
	protected Object getResetValue_() {
		return resetValue_;
	}

	/**
	 * Returns the value_.
	 * @return Object
	 */
	public Object getValue_() {
		return value_;
	}

	/**
	 * Sets the resetValue_.
	 * @param resetValue_ The resetValue_ to set
	 */
	protected void setResetValue_(Object resetValue_) {
		this.resetValue_ = resetValue_;
	}

	/**
	 * Sets the value_.
	 * @param value_ The value_ to set
	 */
	public void setValue_(Object value_) {
		this.value_ = value_;
		fireValueChanged(value_);
	}
}
リスト 3.5.4 ValueCountView.java
package valuemvc;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JTextField;

import valuemodel.IValueModel;

import model.AbstaractCount;
import model.ICount;
import model.IntegerCount;

/**
 * CountView.java
 * 
 * @author Hiroshi TAKEMOTO
 * @version $Id: MVCwithEclipse_c3.html,v 1.2 2005/03/30 13:49:25 take Exp $
 */

public class ValueCountView extends JPanel {
	protected	JButton 	resetButton;		// リセットボタン
	protected	JButton 	incrementButton;	// incrementボタン(カウントアップ)
	protected	JButton 	decrementButton;	// decrementボタン(カウントダウン)
	protected	JTextField	valueBox;			// 値表示用テキストボックス

	protected	ICount		model_;				// カウンタモデル
												// リスナーの生成
	protected	IValueModel.Listener modelListener_ =
		new IValueModel.Listener() {
			public void valueChanged (IValueModel.Event e) {
				valueBox.setText(e.getValue_().toString());
			}
		};
	
	/**
	 * CountViewコンストラクタ
	 * @param model 整数カウンタモデル
	 */
	public ValueCountView (ICount model) {	
		JPanel panel = new JPanel(); // パネルを生成する
		model_ = model;              // モデルをビュー内部に保持する

		// リスナーの登録
		if (model instanceof IValueModel)
			((IValueModel)model).addListener(modelListener_);

		// resetボタン用のアクション(コールバック)を生成する
		// この部分がJDK1.3で新しく導入された部分!
		Action	resetAction = new AbstractAction("reset") {
				// アクションを実行メソッド
			public void actionPerformed(ActionEvent evt)
			{
				// モデルをリセットし、その値をvalueBoxに表示する
				model_.reset();
			}
		};
		// アクションresetActionを持つリセットボタンを生成する
		// この部分がJDK1.3で新しく導入された部分!
		resetButton = new JButton(resetAction);
		// resetButtonをパネルに追加する
		panel.add(resetButton);

		// 同様にincrementボタンを生成
		Action	incrementAction = new AbstractAction("increment") {
			public void actionPerformed(ActionEvent evt)
			{
				model_.increment();
			}
		};
		incrementButton = new JButton(incrementAction);
		panel.add(incrementButton);

		// decrementボタンを生成
		Action	decrementAction = new AbstractAction("decrement") {
			public void actionPerformed(ActionEvent evt)
			{
				model_.decrement();
			}
		};
		decrementButton = new JButton(decrementAction);
		panel.add(decrementButton);

		// 値を表示するテキストボックスを生成する
		valueBox = new JTextField(model_.toString());
		// テキストボックスの値を編集できないよう属性を変更する
		valueBox.setEditable(false);

		// レイアウトをBorderLayoutに変更し、上部にボタンパネル、中央にテキストボックス
		// を配置する
		setLayout(new BorderLayout());
		add(valueBox, "Center");
		add(panel, "North");
	}
}// CountView