Powered by SmartDoc

2次元グラフィックライブラリの作成

竹本 浩
http://www.pwv.co.jp
Javaのプログラムや最新の開発手法を拾得したい人のために、2次元グラフィックライブラリの作成の作成を通じて、XPやデザインパターンを取り入れながら実装する。

目次

Last modified: Sat May 04 13:27:48 JST 2002

1 はじめに

私は、ソフトウェア開発の仕事に従事しているが、年齢と伴に顧客との打ち合わせやグループの調整に時間をとられ、実際にプログラミングをする時間が全体の1割にも満たない。最近の技術動向を実践しようにもプログラムを書く時間は、週末と就寝前のわずかな時間に限られる。このページでは、ある程度のプログラム経験を持つプログラマーの方を対象に日曜プログラミングの世界へとお誘いする。

2 課題

簡単な2次元の図形エディタの作成を行い、MVCモデルに基づいたjavaアプリケーションでの開発を体験することを課題とする。

3 開発環境

日曜プログラマーを対象とするからには、開発環境もフリーで入手できるものを使用する。私の開発環境を以下に示す。

環境のインストールと設定については、別途章を設けて説明する。

参考文献

[1]ケイ・S. ホーストマン. コアJava2〈Vol.1〉基礎編. アスキー出版,
[2]ケイ・S. ホーストマン. コアJava2〈Vol.2〉応用編. アスキー出版,
[3]ジェームス・コスリン; ビル・ジョイ; ガイ・スティール. The Java言語仕様. アジソンウェスレイ,
[4]Joshua Bloch. Effective Java プログラミング言語ガイド. ピアソン・エデュケーション,
[5]エリック・ガンマ; リチャード ヘルム; ラルフ ジョンソン; ジョン ブリシディース. デザインパターン : オブジェクト指向における再利用のための. ソフトバンクパブリッシング,
[6]マーチン ファウラー; ケンドール スコット. UMLモデリングのエッセンス : 標準オブジェクトモデリング言語入門. 翔泳社,

javaを学習するにあって、参考にした文献を以下に示す。(1)

コアJava2[1][2]
かなり厚めの本で、基礎編のVol.1と応用編のVol.2の2巻がある。各章では、javaの基礎から応用までを丁寧に解説されており、例題もよく考えられて作られている。これからjavaを始める方にお薦めの一冊である。本に登場する例題は、付属のCD-ROMに含まれているので動作を確認しながら読み進むのがよい。
Java言語仕様[3]

javaの言語仕様を記述している本で、javaのdocにもこれのHTMLファイルが収録されているので、私はこれをAcrobatで1個のPDFファイルに変換し、分からないことがあったときに検索して参照している。

Webで見るなら、SunのWebPageにある言語仕様のページからHTML, PDFが提供されている。この他に、java言語仕様の日本語版を公開しているページもある。

Effective Javaプログラミング言語ガイド[4]
javaの言語仕様の他に知っておかなければならないものによいプログラムを書くための手引き書が、Effective Javaである。javaでプログラミングするために必要なイディオムを紹介している。javaで仕事をしようと考えている人には必須の一冊である。
デザインパターン[5]
オブジェクト指向プログラミングのデザインパターンと言えば、このGof本をさす。最近のjavaプログラミングではデザインパターンを無視してはよいプログラムが作れないと思えるほど、よく引用されている。付属のCD-ROMにはすべてのデザインパターンの丁寧な説明と用法、そしてjavaによる例題が入っている。これがとても便利であり、是非一般に解放して欲しいと思う作品である。
UMLモデリングのエッセンス[6]
デザインパターンと同様に最近よくでてくるのがUMLである。UMLモデリングのエッセンスでは、UMLの書き方だけではなく、どのように使うかを適切に解説してある。これをよむとUMLの分厚い本を読む必要性を感じられなくなる。そしてすべてのケースをUMLで記述しようとは思わなくなる。
  1. 他にもjavaプログラムクイックリファレンスがあるが、CoreJavaを持っていればそれで十分なので省略した。また、javaによるパーサ構築技法は、独自の言語をjavaで作ってみたい人にはお勧めの本である。

4 2次元図形エディタの仕様

顧客に代わって2次元図形エディタの仕様を決めます。XPでは顧客=ユーザも開発チームの一員ですので、一人でユーザ、開発者を兼用します。以下ではどの立場で検討しているのか示すために、節を分けて記述することにする。

4.1 ユーザの立場

ユーザも一括して図形エディタができるとは思っていないので、できたものを使いながら、仕様の追加・変更を行うことになる。最初の仕様としては、最低限以下の機能を持つものを作成する。

  1. 2次元の簡単な図形(四角、円、直線、フリーハンド曲線)を表示し、編集(移動、変形、追加、削除)ができる。
  2. 作成図形を保存し、再度読み込むことができる。

追加の機能としては、

があるとよいだろう。

その他に

も欲しい機能である。

4.2 開発者のシナリオ

ユーザの仕様に対して、開発者としてどの機能をどのような順序で実現するかをもう少し具体的なシナリオとして記述する。できる限り、最初の段階から動作して動きが確かめられる方が、開発者の志気も高まるし、ユーザが仕様を確認しながら作業を進めることができる。

  1. MVCモデルを使って問題領域とGUI領域に分けて作成する。
  2. 固定の基本図形(四角、円、直線)をロードして表示する。
  3. カーソルツールによって図形の選択、移動ができる。
  4. カーソルツールによって図形の変形ができる。
  5. 図形ツールによって新規図形を作成できる。
  6. 図形のカット、コピー、ペーストができる。
  7. 図形の保存とロード(読み込み)ができる。

5 全体の構成

MVCモデルを使って問題領域とGUI領域を切り分け、テストが容易になるようにMartin Fawler氏の著書UML Distilled: Applying the Standard Object Modeling Language(Application Facadesには、例題の完全なプログラムが紹介されている)の例題に沿って考えてみる。

問題領域はE2DShapeクラス、ビューはE2DViewクラス、E2DShapeとE2DViewの間で様々な中継処理をするのが、E2DFacedeクラスから構成される。

6 問題領域

問題領域では、2次元の図形エディタの図形構造情報(モデル)のことである。基本図形である矩形、円、直線、自由曲線、文字列の表現方法と、これらを組み合わせた構造をどのように表現するか検討する。

最初に、代表的な図形である「矩形」と「文字列」の表現方法を検討する。(2)

  1. 全く異なった性質の図形を最初に扱うことによって表現方式の問題点を洗い出すのが目的である

6.1 E2DShape

最初に、図形情報を保持するクラスのルートクトクラスとして、E2DShapeを作成する。E2DShape以下のクラス構成を図[E2DShapeクラス構成]に示す。

図 6.1.1 E2DShapeクラス構成

技術的なポイントは、図形情報を頂点と変換マトリックス(3)で構成することにある。

プログラミング的なポイントとしては、属性pionts、transMatは、プライベートとして宣言し、属性へのgetメソッドはpublicとし、setメソッドはprotectedとして定義する。これによって属性の安全性を高める。

クラスの単体テストを行なうにあたって、重要なのがtoStringである。E2DSahpeでは、グローバル座標(4)で出力することにする。

// package E2DLib;
import java.awt.geom.*;
import java.lang.String;

/**
 * E2DShape.java
 * 問題領域における図形のルートクラス
 *
 * Created: Sun Feb 10 12:54:51 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version
 */

public class E2DShape {
    private Point2D.Double[] _points;
    private AffineTransform _transMat;
    private boolean _selected = false;

    public E2DShape() {
	_points = new Point2D.Double[0];   // 空の配列を作成する
	_transMat = new AffineTransform(); // 単位マトリックス
    }
    public Point2D.Double[] getPoints() {
	return ((Point2D.Double[])_points.clone());
    }
    public AffineTransform getTransMat() {
	return ((AffineTransform)_transMat.clone());
    }
    protected void setPoints(Point2D.Double[] points) {

	_points = points;
    }
    protected void setTransMat(AffineTransform matrix) {
	_transMat = matrix;
    }
    public String toString() {
	String s = new String(getClass().getName() + ": points = [");
	int size = _points.length;
	if (size > 0)
	    s = s + _transMat.transform(_points[0], null).toString();
	for (int i = 1; i < size; i++)
	    s = s + ", " + _transMat.transform(_points[i], null).toString();
	return (s +  "], " +
		"transMat = " + _transMat.toString());
    }
}// E2DShape
  1. グローバル座標から図形のローカル座標への変換マトリックスをAffineTransformのインスタンスとして保持する。
  2. 各頂点に変換マトリックスを適応した値がグローバル座標である。

6.2 E2DRect

最初に、矩形クラス(E2DRect)をE2DShapeのサブクラスとして定義する。E2DRectは、固定の4つの頂点の座標を持ち、変換マトリックスを操作することによって、矩形を移動したり、回転等の変形を加えることができる。構造的には非常に単純なものである。

// package E2DLib;
import java.awt.geom.*;
import java.lang.String;

/**
 * E2DRect.java
 * 問題領域における矩形クラス
 *
 * Created: Thu Feb 14 15:56:15 2002
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version 1.0
 */

public class E2DRect extends E2DShape {
    /**
     * E2DRect
     * 問題領域における矩形オブジェクトを生成する。<p>
     * 
     * @param top    矩形の左上のY座標
     * @param left   矩形の左上のX座標
     * @param width  矩形の幅(X方向の長さ)
     * @param height 矩形の高さ(Y方向の長さ)
     */
    public E2DRect(double top,
		   double left, 
		   double width,
		   double height) { 
	/**
	 * ローカル座標では、左上の頂点を原点とし、4頂点の
	 * 座標を保持している
	 */
	Point2D.Double[] points = new Point2D.Double[] {
	    new Point2D.Double(0, 0),
	    new Point2D.Double(0, height),
	    new Point2D.Double(width, height),
	    new Point2D.Double(width, 0)
		};	
	setPoints(points);
	/**
	 * 平行移動によってグローバル座標に変換している
	 */
	AffineTransform m = new AffineTransform();
	m.translate(left, top);
	setTransMat(m);
    }
}// E2DRect

6.3 E2DString

次に、文字列クラス(E2DString)をE2DShapeのサブクラスとして定義する。文字列という矩形とはまったく異なったオブジェクトクラスをクラス設計の初期に導入することによって、クラス構成やメソッドの問題点を早期に検出することが期待できる。

ここでも文字列の左上点を原点とする頂点1個と文字列だけを持ち、E2DShapeで定義された変換マトリックスで文字列を操作することになる。

// package E2DLib;
import java.awt.geom.*;
import java.lang.String;

/**
 * E2DString.java
 * 問題領域における文字列クラス
 *
 * Created: Fri Mar 01 12:59:49 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version
 */

public class E2DString extends E2DShape {
    /**
     * E2DString 
     * 問題領域における文字列オブジェクトを生成する<p>
     *
     * @param str 文字列
     * @param x   表示位置のX座標
     * @param y   表示位置のY座標
     */
    public E2DString (String str, double x, double y)
    {
	Point2D.Double[] points = new Point2D.Double[] {
	    new Point2D.Double(x, y)
		};
	setPoints(points);
	AffineTransform m = new AffineTransform();
	setTransMat(m);
	_str = new String(str);
    }
    /**
     * toString
     * 文字列オブジェクトを文字列として表現する
     */
    public String toString() 
    {
	return (super.toString() + ", _str = " + _str);
    }
    /*
     * getString
     * 文字列を返す。
     */
    public String getString() { return (_str); }
    private String _str;
}// E2DString

6.4 単体テスト

私の開発環境JDEでは、beanShellというjavaのインタプリタが附属しており、mainプログラムを容易しなくても作成したクラスをテストすることができる。

bsh % r = new E2DRect(10.0, 20.0, 30.0, 40.0);
bsh % print(r);
E2DRect: points = [Point2D.Double[20.0, 10.0], Point2D.Double[20.0, 50.0], Point2D.Double[50.0, 50.0], Point2D.Double[50.0, 10.0]], transMat = AffineTransform[[30.0, 0.0, 20.0], [0.0, 40.0, 10.0]]
bsh % 

このようにbeanShellの中でインスタンスを生成してその値を出力してみたり、メソッドを呼びだしたりすることができる。

7 ビューワの作成

問題領域の基本クラスができたので、グラフィックに表示してみよう。単体テストだけだと見栄えもよくないので、志気も高まらないのでグラフィック表示用クラスを作成する。

7.1 グラフィック表示の仕様

最初に、グラフィック表示の仕様を設定します。

図形を表示している状態を「ディスプレイ状態」、カーソルツールによって選択され編集可能な状態を「選択状態」と区別します。

各状態での矩形と文字列の表示形式を以下に示す。

矩形と文字列の表示例を図[ディスプレイ例]に示す。

図 7.1.1 ディスプレイ例

7.1 DisplayShapeクラス

グラフィック表示用オブジェクトとしてDisplayShapeクラスを用意する。DisplayShapeはインスタンスを持たないファクトリクラスであり、そのサブクラスで実際の図形を定義し、そのインスタンスの生成メソッドを提供する。また、DipslayShapeの持つ共通のメソッドもDisplayShapeで定義する。

// package E2DLib;
import java.awt.geom.*;
import java.lang.String;
import java.awt.*;
import java.awt.font.*;
import java.util.*;

/**
 * DisplayShape.java
 *
 *
 * Created: Fri Feb 15 01:27:37 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version 1.00
 */

public class DisplayShape
{
    protected DisplayShape ()
	{
		// コンストラクタを持たない
    }
	public static DisplayShape createShape(E2DShape shape)
	{
		return (new DisplayShape());
	}
	public static DisplayShape createRect(E2DShape shape)
	{
		return (new DisplayRect(shape));
	}
	public static DisplayShape createString(E2DShape shape)
	{
		return (new DisplayString(shape));
	}
    public AffineTransform getMatrix() { return (matrix); }
    public Object getShape() { return (shape); }    
    public void draw(Graphics2D g2)
    {
		AffineTransform t = g2.getTransform();
		g2.transform(matrix);
		g2.draw((Shape)shape);
		g2.setTransform(t);
    }

    public String toString() {
		String s = new String(getClass().getName() + ": ");
		s = s + "shape = " + shape.toString()+ ", " +
	        "matrix = [" + matrix.toString()+ "]";
        return (s);
    }

    protected Object shape;
    protected AffineTransform matrix;
}// DisplayShape

class DisplayRect extends DisplayShape
{
	DisplayRect(E2DShape e2dShape)
	{
		Point2D.Double[] points = e2dShape.getPoints();
		Rectangle2D.Double r = new Rectangle2D.Double();
		r.setFrameFromDiagonal(points[0], points[2]);
		shape = r;
		matrix = e2dShape.getTransMat();
    }
}

class DisplayString extends DisplayShape 
{
	DisplayString(E2DShape e2dShape) 
	{
   		Point2D.Double[] points = e2dShape.getPoints();
		matrix = e2dShape.getTransMat();
		_string = ((E2DString)e2dShape).getString();

		shape = null;
		_layout = null;
		matrix.translate(points[0].getX(), points[0].getY());
   	}
    public void draw(Graphics2D g2)
    {
		if (_layout == null) {
			FontRenderContext context = g2.getFontRenderContext();
			Font f = g2.getFont();
			_layout = new TextLayout(_string, f, context);
			Rectangle2D bounds = _layout.getBounds();
			bounds.setRect(bounds.getX() - 2,
						   bounds.getY() - 2,
						   bounds.getWidth() + 3,
						   bounds.getHeight() + 2);
			shape = bounds;

		}

		AffineTransform t = g2.getTransform();
		g2.transform(matrix);
		_layout.draw(g2, 0, 0);
		g2.draw((Shape)shape);
		g2.setTransform(t);
    }
	private TextLayout _layout;
	private String _string;
}

矩形を表示するDisplayRectは、単にRectangle2Dにマッピングしているだけであるが、文字列を表示するDisplayStringは少し異なる。DisplayString生成時には、表示位置と変換マトリックスのみをセットし、表示メソッドdrawで文字列情報を保持するTextLayoutオブジェクトと文字列を囲む矩形をそれぞれ、_layout、shapeメンバ変数にセットする。これは、フォント情報を取り出すためにg2が必要だからである。

7.2 E2DShapeとDisplayShape間のマピング

最初にE2DShapeを作成したときには、E2DShapeにグラフィック表示用オブジェクトを生成するメソッドを含めた。しかし純粋な問題領域のクラスであるE2DShapeにGUI関係のメソッドを含めることは正しくない。そこで、E2DShapeとDisplayShapeをマッピングするクラスを用意する。E2DRect、E2DStringといった異なる図形を生成する場合、メソッドの呼び出し形式が異なるために、どのクラスのインスタンスなのか調べてからDisplayShapeインスタンスを生成しなればならない。

そこで、Visitorパターンを使うことにする。Gof本によるとVisitorパターンの目的は、

あるオブジェクト構造上の要素で実行されるオペレーションを表現する。Visitorパターンにより、オペレーションを加えるオブジェクトのクラスに変更を加えずに、新しいオペレーションを定義することができるようになる。

とある。Gof本で紹介されているVisitorパターンを使用する場面の内、

オブジェクト構造にインタフェースが異なる多くのクラスのオブジェクトが存在し、これらのオブジェクトに対して、各クラスで別々に定義されているオペレーションを実行したい場合。

が、今回Visitorパターンを使用する理由である。

Visitorパターンの構造は、訪問者Visitorと訪問されるNodeから構成される。

このパターンでは、NodeがAcceptメソッドを提供することによって引数として渡されたVisitorのノードに対応したメソッドvisitConcreateElementを呼びだすことになる。

これをE2DShapeとDisplayShape間のマピングに適応すると次のような構造になる。

E2DShapeにAcceptメソッドを追加し、E2DVisitorにVisitorの概念クラスを作成し、実装クラスのインスタンスは、createDisplayVisitorメソッドを呼び出して生成することにした。

E2DVisitor.javaは、次のようになる。

// package E2DLib;
import java.util.*;

/**
 * E2DVisitor.java
 *
 *
 * Created: Thu Mar 07 14:46:41 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version
 */

abstract public class E2DVisitor 
{
	protected E2DVisitor ()
	{
		// インスタンスを生成しない
	}
	public static E2DVisitor createDisplayVisitor()
	{
		return (new DisplayVisitor());
	}
	public List getList() { return (list); }
	abstract public void visitShape(E2DShape shape);
	abstract public void visitRect(E2DRect shape);
	abstract public void visitString(E2DString shape);
	protected List list;
}// E2DVisitor

class DisplayVisitor extends E2DVisitor
{
	DisplayVisitor() 
	{
		list = new Vector();
	}
	public void visitShape(E2DShape shape) 
	{
		// 何も生成しない
	}
	public void visitRect(E2DRect shape) 
	{
		list.add(DisplayShape.createRect(shape));
	}
	public void visitString(E2DString shape) 
	{
		list.add(DisplayShape.createString(shape));
	}
}

7.3 グラフィックマッパー

Visitorができたので、次はE2DShapeとDisplayShape間のマピングを行うグラフィックマッパーGraphicMapperを作成する。GraphicMapperの構造は、きわめて単純で、初期化で作成したE2DShapeの配列から、E2DVisitorを使ってグラフィックオブジェクトを生成し、これを配列に変換してから、displayShapesに保持するだけである。

プログラミング上のテックニック

E2DVisitorによって生成されたグラフィックオブジェクトは、Vectorに保持されたリスト(List)なので、これを配列に変換するために、toArrayメソッドを使用した。toArrayは引数なしの呼び出し形式もあるが、実際に使用するには引数に実際に配列に保持されるオブジェクトの配列を渡して、オブジェクトのサイズを指定する必要がある。

_displayShapes =(DisplayShape[])v.getList().toArray(new DisplayShape[1]);

がその部分である。

GraphicMapper.javaの全ソースは、次のようになる。

// package E2DLib;
import java.awt.geom.*;
import java.util.*;

/**
 * GraphicMapper.java
 *
 *
 * Created: Tue Feb 26 18:45:27 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version
 */

public class GraphicMapper
{
    private E2DShape[] _shapes;
    private DisplayShape[] _displayShapes;

    public GraphicMapper ()
    {
		_shapes = new E2DShape[] {
			new E2DRect(25.0, 25.0, 15.0, 15.0),
			new E2DRect(50.0, 100.0, 15.0, 25.0),
			new E2DString("AbcdefgH", 150, 200, 14)
		};
		makeDisplayShapes();
    }
	public DisplayShape[] getDisplayShapes() { return (_displayShapes); }

    private void makeDisplayShapes()
    {
		E2DVisitor v = E2DVisitor.createDisplayVisitor();

		for (int i = 0; i < _shapes.length; i++)
			_shapes[i].Accept(v);
		_displayShapes =(DisplayShape[])v.getList().toArray(new DisplayShape[1]);

    }
}

7.4 E2DView

グラフィック表示の最後は、ビューの作成である。通常のswingプログラムの方式に従い、Jpanelのサブクラスとして、E2DViewクラスを作成する。E2DViewには、モデル(グラフィックマッパーがモデルになる)とグラフィックオブジェクトを保持する属性を持つ。

ビューの内容を表示するメソッドはpaintComponetであり、ここでグラフィックオブジェクトを描画する。

E2DView.javaの全ソースを以下に示す。

//package E2DLib;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import java.util.*;

/**
 * E2DView.java
 * E2DLib用ビューの作成。
 *
 * Created: Sat Feb 16 15:41:02 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version
 */

public class E2DView extends JPanel
{
    private DisplayShape[] _shapes;
    private GraphicMapper _model;

    public E2DView(GraphicMapper model)
    {
		_model = model;
		_shapes = _model.getDisplayShapes();
    }

    public void paintComponent(Graphics g)
    {  
		super.paintComponent(g);
		Graphics2D g2 = (Graphics2D)g;
	
		int size = _shapes.length;
		for (int i = 0; i < size; i++) {
			_shapes[i].draw(g2);
		}
    }
    public String toString()
    {
		String s = new String("E2DPanel: shapes = ");
		for (int i = 0; i < _shapes.length; i++)
			s = s + "," + _shapes[i].toString();
		return (s);
    }
}//E2DView

7.5 メイン関数

最後にメイン関数(E2DLib)を作成する。これから作成するプログラムをWebでも表示できるようにAppletの機能を含む形式で作成した。(5)実際のメイン関数機能は、initメソッドに記述してある。

E2DLib.javaの全ソースを以下に示す。

//package E2DLib;

import javax.swing.*;
import java.awt.event.*;

/**
 * E2DLib.java
 *
 *
 * Created: Sun Feb 17 23:32:56 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version
 */
// import java.applet.*; // Applet用のクラス

public class E2DLib extends JApplet
{
	public void init() {
		E2DFacade facade = new E2DFacade();
		E2DView view = new E2DView(facade.getModel());
		getContentPane().add(view);
	}
		
	public static void main(String[] args)
	{
		JFrame frame = new JFrame("E2DLibDemo");
		frame.addWindowListener(
			new WindowAdapter() {
				public void windowClosing(WindowEvent evt) {
					System.exit(0);
				}
			}
		);
		JApplet applet = new E2DLib();
		applet.init();
		frame.getContentPane().add(applet);
		frame.pack();
		frame.setSize(300, 300);
		frame.setVisible(true);
		applet.start();
	}

//      public static void main(String[] args) {
//  	E2DFacade facade = new E2DFacade();
//  	JFrame frame = new E2DView(facade.getModel());
//  	frame.show();
//     }
}// E2DLib
  1. この方式は、CoreJavaで紹介されている方法である。

8 モデル化

MVCでは、モデルの更新をビューやコントローラに通知するために、Observerパターンを使用している。

8.1 Observerパターン

プログラムの振る舞いを問題領域に集中するために、プログラムの構造をMVCモデルに適応するようにObserverパターンを追加する。

Gof本によると、Oberverパターンの目的は、

あるオブジェクトが状態を変えたときに、それに依存するすべてのオブジェクトに自動的にそのことが知らされ、また、それらが更新されるように、オブジェクト間に一対多の依存関係を定義する。

とあります。

Observerパターンのjavaにおける実装は、非監視対象(モデル)のクラスObservableと監視オブジェクトが非監視対象の変更通知を受けるためにインタフェースObserverから構成される。

javaにおけるObserverパターンの構造を以下に示す。

図 8.1.1 Observerパターン構造

この機能によってMVCモデルでは、ビューの実装や更新を気にすることなく、モデルの状態を変更し、問題を解決することができる。

まず、モデルとなるGraphicMapperをJavaのモデルクラスObservableのサブクラスに変更する。

public class GraphicMapper extends Observable

テストのために、移動、回転、拡大縮小、選択、非選択のオペレーションをGraphicMapperに追加する。

	public void select(int n) 
	{
		_shapes[n].select();
	}
	public void deSelect(int n)
	{
		_shapes[n].deSelect();
	}
	public void translate(double dx, double dy)
	{
		for (int i = 0; i < _shapes.length; i++)
			if (_shapes[i].isSelected())
				_shapes[i].translate(dx, dy);
		makeDisplayShapes();
	}
	public void scale(double sx, double sy)
	{
		AffineTransform s = AffineTransform.getScaleInstance(sx,sy);
		transform(s);
	}
	public void rotate(double th)
	{
		AffineTransform r = AffineTransform.getRotateInstance(th);
		transform(r);
	}
	// 変換マトリックスの結合
	public void transform(AffineTransform m)
	{
		for (int i = 0; i < _shapes.length; i++)
			if (_shapes[i].isSelected())
				_shapes[i].transform(m);
		makeDisplayShapes();
	}

同様にE2DShapeにも同様のオペレーションを追加する。

    public boolean isSelected()
    {
	return (_selected);
    }
    public void select()
    {
	_selected = true;
    }
    public void deSelect()
    {
	_selected = false;
    }
    public void scale(double sx, double sy) 
    {
	_transMat.scale(sx, sy);
    }
    public void translate(double dx, double dy)
    {
	_transMat.translate(dx, dy);
    }
    public void rotate(double th)
    {
	_transMat.rotate(th);
    }

次に、ビューE2DViewにインタフェースObserverを実装するために、updateメソッドを追加する。

(6)

    public void update(Observable o, Object arg) 
    {
		_shapes = _model.getDisplayShapes();
		repaint();
    }

これでMVCモデルのモデルとビューの組み込みが完了した。

  1. 初期のバージョンでは、repaint();が抜けていたために、画面の更新ができなかった。

8.2 MVCの単体テスト

JDEのbeanshell上で単体で起動してみる。以下のスクリプトを作成して、

リスト 8.2.1 MVC単体テスト用bshスクリプト
import javax.swing.*;
import java.awt.event.*;
// モデルを作成
E2DFacade facade = new E2DFacade();

// フレームとコンテナパネルを作成する
JFrame f = new JFrame("GUI Model Test");
E2DView p = new E2DView(facade.getModel());

f.getContentPane().add(p);
f.pack();
f.setSize(300, 300);
// フレームの表示
f.show();

beanshell上で、source("GuiModel.bsh");と起動すると前回実施したのと同じ画面が表示する。ここからがMVCモデル化によって強化された部分である。beanshell上は、インタプリタで動作しているので、javaの文をインタラクティブに実行できる。まず、facadeからモデルを取得し、文字列を選択して、22.5度ずつ回転し、(-100,-100)平行移動してみる。コンソールでのコマンドは、次のようになる。

bsh % source("GuiModel.bsh");
bsh % m = facade.getModel();
bsh % m.select(2);
bsh % pi = Math.PI;
bsh % m.rotate(pi/8);
bsh % m.translate(-100, -100);

モデルへのオペレーションを行っただけで、画面上で文字列が回転し、

移動する。このようにプログラムの構造をMVCモデルに適応すると、単体テスト動作の確認ができ、プログラムミングを問題領域であるモデルに集中することができる。

9 コマンドオブジェクト

モデルにオペレーションを追加したので、コマンドパターンに従って実施するコマンドオブジェクトを定義する。

9.1 コマンドパターン

コマンドパターンは、オブジェクトに対する要求をコマンドオブジェクトにカブセル化することによって、undoやredoをサポートする。

コマンドパターンを使う理由は、コマンドパターンの適応範囲の内、

実行する動作によりオブジェクトをパラメータ化したい場合。手続き型言語では、そのようなパラメータ化をコールバック関数を使って表現する。すなわち、コールバック関数を、呼び出してほしいところに登録しておく、という形になる。Commandパターンでは、そのようなコールバック関数の代わりにオブジェクトを使う。

ためである。

オブジェクトパターンでは、図[コマンドパターンシーケンス]のようにコマンドクラスとレシーバの他にインボーカが存在し、クライアントがコマンドオブジェクトを生成し、インボーカに渡しておき、インボーカが必要な時にコマンドオブジェクトに実行を依頼するパターンになっている。

図 9.1.1 コマンドパターンシーケンス

9.2 E2DCmd

コマンドの概念クラスとして、E2DCmdを定義する。E2DCmdは、javaのAbstractActionクラスのサブクラスとし、生成時にレシーバをセットする。E2DCmdでは、オペレーションメソッドとしてコマンドの実行を依頼するDoItメソッド提供する。

リスト 9.2.1 E2DCmd
// package E2DLib;
import javax.swing.*;
import java.awt.geom.*;
import java.awt.event.*;

/**
 * E2DCmd.java
 *
 *
 * Created: Fri Mar 15 01:41:22 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version
 */

abstract public class E2DCmd extends AbstractAction {
	public E2DCmd (String name, 
				   Icon icon, 
				   GraphicMapper model)
	{
		putValue(Action.NAME, name);
		putValue(Action.SMALL_ICON, icon);
		target = model;
	}
	public void DoIt()
	{
		actionPerformed(
			new ActionEvent(this, 
							ActionEvent.ACTION_PERFORMED, 
							(String)getValue(Action.NAME)));
	}
	abstract public void actionPerformed(ActionEvent evt);
	protected GraphicMapper target;		
}// E2DCmd

9.3 SelectCmd

E2DCmdの具象クラスとしてオブジェクトの選択を行うコマンドクラスを作成する。SelectCmdでは、コマンド実行するために選択範囲(select)をパラメータとする。選択範囲(矩形)はRectangle2Dオブジェクトをselectメソッドでコマンド実行前にセットする。(7)

リスト 9.3.1 E2DCmd
// package E2DLib;
import javax.swing.*;
import java.awt.geom.*;
import java.awt.event.*;

/**
 * SelectCmd.java
 *
 *
 * Created: Fri Mar 15 02:19:14 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version
 */

public class SelectCmd extends E2DCmd {
	public SelectCmd (String name, Icon icon, 
					  Rectangle2D selection, GraphicMapper model){
		super(name, icon, model);
		putValue("selection", selection);
	}
	public void actionPerformed(ActionEvent evt)
	{
		Rectangle2D s = (Rectangle2D)getValue("selection");
		target.select(s);
	}
	public void select(Rectangle2D selection)
	{
		putValue("selection", selection);			
	}
}// SelectCmd
  1. SelectCmdは、CmdPoolから取り出し、キャストをSelectCmdにキャストをするために、publicで宣言した。これが、問題!。

    本来はputValueですべて実施すべきなので?あるいはE2DCmdにパラメータ 設定メソッドを集中してもよいのではないか?そうすればSelectCmdを E2DCmd.javaでprotectedで宣言できてよい。 また、この解法にはCmdPoolにコマンドオブジェクトを登録する場合に 各コマンドクラスを匿名クラスで定義できるというメリットもある。

9.4 Junit

E2DShapeにメソッドを追加したところで、Junitを使った単体テストを実施してみる。

10 図形の選択

図形の選択に必要な問題領域のメソッド、コマンドオブジェクトの定義ができたので、GUI周りの処理を実装してみる。

10.1 図形の選択方式

矩形の選択をどのように行なうかは、図形表示クラスで提供されているメソッドに依存する。javaのShapeクラスのcontainメソッドは非常に優れているが、図形が回転した場合に正しく選択することができない。

図 10.1.1 回転した図形の選択

そこで図形の選択方式として、

図形または図形を包括する矩形の頂点の内1個でも選択範囲の矩形に含まれる場合に、「選択」される

ことにする。

10.2 カーソルツール

カーソルツールはマウスのイベントを処理する特別なグラフィックオブジェクトである。マウスボタンが押されてから離されるでその範囲を点線の矩形(ラバーバウンド)で表示する(図[ラバーバウンド]参照)。

図 10.2.1 ラバーバウンド

そして矩形内にある図形を選択するために、SelectCmdオブジェクトのDoItを実行する。

カーソルツールの生成時には、ラバーバンドを描画するときの波線のストローク(線種)と空の矩形オブジェクトを生成する。描画メソッドではdrawでは、ストロークの切り替えとマウスダウン点とカレントマウスポイントからなる矩形(Rectangle2D)を描画する。mouseXXXXのように"mouse"で始まるメソッドは、MouseListener, MouseMotionListenerインタフェース関連のメソッドであり、次節で説明するマウスイベントの委譲を処理する。

リスト 10.2.1 E2DCmd
// package E2DLib;
import java.awt.geom.*;
import java.lang.String;
import java.awt.event.*;
import java.awt.*;

/**
 * CursorTool.java
 * 描画画面上でのカーソルツールのマウス動作に対するアクションを
 * 規定するクラス。
 *
 * 通常のドロー系のエディタのカーソルツールを参考にした。
 *
 * Created: Thu Feb 21 06:56:12 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version
 */

public class CursorTool 
    implements MouseListener, MouseMotionListener
{
    public CursorTool (GraphicMapper model)
    {
	_model = model;
	_selectShape = new Rectangle2D.Double();
	float miterLimit = 2*DASH_LEN;
        float[] dashPattern = {DASH_LEN, DASH_LEN };
	_stroke = new BasicStroke(1F, 
				  BasicStroke.CAP_BUTT, 
				  BasicStroke.JOIN_BEVEL,
				  miterLimit, dashPattern,
				  10F);
    }
    public void draw(Graphics2D g2)
    {
	Stroke stroke = g2.getStroke();
	g2.setStroke(_stroke);
	g2.draw(_selectShape);
	g2.setStroke(_stroke);
    }
    public Shape getToolShape() 
    {
	return (_selectShape);
    }
    public void mousePressed(MouseEvent event)
    {  
	_pressPoint = event.getPoint();

	EditableShape[] editable = _model.getEditableShapes();
	if (editable == null)
	    return;
	for (int i = 0; i < editable.length; i++) {
	    if (editable[i].contains(_pressPoint)) {
		_dragged = editable[i];
		_dragged.mousePressed(event);
		return;
	    }
	}
    }

    public void mouseReleased(MouseEvent event)
    {
	if (_dragged != null) {
	    _dragged.mouseReleased(event);
	    _dragged = null;
	}
	else {
	    _selectShape.setFrameFromDiagonal(_pressPoint, 
					      event.getPoint());
	    SelectCmd cmd = (SelectCmd)
		CmdPool.getCmd(CmdPool.SELECT_CMD);
	    cmd.select(_selectShape);
	    cmd.DoIt();
	    // セレクト領域をクリアする代わりに空の矩形を作る
	    _selectShape = new Rectangle2D.Double();
	}
    }

    public void mouseEntered(MouseEvent event)
    {
    }

    public void mouseExited(MouseEvent event)
    {
    }

    public void mouseClicked(MouseEvent event)
    {
    }

    public void mouseMoved(MouseEvent event)
    {
    }

    public void mouseDragged(MouseEvent event)
    { 
	if (_dragged != null)
	    _dragged.mouseDragged(event);
	else
	    _selectShape.setFrameFromDiagonal(_pressPoint, 
					      event.getPoint());
    }
    private GraphicMapper _model;
    private EditableShape _dragged = null;
    private Point2D _pressPoint;
    private Rectangle2D.Double _selectShape;
    private BasicStroke _stroke;
    private static float DASH_LEN = 5F;
    public String toString() 
    {
	return ("Cursor: _pressPoint =" + _pressPoint.toString() +
		"_selectTool =" + _selectShape.toString());
    }
}// CursorTool

10.3 マウスイベントの委譲

現段階では、マウスイベントを処理するツールは、カーソルツールだけであるが、これからオブジェクト生成ツールや、他のオペレーションを処理するツールが必要となる。そのため、マウスイベントの内ツールに処理を依頼する部分をMouseListener, MouseMotionListenerインタフェースを定義することによってツールに委譲する。これは、ブリッジパターンと呼ばれ、Gof本[5]の適応可能性の内、

抽出されたクラスとその実装を永続的に結合することを避けたい場合。たとえば、実装を実行時に選択したり交換したりしなければならないときに、このような場合が起こり得る。

がこのパターンを選択した理由となる。

10.4 コマンドプール

コマンドを実行するインボーカ(DisplayShape)が、ターゲットであるレシーバを知らないことが問題となった。そこでコマンドをオブジェクトプールしておくクラスCmdPoolを導入した。これによってコマンドオブジェクトの再利用も可能となった。

コマンドプールにはコマンドを保持するハッシュテーブルとコマンド名のリテラルが定義されている。

リスト 10.4.1 CmdPool
// package E2DLib;
import java.lang.*;
import java.util.*;
/**
 * CmdPool.java
 *
 *
 * Created: Thu Mar 14 19:44:38 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version
 */

public class CmdPool {
	protected CmdPool (){
		// インスタンスを生成しない
	}
	public static void setCmd(String name, Object cmd)
	{
		_map.put(name, cmd);
	}
	public static Object getCmd(String name)
	{
		return (_map.get(name));
	}
	private static HashMap _map = new HashMap();
	public static final String TRANSFORM_CMD = "transform";
	public static final String TRANSLATE_CMD = "translate";
	public static final String SELECT_CMD = "select";
}// CmdPool

10.5 EditableShape

図形選択の最後の処理は、選択された図形が選択状態であることを表示することである。DisplayShapeと同様に選択された図形(編集可能な図形)EditableShapeを定義する。

リスト 10.5.1 EditableShape
// package E2DLib;
import java.awt.geom.*;
import java.lang.String;
import java.awt.*;
import java.awt.font.*;
import java.util.*;
import java.awt.event.*;

/**
 * EditableShape.java
 *
 *
 * Created: Thu Mar 14 22:16:41 2002
 *
 * @author <a href="mailto:take@pwv.co.jp">Hiroshi TAKEMOTO</a>
 * @version
 */

public class EditableShape extends DisplayShape
    implements MouseListener, MouseMotionListener
{
    protected Point2D.Double[] points;
    protected Point2D pressPoint;
    protected Rectangle2D.Double[] pointRects;
    protected static int SIZE = 8;

    protected EditableShape (){
	// コンストラクタを持たない
    }
    public static DisplayShape createShape(E2DShape shape)
    {
	return (new EditableShape());
    }
    public static DisplayShape createRect(E2DShape shape)
    {
	return (new EditableRect(shape));
    }
    public static DisplayShape createString(E2DShape shape)
    {
	return (new EditableString(shape));
    }
    public void draw(Graphics2D g2) {
	AffineTransform t = g2.getTransform();
	AffineTransform m = new AffineTransform(transMat);
	m.concatenate(matrix);
	g2.transform(m);
	g2.draw((Shape)shape);
	for (int i = 0; i < pointRects.length; i++)
	    g2.fill(pointRects[i]);
	g2.setTransform(t);
    }
    public Point2D globalToLocal(Point2D gp)
    {
	try {
	    Point2D lp = matrix.inverseTransform(gp, null);
	    return (lp);
	}
	catch (NoninvertibleTransformException e) {
	    System.out.println("Can't convert to local point");
	    return (null);
	}
    }
    public boolean contains(Point2D gp)
    {
	Point2D lp = globalToLocal(gp);
	for (int i = 0; i < pointRects.length; i++)
	    if (pointRects[i].contains(lp))
		return (true);
	return (((Shape)shape).contains(lp));
    }

    public void mousePressed(MouseEvent event)
    {  
	pressPoint = event.getPoint();
	transMat = new AffineTransform();
    }

    public void mouseReleased(MouseEvent event)
    { 
	Point2D curPoint = event.getPoint();

	TranslateCmd cmd = (TranslateCmd)CmdPool.getCmd(
	    CmdPool.TRANSLATE_CMD);
	cmd.translate(curPoint.getX() - pressPoint.getX(),
		      curPoint.getY() - pressPoint.getY());
	cmd.DoIt();
    }

    public void mouseDragged(MouseEvent event)
    {  
	Point2D curPoint = event.getPoint();
	transMat = AffineTransform.getTranslateInstance(
	    curPoint.getX() - pressPoint.getX(),
	    curPoint.getY() - pressPoint.getY());
    }

    public void mouseEntered(MouseEvent event) {}

    public void mouseExited(MouseEvent event) {}

    public void mouseClicked(MouseEvent event) {}

    public void mouseMoved(MouseEvent event) {}

    private Point2D _pressPoint;	
    protected AffineTransform transMat = new AffineTransform();
}// EditableShape

class EditableRect extends EditableShape
{
    private int _curPt = -1;
    EditableRect (E2DShape e2dShape) {
	Point2D.Double[] points = e2dShape.getPoints();
	// 矩形のグラフィック要素を作成する
	Rectangle2D.Double r = new Rectangle2D.Double();
	r.setFrameFromDiagonal(points[0], points[2]);
	shape = r;
	// 変換マトリックスをセットする
	matrix = e2dShape.getTransMat();
	// 頂点矩形(頂点を示す矩形)を作成する
	pointRects = new Rectangle2D.Double[4];
	for (int i = 0; i < 4; i++) {
	    pointRects[i] = new Rectangle2D.Double(
		points[i].getX()-SIZE/2, 
		points[i].getY()-SIZE/2, 
		SIZE,
		SIZE);
	}
    }
}

class EditableString extends EditableShape
{
    EditableString(E2DShape e2dShape)
    {
	Point2D.Double[] points = e2dShape.getPoints();
	// points[0]のグローバル座標に移動する
	matrix = AffineTransform.getTranslateInstance(points[0].getX(), 
						      points[0].getY());
	// 変換マトリックスを適応する
	matrix.concatenate(e2dShape.getTransMat());
	// shapeには空のRectangleをセットする必要あり
	shape = new Rectangle2D.Double();
	_string = ((E2DString)e2dShape).getString();
	_layout = null;
	// 頂点矩形なし
	pointRects = new Rectangle2D.Double[0];
    }
    public void draw(Graphics2D g2)
    {
	if (_layout == null) {
	    FontRenderContext context = g2.getFontRenderContext();
	    Font f = g2.getFont();
	    _layout = new TextLayout(_string, f, context);
	    Rectangle2D bounds = _layout.getBounds();
	    bounds.setRect(bounds.getX() - 2,
			   bounds.getY() - 2,
			   bounds.getWidth() + 3,
			   bounds.getHeight() + 2);
	    shape = bounds;

	}

	AffineTransform t = g2.getTransform();
	AffineTransform m = new AffineTransform(transMat);
	m.concatenate(matrix);
	g2.transform(m);
	_layout.draw(g2, 0, 0);
	g2.draw((Shape)shape);
	g2.setTransform(t);
    }
    private TextLayout _layout;
    private String _string;
}

さらにEditableShapeを生成するために、EditableVisitorを定義する。

リスト 10.5.2 EditableVisitor
class EditableVisitor extends DisplayVisitor
{
	EditableVisitor() 
	{
		super();
	}
	public void visitRect(E2DRect shape) 
	{
		if (shape.isSelected())
			list.add(EditableShape.createRect(shape));
	}
 	public void visitString(E2DString shape) 
	{
		if (shape.isSelected())
			list.add(EditableShape.createString(shape));
	}
}

10.6 コマンドオブジェクトの登録

実際にSelectコマンドを登録しているのは、E2DFacadeの生成コマンド内である。

	CmdPool.setCmd(CmdPool.SELECT_CMD, 
		new SelectCmd(CmdPool.SELECT_CMD, 
		new ImageIcon(), 
		new Rectangle2D.Double(), 
		_model));

11 図形の移動

11.1 操作の定義

11.2 マウスポイントを含む判定

11.3 移動コマンド

12 図形の変形

12.1 変形の仕様

12.2 選択図形の変形

12.3 変形コマンド