広告

■□■□■□■□■□■□■□■□■□■□■□■□■□■□■
                      2012年03月11日

   Java総合講座 - 初心者から達人へのパスポート
                 2009年11月開講コース 061号

                                セルゲイ・ランダウ
 バックナンバー: http://www.flsi.co.jp/Java_text/
■□■□■□■□■□■□■□■□■□■□■□■□■□■□■


-------------------------------------------------------
・現在、このメールマガジンは以下の2部構成になっています。
[1] 当初からのコース:vol.xxx(xxxは番号)が振られています。
   これは現在、中級レベルになっています。
[2] 2009年11月開講コース:xxx号(xxxは番号)が振られています。
   これは現在、初心者向けのレベルになっています。
・このメールマガジンは、画面を最大化して見てください。
小さな画面で見ていると、不適切な位置で行が切れてしまう
など、問題を起すことがあります。
・このメールマガジンに掲載されているソース・コード及び
文章は特に断らない限り、すべて筆者が著作権を所有してい
ます。また、これらのソース・コードは学習用のためだけに
提供しているものです。
-------------------------------------------------------


========================================================
◆ 01.グラフィックスのプログラミング
========================================================

ラスター・グラフィックス(JPEGなど)では画像を小さなドット(点)
の集まりで表現するため、画像を拡大すると小さなドットが拡大されて
粗いドットの集まりに見え、全体的に画像がぼやけてきますが、ベクター・
グラフィックスなら、画像を拡大してもぼやけないという特長があります。


今回は、このベクター・グラフィックスの特長を体現すべく、拡大
の機能(いわゆるズームの機能)を組み込むことにしましょう。


この拡大のメソッドもやはりDecoratedShapeに記述すればそのサブクラス
すべてに対して同じメソッドの呼び出しで拡大操作を行えることになり
ます。
ただし、拡大のやり方は図形ごとに異なるので、実際のメソッドは
DecoratedShapeのサブクラス(DecoratedFreeCurve、DecoratedLine、
DecoratedRectangle、DecoratedEllipse)のほうに実装していきます。

という訳で、DecoratedShapeにはこの拡大操作のメソッドを抽象メソッド
として組み込んでおきましょう。

では、DecoratedShapeのソース・コードに下記のような行を追加して下さい。
--------------------------------------------------------
   abstract public void magnify(double mag);
--------------------------------------------------------
これはabstractを指定していますから、抽象メソッドを意味します。
また「拡大する」という意味を込めてmagnify()というメソッド名にしました
が、引数のmagは拡大の倍率を指定するために用意しています。つまり、この
メソッドは図形をmag倍に拡大するものとします。
このmagという引数はdouble型なので、小数点以下も含み、例えば0.5といった
1より小さい数も指定可能になります。そして例えば0.5の倍率で拡大すると
いうことは、実際には半分のサイズに縮小することを意味します。
したがって、この操作は正確には拡大縮小操作と言ったほうがいいかも知れ
ません。


念のためにこのコードを追加したDecoratedShapeの全ソース・コードを
下に提示しておきます。
--------------------------------------------------------
package jp.co.flsi.lecture.cg;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.io.Serializable;

public abstract class DecoratedShape implements Serializable {
   private static final long serialVersionUID = 1L;
   private Color color = Color.BLACK;
   private float stroke = 0.1f;
   private Shape shape = null;

   public Color getColor() {
      return color;
   }

   public Shape getShape() {
      return shape;
   }

   public float getStroke() {
      return stroke;
   }

   public void setColor(Color clr) {
      color = clr;
   }

   public void setShape(Shape shp) {
      shape = shp;
   }

   public void setStroke(float strk) {
      stroke = strk;
   }

   public void paintSelf(Graphics2D g2d) {
      g2d.setColor(getColor());
      g2d.setStroke(new BasicStroke(getStroke()));
      g2d.draw(getShape());
   }

   abstract public void magnify(double mag);

}
--------------------------------------------------------

このように、DecoratedShapeに抽象メソッドを宣言しておけば、DecoratedShape
のサブクラスにこのメソッド(magnify())を実装することが必須となります。
すると、もしサブクラスにこのメソッドを実装するのを忘れたら、サブクラスが
コンパイル・エラーになりますから、Eclipseではソース・コード上にエラーの
マークが付くことになります。
忘れっぽい人には大助かりですね。


では、各サブクラスでこのメソッドを実装していきましょう。

なお、今回は話を簡単にするために、横方向(x軸の方向)にも縦方向
(y軸の方向)にも同じ倍率で拡大することとし(そのためにmagnify()
メソッドの引数は一つだけにしている)、拡大は原点を中心に
描かれている全図形を含めた全体像を均一に拡大することとします。

これはちょうどズームの機能を表現していると言えますが、ただ、原点
が固定されることに注意して下さい。
原点はJPanelの左上の隅になりますので、ここを中心に拡大するという
ことは全体が右下方向に膨らんでいくようなイメージになり、普通の
ズームとちょっとイメージが違うと思います。
(話を簡単にするための手抜きです。もっと自然なイメージにしたい人は、
JPanelの中心を固定して拡大するように自力でプログラムを書き換えて
みて下さい。この場合は、拡大だけでなく平行移動の操作もプログラミング
する必要があります。)


まずはDecoratedLineのソース・コードを開きましょう。
まだmagnify()メソッドを実装していないのでクラス名(DecoratedLine)の所に
エラーの赤いアンダーラインがついているでしょう。
そこにカーソルを入れてCtrl+1(Ctrlキーを押しながら数字の1キーを押す)を
押して下さい。
エラーの対処法のリストが出てきますので、その中から先頭の「実装されていない
メソッドの追加」を選択(ダブル・クリック)して下さい。

すると、
--------------------------------------------------------
   @Override
   public void magnify(double mag) {
      // TODO 自動生成されたメソッド・スタブ
     
   }
--------------------------------------------------------
というようなコードが自動生成されますね。
これを下記のように編集して下さい。
--------------------------------------------------------
   @Override
   public void magnify(double mag) {
      Line2D line = (Line2D)getShape();
      line.setLine(mag * line.getX1(), mag * line.getY1(), mag * line.getX2(), mag * line.getY2());
   }
--------------------------------------------------------
ここでgetX1(), getY1(), getX2(), getY2()はそれぞれ、Line2D
が表現する直線の始点のx座標、始点のy座標、終点のx座標、終点のy座標
を得るためのメソッドです。
また、setLine()というメソッドは直線を引き直すためのメソッドで
第1引数、第2引数、第3引数、第4引数にはそれぞれ新しい直線の始点の
x座標、始点のy座標、終点のx座標、終点のy座標を指定します。
これらの引数にそれぞれ拡大の倍率(mag)を掛け算していますから、
各座標が原点を中心にその倍率分移動することになりますね。


次に、DecoratedRectangleのソース・コードを開きましょう。
これも先ほどと同様にしてmagnify()メソッド
--------------------------------------------------------
   @Override
   public void magnify(double mag) {
      // TODO 自動生成されたメソッド・スタブ
     
   }
--------------------------------------------------------
を自動生成させ、これを下記のように編集して下さい。
--------------------------------------------------------
   @Override
   public void magnify(double mag) {
      Rectangle rectangle = (Rectangle)getShape();
      rectangle.setRect(mag * rectangle.getX(), mag * rectangle.getY(), mag * rectangle.getWidth(), mag * rectangle.getHeight());
   }
--------------------------------------------------------
ここでgetX(), getY(), getWidth(), getHeight()はそれぞれ、Rectangle
が表現する矩形の始点のx座標、y座標、横幅、高さを得るためのメソッド
です。
また、setRect()というメソッドは矩形の枠(境界)を引き直すための
メソッドで第1引数、第2引数、第3引数、第4引数にはそれぞれ新しい矩形の
始点のx座標、始点のy座標、横幅、高さを指定します。


次に、DecoratedEllipseのソース・コードを開きましょう。
これも先ほどと同様にしてmagnify()メソッド
--------------------------------------------------------
   @Override
   public void magnify(double mag) {
      // TODO 自動生成されたメソッド・スタブ
     
   }
--------------------------------------------------------
を自動生成させ、これを下記のように編集して下さい。
--------------------------------------------------------
   @Override
   public void magnify(double mag) {
      Ellipse2D ellipse = (Ellipse2D)getShape();
      ellipse.setFrame(mag * ellipse.getX(), mag * ellipse.getY(), mag * ellipse.getWidth(), mag * ellipse.getHeight());
   }
--------------------------------------------------------
ここでgetX(), getY(), getWidth(), getHeight()はそれぞれ、Ellipse2D
が表現する楕円の表示枠矩形の左上隅のx座標、左上隅のy座標、横幅、高さ
を得るためのメソッドです。
(楕円はまず仮想的に矩形を想定して、その矩形の各辺に接するような楕円を
描くというやり方で描きますが、その仮想的な矩形をここでは表示枠矩形と
呼びます。)
また、setFrame()というメソッドは楕円を引き直すためのメソッドで、
第1引数、第2引数、第3引数、第4引数にはそれぞれ新しい楕円の表示枠矩形
の左上隅のx座標、左上隅のy座標、横幅、高さを指定します。


次に、DecoratedFreeCurveのソース・コードを開きましょう。
これも先ほどと同様にしてmagnify()メソッド
--------------------------------------------------------
   @Override
   public void magnify(double mag) {
      // TODO 自動生成されたメソッド・スタブ
     
   }
--------------------------------------------------------
を自動生成させ、これを下記のように編集して下さい。
--------------------------------------------------------
   @Override
   public void magnify(double mag) {
      AffineTransform trans = new AffineTransform();
      trans.scale(mag, mag);
      ((GeneralPath)getShape()).transform(trans);
   }
--------------------------------------------------------
ここでAffineTransformというのは数学のアフィン変換を表現するクラスで
java.awt.geomパッケージに入っています。
(当メールマガジンは数学を教えるものではないのでアフィン変換について
は説明しません。)
このAffineTransformのscale(mag, mag)というメソッドを実行すると、x軸方向
にもy軸方向にもmag倍の倍率で拡大を行うアフィン変換になります。
これをGeneralPathのtransform()というメソッドの引数に指定してやると、
GeneralPathがx軸方向(横方向)にもy軸方向(縦方向)にもmag倍に拡大され
ることになります。



では、続いて、DrawingPanelに描かれたこれらの図形を一挙に拡大するように
DrawingPanelにも拡大のメソッドを実装しましょう。
ただし、DrawingPanel自体を拡大するわけではなく、DrawingPanelの中に表示
される各図形をズーム・アップして見るという意味からzoom()というメソッド名
にしておきます。

DrawingPanelのソース・コードを開いて下記のようなメソッドを追加して
下さい。
--------------------------------------------------------
   public void zoom(double mag) {
      for (DecoratedShape shape : getShapes()) {
         shape.magnify(mag);
      }
      repaint();
   }
--------------------------------------------------------
このメソッドでは、DrawingPanelに描かれた各図形をshapesフィールド
(getShapes()メソッドで取り出したshapesフィールド)から一つずつ
取り出しては、そのmagnify()メソッドを実行しています。
つまり、DrawingPanelに描かれた全ての図形を拡大表示しています。
そして最後に例によってrepaint()メソッドを呼び出して描画し直して
います。



では、最後にDrawingToolPanelに画像を拡大するためのGUI部品を貼り付けて
プログラミングしましょう。

DrawingToolPanelのDesignビューを開いて下さい。

今回貼り付けるGUI部品はJSliderというものですが、これを「楕円」ボタンの
下に貼り付ける予定です。

(1) DrawingToolPanelの面積が狭くてJSliderを貼り付ける空き場所がない人は、
Structure欄の配下のComponents欄のリストの中から
(javax.swing.JPanel)
の行を選択します。
すると、DrawingToolPanelのウインドウの画像の周囲に黒線の枠が表示されます
ので、そのうちの下部の黒線の中にあるハンドルをドラッグして下のほうに
引き伸ばしておきましょう。
そうすれば、「楕円」ボタンの下に空き場所ができますね。

(2) PaletteのComponentsの配下からJSliderを取り出して、DrawingToolFrameの
「楕円」ボタンの下に貼り付けて下さい。

(3) おそらく貼り付けたJSliderの幅が大きすぎるでしょうから、JSliderの周囲に
表示されるハンドルをドラッグして幅を小さくするか、あるいは以前お話した方法
で幅や位置を他の(「楕円」ボタンなどの)GUI部品に合わせて調整して下さい。
またJSliderの高さも十分に広げておきましょう(あとでスライダーの目盛りを
表示させるために余裕を持たせます)。

(4) この貼り付けたJSliderの変数名を
zoomSlider
に変更しておきましょう。

また、maximumプロパティーの値を
50
に変更(特に断らない限り、数値は半角にすること)し、
minimumプロパティーの値を
10
に変更し、
toolTipTextプロパティーの値を
ズーム
にして、valueプロパティーの値(デフォルト値)を
30
に変更しておきましょう。(これはmaximum=50とminimum=10の間の真ん中に
していることに注意して下さい。)
(toolTipTextプロパティーの値を「ズーム」に設定したので、マウス・ポイン
ターをスライダーの所に持っていくとスライダーに「ズーム」という文字列が
表示され、何のためのスライダーか分かるのですが、本来ならばスライダーの上
にラベルか何かで何のためのスライダーか分かるように説明書きをしておいた
ほうがいいでしょう。)

あと、majorTickSpacingプロパティーの値を
10
にして、paintTicksプロパティーを「true」に変更する(クリックすることに
よってチェックマークを入れると「false」から「true」に変わる)と、
スライダーに10ごとの目盛りが表示されます。
(ついでに、もしpaintLablesプロパティーを「true」に変更すると、目盛りの
下に数字が表示されますが、今回は数字の表示はしません。)

(5) 貼り付けたJSliderを右クリックし、「Add event handler」→「変更」→「stateChange」
を選択します。すると、下記のようなコードが自動生成されますね。
--------------------------------------------------------
      zoomSlider.addChangeListener(new ChangeListener() {
         public void stateChanged(ChangeEvent e) {
         }
      });
--------------------------------------------------------
スライダー(JSlider)をマウスでドラッグしてスライドさせることによって
スライダーの状態が変わったときにはこのstateChanged()というメソッドが
呼び出されますので、このメソッドの中で先ほどのDrawingPanelのzoom()メソッ
ドを実行させるようにすればいいのです。したがって、上のコードを
--------------------------------------------------------
      zoomSlider.addChangeListener(new ChangeListener() {
         public void stateChanged(ChangeEvent e) {
            drawingPanel.zoom(zoomSlider.getValue() / 30.0);
         }
      });
--------------------------------------------------------
のように編集して下さい。
(このコードのうち、スライダーのgetValue()というメソッドは、スライダー
が指している値(先ほど目盛りを表示させたが、その目盛りは左から順番に
10, 20, 30, 40, 50という値を示している)を取り出すメソッドです。
上のコードでは、この値を30.0で割ったものを拡大の倍率としてzoom()メソッド
の引数に指定しています。
つまり、スライダーが30、つまり真ん中の目盛りの所に置かれているときに、
倍率が1になるように計算しています。したがって、スライダーをそれより右
にスライドさせると倍率は1より大きくなり拡大を意味することになりますが、
スライダーをそれより左にスライドさせると倍率は1より小さくなり縮小を意味
することになります。
ちなみに上の計算式で30ではなく30.0で割り算しているのは、JSliderのvalueプロ
パティーがint型(整数)であるため、30で割り算すると結果も整数にされてしまう
からです。30.0で割れば小数点数で計算されますので、zoomのdouble型の引数に
小数点以下の数値まで引き渡すことができます。)

すると、ここでもzoomSliderがfinal指定されていないためにエラーが出ますね。
というわけで、例によって
--------------------------------------------------------
      JSlider zoomSlider = new JSlider();
--------------------------------------------------------
という行にfinalを指定して
--------------------------------------------------------
      final JSlider zoomSlider = new JSlider();
--------------------------------------------------------
というふうに書き換えましょう。



では、DrawingToolFrameを実行してテストしてみましょう。

試しに矩形(または直線、または楕円)を描いてからスライダーをスライドして
拡大してみると、なぜか拡大された図形の他に最初の大きさの図形もそのまま
残って描画されますね。

何かプログラムにミスがあるようです。どこがおかしいのか分かりますか?


DrawingPanelのソース・コードのpaintImage()メソッドの中をよく
見て下さい。その中に
--------------------------------------------------------
         if (!mousePressed) {
            getShapes().add(aShape);
            ptIndex = 0;
         }
         else {
            aShape.paintSelf(g2dImage);
         }
--------------------------------------------------------
という箇所がありますが、実はここがおかしいのです。
mousePressedがfalseのとき(つまりマウスのボタンが離されているとき)には
図形を描くためのドラッグ操作を終えているわけですから、図形が完成したもの
としてshapesフィールド(ArrayListオブジェクト)にその図形を追加した後、
ptIndexの値を0にリセット(初期値に戻す)していますが、ptIndexは自由曲線の
ためだけのフィールドであって、矩形や直線、楕円には関係しません。
一方、矩形や直線、楕円はstartPoint(始点)とendPoint(終点)だけを使って
描いているのですが、このstartPointとendPointのリセットが行われていません。

つまり、図形が完成して関係する変数の値をリセットするタイミングになったとき
に自由曲線関係の変数だけリセットして他の図形に関係する変数のリセットをし忘れ
ているわけです。

そしてこのリセットが行われないとどうなるかというと、次の再描画(repaint())の
ときに、その上のほうにある
--------------------------------------------------------
      else if(endPoint != null) {
         if (shapeType == ShapeEnum.LINE) {
            aShape = new DecoratedLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
         }
         else {
            Point tempStartPoint;
            Point tempEndPoint;
            int drawWidth;
            int drawHeight;
            tempEndPoint = new Point(Math.max(endPoint.x, startPoint.x), Math.max(endPoint.y, startPoint.y));
            tempStartPoint = new Point(Math.min(endPoint.x, startPoint.x), Math.min(endPoint.y, startPoint.y));
            drawWidth = tempEndPoint.x - tempStartPoint.x;
            drawHeight = tempEndPoint.y - tempStartPoint.y;
            if(shapeType == ShapeEnum.ELLIPSE) {
               aShape = new DecoratedEllipse(tempStartPoint.x, tempStartPoint.y, drawWidth, drawHeight);
            }
            else if(shapeType == ShapeEnum.RECTANGLE) {
               aShape = new DecoratedRectangle(tempStartPoint.x, tempStartPoint.y, drawWidth, drawHeight);
            }
         }
         aShape.setColor(lineColor);
         aShape.setStroke(drawStroke);
      }
--------------------------------------------------------
というコードが(endPointがnullでないために)実行されてしまい、shapesフィールドの
中に追加されている図形の他にこのstartPointとendPointを使って描かれる図形も重複して
描画されてしまいます。

そして、拡大の操作はshapesフィールドの中に追加されている図形に対して行われるのに
対し、このstartPointとendPointは拡大のメソッド(zoom())には何も影響を受けないため
拡大前の図形を重複して描画してしまうことになります。

というわけで、このstartPointとendPointのリセットを行っていなかったために、不具合が
生じたのです。

この不具合に対処するために、先ほどの
--------------------------------------------------------
         if (!mousePressed) {
            getShapes().add(aShape);
            ptIndex = 0;
         }
         else {
            aShape.paintSelf(g2dImage);
         }
--------------------------------------------------------
という箇所にstartPointとendPointのリセットのコードを追加して
--------------------------------------------------------
         if (!mousePressed) {
            getShapes().add(aShape);
            ptIndex = 0;
            startPoint = null;
            endPoint = null;
         }
         else {
            aShape.paintSelf(g2dImage);
         }
--------------------------------------------------------
のように編集して下さい。
これで、拡大前の図形が重複して描画されることは無くなるはずです。

このように変数のリセットをし忘れたために不具合が生じるということは
よくあるのですが、なかなか分かりにくい場合が多いので要注意です。
リセットし忘れがないか意識的にチェックする習慣をつけておきましょう。



では、ここで再度DrawingToolFrameを実行してテストしてみましょう。
今度は、先ほどのような問題は起こりませんね。

それから、図形をいくら拡大してもぼやけないというベクター・グラフィックス
の特長も確認できますね。
「名前をつけてベクター保存」でファイルに保管し、「ベクターファイルを開く」で
それを開いてから拡大してみても、やはりぼやけることはありませんね。
(JPEGファイルを拡大する機能は作り込んでいませんが、これはWindows付属の
ペイントなどのアプリケーションで開けばズームして見ることができますので、
そちらのほうでラスター・グラフィックスのぼやけ具合を確認してください。)



なお、試してみればわかるように、例えば図形を何度か拡大した後スライダーを
真ん中に戻しても図形が元のサイズに戻るわけではありませんので、スライダー
操作に対する図形の拡大・縮小の動きに違和感があるでしょう。
つまり、スライダーの位置と拡大・縮小の動きにずれがあるような違和感を感じる
ことと思います。

今回のプログラムでは、例えばスライダーを右にスライドするたびに図形が拡大
され、拡大したときの新たな座標やサイズの値で図形のデータが書き換わって
しまいますので、元のサイズは記憶されていませんし、そのためスライダーの位置
が元の図形のサイズに対する倍率に一致するわけではありません。

スライダーを真ん中に戻したら図形が元のサイズに戻るというようにするためには、
図形のオリジナルなサイズや位置のデータを別途保管しておいて、スライダーを
スライドするたびにそのオリジナルなデータを使って計算し直すという必要があり
ますが、プログラムが複雑になるため当記事では割愛します。意欲のある読者は
自主課題としてやってみて下さい。



ところで、自由曲線、直線、楕円、矩形を描いて、拡大や縮小を何度も繰り返して
いると、矩形だけが他の図形と大きく異なったサイズに変わってくることが分かる
でしょう。

どうしてこのような現象が生じるかというと、実は、矩形だけ位置やサイズのデータ
の精度が粗いために生じるのです。

DecoratedFreeCurve、DecoratedLine、DecoratedEllipse、DecoratedRectangleの
各コンストラクターのソースを見て下さい。
DecoratedFreeCurve、DecoratedLine、DecoratedEllipseはそれぞれGeneralPath、
Line2D、Ellipse2Dというjava.awt.geomパッケージのクラスを使っていますが、
DecoratedRectangleだけはRectangleというjava.awtパッケージのクラスを使って
います。
このRectangleが位置やサイズの情報としてint型を使っているのに対し、他の図形は
float等の精度のよい型を使っています。
したがって、拡大・縮小の計算をしているときにその精度の違いだけずれが生じて
きて、それを繰り返しているうちにずれが拡大されてしまうのです。

この問題に対処するためには、DecoratedRectangleの精度を改善してあげればいいです。
そのためには、RectangleをすべてRectangle2Dというjava.awt.geomパッケージの
クラスに置き換えてしまいます。

という訳で、DecoratedRectangleのソース・コード
--------------------------------------------------------
package jp.co.flsi.lecture.cg;

import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;

public class DecoratedRectangle extends DecoratedShape {

   public DecoratedRectangle() {
      setShape(new Rectangle());
   }

   public DecoratedRectangle(int x0, int y0, int width, int height) {
      setShape(new Rectangle(x0, y0, width, height));
   }

   @Override
   public void magnify(double mag) {
      Rectangle2D rectangle = (Rectangle2D)getShape();
      rectangle.setRect(mag * rectangle.getX(), mag * rectangle.getY(), mag * rectangle.getWidth(), mag * rectangle.getHeight());
   }

}
--------------------------------------------------------

--------------------------------------------------------
package jp.co.flsi.lecture.cg;

import java.awt.geom.Rectangle2D;

public class DecoratedRectangle extends DecoratedShape {

   public DecoratedRectangle() {
      setShape(new Rectangle2D.Float());
   }

   public DecoratedRectangle(int x0, int y0, int width, int height) {
      setShape(new Rectangle2D.Float(x0, y0, width, height));
   }

   @Override
   public void magnify(double mag) {
      Rectangle2D rectangle = (Rectangle2D)getShape();
      rectangle.setRect(mag * rectangle.getX(), mag * rectangle.getY(), mag * rectangle.getWidth(), mag * rectangle.getHeight());
   }

}
--------------------------------------------------------
に書き換えてみて下さい。


そして、DrawingToolFrameをテストしてみましょう。
これでこの問題は解決されましたね。


このように精度を要求される計算などを行う場合には、Rectangleではなく、
java.awt.geomパッケージに入っているRectangle2Dを使う必要があります。


さて、このように各図形のクラスに変更を加えた後でDrawingToolFrameを
実行し、クラスを変更する前に保管していたベクターファイルを開いて
みるとどうなるか試してみて下さい。

InvalidClassExceptionが発生しますね(コンソールを確認のこと)。

これは059号でお話したことですが、プログラムのコンパイル時の
serialVersionUIDと直列化して保管していたファイル内に記録
されているserialVersionUIDに食い違いがあることを意味します。

以前、直列化のお話をしたときには言い忘れていましたが、
serialVersionUIDを指定していない場合はコンパイル時にコンパイラー
が自動的にバージョンIDを割振ります。
これによって、serialVersionUIDを指定しなかった場合でも、
異なるバージョンのプログラムで保管(直列化)したファイルには
異なるバージョンIDが振られることになります。
そして、バージョンIDの食い違いがあったときには先ほどのように
プログラムの実行時にInvalidClassExceptionを通知してくれます。

それなら、serialVersionUIDは明記せず、コンパイラーが自動的に
割り振るバージョンIDにお任せしたほうがいいのでは、と思うかも
しれません。
しかし、コンパイラーが自動的に割り振るバージョンIDは、コンパイラー
によっても異なるため、同じソースでも異なるコンパイラーでコンパイル
すると異なるバージョンIDになる可能性があります。

という訳で、やはりプログラムのバージョン管理をきちんと行って
バージョンごとにきちんと取り決めたserialVersionUIDを指定(明記)
することが望ましいのです。



========================================================

ところで、以前(057号で)アンチエイリアシングのお話をしましたが、
そのとき自由曲線にまだ少しぎこちなさが残っているので後で対処する
という話をしました。


では、原点(左上の隅)近くでマウスを上下に激しくゆさぶるように
ドラッグして自由曲線を描いてみて下さい。
つまりわざとぎざぎざの線にしてみるのです。

この自由曲線を拡大して見ると、まれに(滅多にできないが)尖った
部分ができていることがあります。

この尖った部分をなめらかにするためのテクニックがあるので、
それをここで紹介しておきます。


では、DecoratedShapeのpaintSelf()メソッド
--------------------------------------------------------
   public void paintSelf(Graphics2D g2d) {
      g2d.setColor(getColor());
      g2d.setStroke(new BasicStroke(getStroke()));
      g2d.draw(getShape());
   }
--------------------------------------------------------
を次のように書き換えてみて下さい。
--------------------------------------------------------
   public void paintSelf(Graphics2D g2d) {
      g2d.setColor(getColor());
      g2d.setStroke(new BasicStroke(getStroke(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
      g2d.draw(getShape());
   }
--------------------------------------------------------

ここで、BasicStroke()コンストラクターの引数に指定している
BasicStroke.CAP_ROUNDというのは、線の端を丸くすることを意味
し、BasicStroke.JOIN_ROUNDは線と線のつなぎ部分に丸みをおびさせる
ことを意味します。
(丸くする以外にもいくつかの指定が可能ですが、必要な人はAPI仕様
などで調べてみてください。)
つまり、線と線のつながり部分が角ばっているとぎこちなく見えます
から、それに丸みを与えることによってなめらかに見せるというテク
ニックです。


ソース・コードを保管したら、一度DrawingToolFrameを実行してテスト
してみてください。
まあ、見た目には以前との違いはあまりはっきりしませんが、こういう
テクニックがあるということだけは頭に入れておきましょう。


(続く)


以上、今回は
┌───────────────────────────┐
・図形(直線、矩形、楕円、自由曲線)の拡大縮小の仕方
・変数のリセットのし忘れに注意
・矩形の精度
・serialVersionUIDとInvalidClassException
・BasicStroke.CAP_ROUNDとBasicStroke.JOIN_ROUND
└───────────────────────────┘
などについて学習しました。

では、今日はここまでにします。



┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
★ホームページ:
      http://www.flsi.co.jp/Java_text/
★このメールマガジンは
     「まぐまぐ(http://www.mag2.com)」
 を利用して発行しています。
★バックナンバーは
      http://www.flsi.co.jp/Java_text/
 にあります。
★このメールマガジンの登録/解除は下記Webページでできます。
      http://www.mag2.com/m/0000193915.html
★このメールマガジンへの質問は下記Webページにて受け付けて
 います。わからない所がありましたら、どしどしと質問をお寄
 せください。
      http://www.flsi.co.jp/Java_text/
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Copyright (C) 2012 Future Lifestyle Inc. 不許無断複製