広告

■□■□■□■□■□■□■□■□■□■□■□■□■□■□■
                      2012年01月27日

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

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


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


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

さて、前回作成したお絵描きソフトは超簡単でかなりいい加減なもの
だと言ったのですが、何がいい加減なのかお話しておきましょう。


まず、GraffitiAppletを実行して試してみてください。
このアプレット(GraffitiApplet)でお絵描きしてから、(「最小化」
ボタン(ウインドウの右上の隅にある3つのボタンのうち[-]のマーク
のボタン)をクリックして)アプレットのウインドウを隠し(最小化し)、
再度表示してみると、描いていた絵が消えてしまいますね。


ウインドウのシステムでは、最小化などによって隠れていたウインドウ
が再度表示されるときには、そのウインドウに再描画の命令を送ること
によって、ウインドウを表示させようとします。

JavaのGUI部品では、再描画を行うメソッドはrepaint()という名前
になっているので、「再描画の命令を送る」という行為はrepaint()
メソッドを呼び出すという行為によって実行される仕組みになって
います。

一般のウインドウ、たとえばJFrameオブジェクトは、repaint()メソ
ッドが呼び出されると、repaint()メソッドが自分自身(JFrameオブ
ジェクト)のupdate()メソッドを呼び出し、そのupdate()メソッド
がpaint()メソッドを呼び出し、paint()メソッドは自分自身(JFrame
オブジェクト)を描画するとともに、そこに張り付いているすべて
のGUI部品(たとえばJButtonやJTextFieldなど)のrepaint()メソッ
ドを呼び出す・・・というふうに連鎖反応的に、そのJFrameオブジェ
クト内のすべてのGUI部品に渡って再描画が行われていきます。
(なお、AWTでは、update()メソッドはいったん自分の背景画像をクリ
アして(真っ白の状態にして)からpaint()メソッドを呼び出すという
ことを行っていましたが、これでは効率がよくないため、Swingでは、
update()は背景のクリアはせずに即座にpaint()を行うというように
改善されています。)

このことはすべてのGUI部品(Component)に対して同様であり、
JFrameをJAppletに置き換えても同じように、repaint()メソッドが
呼び出されると、repaint()メソッドが自分自身(JAppletオブジェ
クト)のupdate()メソッドを呼び出し・・・(以下省略)・・・、
という同様の振る舞いをします。

ボタンやテキストフィールドなどのGUI部品なら自動的
に再描画されるのですが、今回作成したお絵描きソフトで描いた絵の
ようにGUI部品でないものについては、隠れる前の描画内容を記憶し
ておいて、再描画をするときにその内容を再現するようにプログラミ
ングしておかないことには、自動的には再現されません。

グラフィックスのプログラムを作成するためには、このような再描画
の仕組みを頭に入れてプログラミングを行う必要があるのです。
(今回はこの程度の説明でプログラミングを行いますが、後の記事で
もう少し細かい仕組みを説明致します。)


では具体的に何をすればいいかというと、描画内容をメモリー上に記憶
してその記憶どおりに描画させるようにGraffitiAppletのpaint()メソッ
ドを実装すればよいのです。
(正確にいうとJApplet自身はpaint()メソッドは持っておらず、
Containerのpaint()メソッドを継承しているだけです。つまり、
GraffitiAppletにpaint()メソッドを実装するということは、
Containerのpaint()メソッドを上書き(override)することを意味
します。)


はい。では、さっそくプログラミングをやってみましょう。

Eclipseを起動してください。


まず最初に、マウスをドラッグして描く自由曲線(ここでは自由曲線は
マウスを使ってフリーハンドで描く曲線のことだと思って下さい)の
内容を記憶するためのクラスとして、DecoratedFreeCurveという名前の
クラスを作ることにしましょう。(FreeCurveは文字通り自由曲線の意味
だが、線の色や太さを変えたりして飾り付けをできることを意味する
ためにDecoratedという言葉を付け加えた。)

┌補足─────────────────────────┐
ちなみに、自由曲線は主にCAD(Computer Aded Design)などで
使われる用語であり、与えられたいくつかの点を、与えられた
順番で通るように定義された滑らかな曲線を意味します。
└───────────────────────────┘


前回は、自由曲線を折れ線(線分をつなぎ合わせたもの)で表現して
いましたが、これは一見曲線のように見えても細かく見るとガタガタ
の線であり、曲線とは言えません。
それで今回は、ちゃんと曲線で表現する方法を説明いたします。

実はJavaにはGeneralPathという自由曲線を描けるクラスが用意されて
おり、これを使えば簡単に自由曲線を描けるのです。
というわけで、今回はこれを使うことにします。


さらには、これから作成するお絵描きソフトには直線や矩形、楕円など
の図形も描けるようにすることとします。


このように、自由曲線、直線、矩形、楕円などの様々な図形を扱う場合は
それらの共通のスーパークラスを定義して、(線の色を設定するとか、
線の太さを設定するとか、描画するなどの)共通のメソッドをスーパー
クラスに用意してしまえばプログラムが簡潔になり、プログラミングが
楽になります。


というわけで、まずはこれらのスーパークラスとしてDecoratedShapeと
いう名前のクラスを作成しておきましょう。(Shapeは図形の意味です。)

では、Eclipseを起動してください。

(1) パッケージ・エクスプローラーの中のJStudy1の配下のsrcの配下の
jp.co.flsi.lecture.cg(パッケージ名)を右クリックし、
「新規」→「クラス」を選択します。

(2) 「新規Javaクラス」ウインドウにおいて「ソース・フォルダー」欄に
「JStudy1/src」が入力されていることを確認し、「パッケージ」欄に
「jp.co.flsi.lecture.cg」が入力されていることを確認し、
「名前」の欄に

DecoratedShape

と入力し、「完了」ボタンをクリックします。


DecoratedShape.javaのエディターが開きましたら、以下のようにソース・
コードを編集しましょう。

--------------------------------------------------------
package jp.co.flsi.lecture.cg;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;

public abstract class DecoratedShape {
   private Color color = Color.BLACK;
   private Stroke stroke = new BasicStroke(0.1f);
   private Shape shape = null;

   public Color getColor() {
      return color;
   }

   public Shape getShape() {
      return shape;
   }

   public Stroke getStroke() {
      return stroke;
   }

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

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

   public void setStroke(Stroke strk) {
      stroke = strk;
   }
  
   public void paintSelf(Graphics2D g2d) {
      g2d.setColor(getColor());
      g2d.setStroke(getStroke());
      g2d.draw(getShape());
   }

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

ソース・コードを少し解説しておきましょう。

まず、このクラスは特定の図形を表さない抽象的なものなので、インスタンス
を作ったりしないようにabstractを指定してあります。つまり抽象クラスに
しています。

最初のimport文の中にGraphics2Dというクラス名が出てきますが、この
Graphics2DはGraphicsのサブクラスで、グラフィックスに関して高度な制御
機能を持つものです。(Graphics2Dを使う理由はあとで述べます。)
ただし、2Dというのは2次元(2-dimensional)、つまり平面(x座標とy座標と
いう2つの次元だけで表現される空間)上の世界を意味し、Graphics2Dは
平面上のグラフィックスだけを扱います。

ちなみに、Javaには3Dすなわち3次元空間(立体)における幾何学を表現す
るためのAPI(パッケージ)もあります。


上記ソース・コードのメソッドの中には、getColor()、setColor()、getStroke()、
setStroke()というのがありますが、これらはそれぞれ色(Color)や線の種類
(Stroke)の情報を設定したり取り出したりするためのメソッド(setter, getter)
です。

上記ソース・コードに登場するBasicStrokeはStrokeインターフェースを
実装したクラスで、java.awtパッケージに含まれています。
「new BasicStroke(0.1f);」という形式でコンストラクターを呼び出すと、
引数で指定した線の太さを情報として持つStrokeオブジェクトが生成され
ます。

上記ソース・コードでは、
   private Color color = Color.BLACK;
   private Stroke stroke = new BasicStroke(0.1f);
というフィールドの定義によって、色のデフォルトは黒(Color.BLACK)、
線の幅(太さ)のデフォルトは0.1f(標準的な実線の太さの0.1倍)に
初期設定していますね。


なお、このお絵描きソフトでは、後で、自由曲線だけでなく直線や長方形
(矩形)や楕円(それぞれDecoratedLine、DecoratedRectangle、DecoratedEllipse
という名前にする)といった図形のクラスも用意する予定ですが、これらの
色と線の幅(矩形などの閉じた図形の場合は輪郭の線の幅)の情報は、個々
の図形ごとに、インスタンス変数としてcolorやstrokeのフィールドに持た
せることにします。

なお、細かく言うと、矩形などの閉じた図形の場合には、輪郭の線の色と
内部の色(いわゆる、塗りつぶしの色)という2種類の色を用意すべきで
しょうし、単純な色だけでなくグラデーションやテキスタイル模様のような
のも用意すべきでしょうけれど、細かいことを言っているときりがないし、
プログラムが複雑になりすぎて説明に時間がかかってしまいますので、
ここでは省略します。
細かいことは演習として自分でやってみてください。


上記ソース・コードでは、さらにgetShape()やsetShape()という名前で
Shape型のオブジェクトを取得したり設定したりするgetterメソッド、
setterメソッドが用意されていますが、これは最後のpaintSelf()という
メソッドでShape型のオブジェクトを使用するために用意しています。
(これらはshapeというShape型のフィールドののgetterメソッド、setter
メソッドになっていますね。)

ここで言うShape型とは、java.awtパッケージに入っているShapeという
型のことで、このShapeは図形に関する処理を行うためのインターフェース
です。
JavaにはこのShapeインターフェースを実装した各種の図形のクラスが
用意されており、このShape型を通して図形の描画などの処理が行える
ようになっています。

DecoratedShapeクラスは、このShape型を利用し、さらに(線の色や
線の太さなどの)装飾的な情報を付加したオブジェクトを作れるよう
に定義しています。


paintSelf()メソッドは図形の描画を行うのですが、図形によって描画内容
が異なるため、描画時にはどの図形なのか分かっていなければなりません。

そのため、予めsetShape()メソッドを実行することによってどの図形なのか
記憶しておき、paintSelf()メソッドの中ではgetShape()メソッドの呼び出し
によってその図形の情報を取り出して使うことにします。

そのような手順を踏めば、DecoratedShapeという抽象クラスの中に具体的
な図形の描画をできるメソッド(それがpaintSelf()メソッド)を用意する
ことができるのです。
そうすれば、DecoratedShapeのサブクラス(具体的な図形を表現する
DecoratedFreeCurve、DecoratedLine、DecoratedRectangle、DecoratedEllipse
といったクラス)で個別に描画のメソッドを用意する必要がなくなり、
プログラムが簡潔になります。(プログラムを簡潔にするためのテクニック
の一つなので頭に入れておきましょう。)

各サブクラスで描画を行う際には、事前にsetShape()メソッドを実行して
具体的な図形のクラスを指定しておく必要がありますが、これは各サブクラス
のコンストラクターの中でsetShape()メソッドを実行してShape型の具体的な
クラスを指定するという方法で行えます。
各サブクラスのインスタンス生成時には必ずコンストラクターが呼び出され
ますから、必ず描画よりも前にsetShape()メソッドが実行されることになる
からです。
(後ほどDecoratedFreeCurve、DecoratedLine、DecoratedRectangle、
DecoratedEllipseのプログラミングのときにコンストラクターを見れば
そのやり方がわかります。)


では、そのpaintSelf()メソッドの部分のソース・コード

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

を見ておきましょう。

このメソッドは、このクラスが表す図形自身を描画するメソッドなので、
paintSelfという名前にしました(Selfが自身を意味します)。

このメソッドは、Graphics2Dクラスのオブジェクトを引数g2dとして受け
取るようにしてあります。

そしてg2dのsetColor()メソッドを呼び出して線の色を設定し、setStroke()
メソッドを呼び出すことによって線の種類を設定しています。

(このsetColor()メソッドは前回説明した通りGraphicsのメソッドですが、
Graphics2DはGraphicsのサブクラスなのでg2dでも実行できる訳です。
一方、setStroke()はGraphics2Dのメソッドであり、Graphicsにはこのメソッ
ドはありません。Strokeが扱えるのはGraphics2Dの高度機能の一つです。
setStroke()メソッドを呼び出し、Stroke型の引数によって線の種類を指定
しておけば、その後でGraphics2Dオブジェクトが描画したときに、その種類
の線が引かれることになります。)

最後にg2dのdraw()メソッドを呼び出していますが、このGraphics2Dのdraw()
メソッドは、引数に指定された図形(Shapeインターフェースを実装したもの)
を描画するものです。
(これもGraphics2Dのメソッドであり、Graphicsにはありません。
paintSelf()メソッドがGraphicsではなくGraphics2Dのオブジェクトを引数に
指定しているのは、このdraw()メソッドとsetStroke()を使いたいためです。)



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

では、続いてDecoratedFreeCurveを作成しましょう。


前回は、マウスをドラッグするときにマウスから得られる複数の点の間
を線分でつなぎ合わせることによって、フリーハンドの曲線(のように
見える折れ線)を描きましたが、このときのように、複数の点を線で
結んでいったものをパス(pathまたはgeometric path:幾何学的パス
(pathは本来、経路とか軌道という意味))と呼ぶことがあり、Javaには
このパスを表現するGeneralPathというクラス(java.awt.geomパッケー
ジに入っている)があります。

GeneralPathを使うと、各点を2次曲線または3次のベジェ曲線
(Bezier curve)を使って結び付けることができ、前回描いた
(折れ線による)曲線よりもなめらかな曲線を表現することが
できます。

(ベジェ曲線についての説明は省略します。必要な人はウィキペ
ディア(http://ja.wikipedia.org/)などを使って自分で調べて
ください。
この他、GeneralPathでは、各点を直線で結ぶことによって折れ線
を作ったり、あるいは閉じた図形(多角形など)を作ることもでき
ます。自由自在に図形を描くことができるのです。ちなみに、閉じた
図形を作るためには最後にclosePath()というメソッドを呼び出し
ます。)


今回は、このGeneralPathに3次のベジェ曲線を使用することに
します。


では、GeneralPathを使って自由曲線を表現するクラスDecoratedFreeCurve
をjp.co.flsi.lecture.cgパッケージの中に作りましょう。


(1) Eclipseのパッケージ・エクスプローラーの中のJStudy1の配下の
srcの配下のjp.co.flsi.lecture.cg(パッケージ名)を右クリックし、
「新規」→「クラス」を選択します。

(2) 「新規Javaクラス」ウインドウにおいて「ソース・フォルダー」欄に
「JStudy1/src」が入力されていることを確認し、「パッケージ」欄に
「jp.co.flsi.lecture.cg」が入力されていることを確認し、
「名前」の欄に

DecoratedFreeCurve

と入力し、「完了」ボタンをクリックします。


DecoratedFreeCurve.javaのエディターが開きましたら、以下のように
ソース・コードを編集しましょう。

--------------------------------------------------------
package jp.co.flsi.lecture.cg;
import java.awt.geom.GeneralPath;

public class DecoratedFreeCurve extends DecoratedShape {
  
   public DecoratedFreeCurve() {
      setShape(new GeneralPath());
   }
  
   public void addCurve(float x1, float y1, float x2, float y2, float x3, float y3) {
      ((GeneralPath)getShape()).curveTo(x1, y1, x2, y2, x3, y3);
   }
}
--------------------------------------------------------

このDecoratedFreeCurveクラスはDecoratedShapeのサブクラスに
していますので、ほとんどのメンバー(メソッドやフィールド)は
DecoratedShapeによって定義済みです。
そのため、随分簡単なソース・コードになっています。

まず、このソース・コードに定義されているコンストラクターでは
(先ほどDecoratedShapeを作成したときにお話ししたように)
      setShape(new GeneralPath());
というメソッド呼び出しを行っています。

こうしておけば、DecoratedFreeCurveのインスタンス生成時に必ずGeneralPath
のインスタンスが生成されてそれがshapeフィールドに設定されます。
(ちなみにGeneralPathはShapeインターフェースを実装しています。)

したがって、このDecoratedFreeCurveのインスタンスの描画時(DecoratedShape
のpaintSelf()メソッドの実行時)には必ずGeneralPathの描画が行われること
になるわけです。



さて前回は、直線(線分)の末尾に直線(線分)を描き加えていくというふうに、
マウスをドラッグするたびに直線(線分)をどんどんと追加することによって
フリーハンドの曲線を描いていましたが、今回は直線の代わりに曲線(ベジェ曲線)
を描き加えていくことになります。
上のソース・コードの最後のaddCurve()というメソッドが曲線(ベジェ曲線)
を追加するためのメソッドになっています。

GeneralPathでは、ベジェ曲線を追加するためのメソッドとして
curveTo(x1, y1, x2, y2, x3, y3)というメソッドが提供されており、
このメソッドを繰り返し呼び出すことによって、既に描かれているパス
(自由曲線)の末尾にどんどんと曲線(ベジェ曲線)が追加されていくこと
になっています。
curveTo(x1, y1, x2, y2, x3, y3)を実行すると、その時点までに描かれているパス
の末尾に、点(x3, y3)までつながるベジェ曲線が追加されますが、そのときの曲がり
具合は点(x1, y1)と点(x2, y2)によって制御されます。
実は、追加される曲線は点(x1, y1)と点(x2, y2)の近くを曲がりますが、それらの点
を通過するわけではありません。これによって、既に描かれているパスと追加される
曲線のつながり部分がなめらかに調整され、全体的に自然な一つながりのなめらかな
自由曲線に見えるようになります。

したがって、マウスをドラッグしているときに次々と検知されるマウスの位置情報
(から取り出した点の座標)を次々に引数にしてaddCurve()メソッドの実行を
繰り返していけば、マウスの奇跡がなめらかな自由曲線になっていきます。



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

では、マウスをドラッグしたときの軌跡をこのDecoratedFreeCurveオブジェクト
に記憶させ、それを画面上に描画するようにお絵描きソフトを作成しましょう。

前回はアプレット(JAppletのサブクラス)に直接お絵描きの機能を持たせて
いましたが、今回はパネル(JPanelのサブクラス)にお絵描きの機能を持たせ、
これをアプレット(JAppletのサブクラス)に貼り付けて使用することにします。

そうすると、このお絵描きパネルはあとでフレーム(JFrameのサブクラス)に
貼り付けて(アプレットではなく)アプリケーションとしても使用することが
できます。

つまり、お絵描きの機能を部品化するわけです。


では、以下のようにしてJPanelのサブクラスを作りましょう。

(1) パッケージ・エクスプローラーの中のJStudy1の配下のsrcの配下の
jp.co.flsi.lecture.cg(パッケージ名)を右クリックし、
「新規」→「その他」を選択します。

(2) 「新規」ウインドウにおい「WindowBuilder」配下の
「Swing Designer」配下の「JPanel」を選択し、「次へ」ボタンを
クリックします。

(3) 「New JPanel」ウインドウにおいて「ソース・フォルダー」欄に
「JStudy1/src」が入力されていることを確認し、「パッケージ」欄に
「jp.co.flsi.lecture.cg」が入力されていることを確認し、
「名前」の欄に

DrawingPanel

と入力し、「完了」ボタンをクリックします。


DrawingPanelのエディターが開いたら、以下のようにソース・コード
を編集しましょう。

(1) 以下のフィールドを追加します。

--------------------------------------------------------
private Color lineColor = Color.BLUE;
private Stroke drawStroke = new BasicStroke(1f);
private Point startPoint = null;
private Point endPoint = null;
private Point[] pts = new Point[10000];
private int ptIndex = 0;

private ArrayList<DecoratedShape> shapes = new ArrayList<DecoratedShape>();

private boolean mousePressed = false;

private enum ShapeEnum {
   FREECURVE,
   LINE,
   RECTANGLE,
   ELLIPSE;
}

private ShapeEnum shapeType = ShapeEnum.FREECURVE;
--------------------------------------------------------

ここでPointというのはjava.awtパッケージにはいっているクラスで、
点の座標の情報を持つクラスです。
前回は、int型の変数でx座標とy座標を表しましたが、これから点の
座標を頻繁に使っていきますので、Pointクラスを使ってフィールドの
定義を簡潔にしておきます。

startPointというフィールドは図形を描き始める始点を表します。

またendPointというフィールドは図形を描き終わる終点を表します。

ptsというPoint型の配列は、マウスをドラッグしたときの軌跡の各点を記憶
しておくための配列です。配列ではなくVectorやArrayListなどを使ったほうが
いいのですが、あとで自由曲線のオブジェクト自身を入れるArrayListを用意
しますので、それと混同したりしないように、マウスの軌跡の各点のほうは
配列に入れることにしました。
これをArrayListに変えてもっとすっきりさせる作業は、皆さんの任意の自主演習
としておきます。
(ArrayListは(038号などでお話ししましたが)Vectorによく似たコレクション
(collection)のクラスで、Vectorと同じように使え、しかもVectorよりも性能
がいいので、最近ではArrayListが好んで使われています。当メールマガジンでも
今回からArrayListに切り替えていきます。)

なお、配列の要素数を10000にしているのは充分な要素数と思われる数値を適当
に設定したものです。人によっては延々とドラッグを続けて、そのうちに点の数
が配列の要素数を越えてしまうこともあるかもしれませんが(そこまでドラッグ
するためにはかなりの根性が要ると思いますが)、要素数を越えて配列の要素に
アクセスしようとするとArrayIndexOutOfBoundsExceptionというエラー(Exception
=例外)が発生することになります。根性がありすぎてArrayIndexOutOfBoundsException
が発生した人は、配列をArrayListに変えてください。
(ArrayListに変える作業のほうが根性は要らないと思います。)

その下のptIndexというフィールドは、現在ptsの配列の中で何番目の要素まで
点のデータが記憶されたかを示す数値です。初期値は0にしておきます。

その下のshapesというArrayListオブジェクトは、描いた図形(まずは自由曲線を
扱うが、あとで一般の図形を扱えるようにDecoratedShape型を指定してある)を
複数保管しておくための入れ物です。

その下のmousePressedというboolean型のフィールドは、現在マウスのボタンが
押されている状態(つまりドラッグの途中の状態)なのか、それともマウスの
ボタンが離されている状態なのかを表す変数で、ボタンが押されている状態で
あればtrue、そうでなければfalseとします。初期値はfalseにしておきます。


その下にあるenumというのは、一種の型を定義するもので、ここではShapeEnum
という型を定義してあります。(enumはenumerated type(=列挙型)という言葉
を略して作られたキーワードです。)

そしてその定義の中に

private enum ShapeEnum {
   FREECURVE,
   LINE,
   RECTANGLE,
   ELLIPSE;
}

のように文字列のリストを書いておくと、それらの文字列に番号付けが
行われます。
例えば、上記の例ではFREECURVE、LINE、RECTANGLE、ELLIPSEにそれぞれ
0、1、2、3という番号が付きます。つまり、リストの先頭から順番に0から
始まる番号が自動的に付くのです。
あるいは、別の番号を割り当てることもできます。(こういった番号の使い方
など、詳しいことは後の記事で説明します。)

しかしながら、今回は番号は使わずに、FREECURVE、LINE、RECTANGLE、
ELLIPSEという文字列を直接使うことにします。これらはそれぞれ日本語に
訳すと自由曲線、直線、矩形、楕円という意味になりますね。このことから
分かるように、これらは、現在描いている図形の種類を記憶するために定義
したのです。

その下にある

private ShapeEnum shapeType = ShapeEnum.FREECURVE;

というフィールドは、先ほど定義したShapeEnum型の変数としてshapeType
というフィールドを定義したものです。
このshapeTypeフィールドに現在描いている図形の種類を記憶させます。

初期値としてShapeEnum.FREECURVEを代入していますが、これはデフォルトを
自由曲線を描く状態にしたいためです。

このようにenum型であるShapeEnum型に定義した変数には、ShapeEnumの定義の中
で列挙したものしか代入できません。つまり、ShapeEnum.FREECURVE、ShapeEnum.LINE、
ShapeEnum.RECTANGLE、ShapeEnum.ELLIPSEのうちのどれかしか代入できません。
それら以外のものを代入しようとすると、エラーになります。(Eclipseでプログラム
を編集しているときにコンパイル・エラーで赤色のマークが付きます。)

このようにエラーになることによって、プログラムのミスを事前に防いでくれますので、
enumは有用なのです。
また、図形の種類を数字などで表現するよりは文字列で表現したほうが、はるかに
分かりやすいですから、enumはソース・コードを分かりやすく読みやすくするという
効果もあります。(例えば、上のshapeTypeをint型にして、値が0のときは自由曲線、
1のときは直線、2のときは矩形・・・などとしていると、いちいち数字の意味を
覚えておかなければソース・コードを読めなくなってしまうし、プログラミング
時にも間違いをおかしやすくなってしまいます。)


(2) 次にDesignビューでJPanel(灰色の四角形)の中を右クリック
し、「Add event handler」→「mouse」→「mousePressed」
を選択します。

すると、下記のソース・コードが生成されますね。
--------------------------------------------------------
      addMouseListener(new MouseAdapter() {
         @Override
         public void mousePressed(MouseEvent e) {
         }
      });
--------------------------------------------------------

この部分を次のように書き換えましょう。

--------------------------------------------------------
      addMouseListener(new MouseAdapter() {
         @Override
         public void mousePressed(MouseEvent e) {
            startPoint = new Point(e.getX(), e.getY());
            mousePressed = true;
         }

         @Override
         public void mouseReleased(MouseEvent e) {
            endPoint = new Point(e.getX(), e.getY());
            mousePressed = false;
            repaint();
         }

      });
--------------------------------------------------------

このmousePressed()メソッドでは、startPoint変数に現在のマウスの位置
(x座標とy座標)を表すPointオブジェクトを記憶させています。また
mousePressedをtrueにすることによって、マウスのボタンが押されたこと
を記憶しています。
マウスのボタンを押すということはドラッグの始まりですから、このPoint
オブジェクトは図形を描くときの始点を意味しますね。
なお、この2つの引数を持つPointのコンストラクターでは、第一引数にはx座標、
第二引数にはy座標を指定します。

mouseReleased()メソッドは、マウスのボタンが離されたときに実行される
メソッドです。
同じようにendPoint変数に現在のマウスの位置(x座標とy座標)を表すPointオ
ブジェクトを記憶させてから、mousePressedをfalseにすることによって、マウ
スのボタンが離されたことを記憶しています。

mouseReleased()メソッドの最後でrepaint()メソッドを呼び出していることに
注意してください。このようにrepaint()はプログラムの中で呼び出してもかま
わないのです。repaint()メソッドが呼び出されると、さきほどお話したように
自動的にpaint()メソッドが呼び出されることになります。
(paint()メソッドはあとで編集します。)


(3) 再度Designビューでアプレット(灰色の四角形)の中を右クリックし、
「Add event handler」→「mouseMotion」→「mouseDragged」を選択します。

すると、下記のソース・コードが生成されますね。

--------------------------------------------------------
      addMouseMotionListener(new MouseMotionAdapter() {
         @Override
         public void mouseDragged(MouseEvent e) {
         }
      });
--------------------------------------------------------

の部分を次のように書き換えましょう。

--------------------------------------------------------
      addMouseMotionListener(new MouseMotionAdapter() {
         @Override
         public void mouseDragged(MouseEvent e) {
            if(shapeType == ShapeEnum.FREECURVE) {
               pts[ptIndex] = new Point(e.getX(), e.getY());
               ptIndex++;
            }
            repaint();
         }
      });
--------------------------------------------------------

とりあえずは自由曲線のみの描画を行うのでshapeTypeがShapeEnum.FREECURVEである
ことをわざわざ確認する必要はありませんが、あとで直線や矩形などの図形も描け
るようにするときのために、前もって自由曲線のときのみに行われる処理をif文
のブロックで囲んでいます。
この処理では、配列ptsのptIndex番目の要素に現在のマウスの位置(x座標とy座標)
を表すPointオブジェクトを記憶させた後、ptIndexの値を1増やしていますね。
そのあとrepaint()メソッドを呼び出しています。

repaint()メソッドはmouseDragged()メソッドの中でもmouseReleased()メソッドの
中でも呼び出されていますが、このあと編集するpaint()メソッドではどちらのメソ
ッドから呼び出されているのかが問題になります。これは、mousePressedの値が
trueになっているかfalseになっているかを調べればわかりますね。


(4) 以下のようにpaint()メソッドを追加します。

--------------------------------------------------------
   public void paint(Graphics g) {
      Graphics2D g2d = (Graphics2D) g;
      Image image = createImage(getSize().width, getSize().height);
      Graphics2D g2dImage = (Graphics2D) image.getGraphics();

      DecoratedShape aShape = null;

      if(shapeType == ShapeEnum.FREECURVE) {
         aShape = new DecoratedFreeCurve();

         if (ptIndex > 9) {
            ((GeneralPath)aShape.getShape()).moveTo(pts[0].x, pts[0].y);
            for(int i = 3; i < ptIndex - 6; i += 9) {
               ((DecoratedFreeCurve)aShape).addCurve(pts[i].x, pts[i].y, pts[i+3].x, pts[i+3].y, pts[i+6].x, pts[i+6].y);
            }
         }
         aShape.setColor(lineColor);
         aShape.setStroke(drawStroke);
      }

      if (aShape != null) {
         if (!mousePressed) {
            shapes.add(aShape);
            ptIndex = 0;
         }
         else {
            aShape.paintSelf(g2dImage);
         }
      }

      for (int i = 0; i < shapes.size(); i++) {
         shapes.get(i).paintSelf(g2dImage);
      }

      g2d.drawImage(image, 0, 0, getSize().width, getSize().height, null);
   }
--------------------------------------------------------

paint()メソッドはGraphics型の引数を持っていますが、これはJavaのシステム
(JVM)が与えてくれるものです。(paint()メソッドはJavaのシステムによって
呼び出されます。)
したがって、paint()メソッドを実装するときには、前回のようにgetGraphics()
メソッドを呼び出してGraphicsオブジェクトを取り出したりせずに、引数の
Graphicsオブジェクトを使うというルールになっています。

このGraphicsオブジェクトはGraphics型として渡されてきますが、実際は
GraphicsのサブクラスであるGraphics2Dのインスタンスです。
したがって、Graphics2DにキャストしてやればGraphics2D型のオブジェクトと
して使えるのです。
最初のコード
--------------------------------------------------------
Graphics2D g2d = (Graphics2D) g;
--------------------------------------------------------
がそれを行っています。

その後このg2dに絵を表示させようというわけですが、そのままg2dに絵を描かせ
ようとすると、ディスプレイ(スクリーン)に絵を描いている途中の線分が一つ
一つ増えていく過程が見えてきて・・・というのはウソで、動きが速いからそこ
まで細かいところは見えないと思いますが、少なくともスクリーン上で画像がちら
ついて見えたりします。

これでは格好良くないので、最初はメモリー上だけに絵を描いていって、絵が
完成したらそれを一挙にg2dに表示させることにします。
このようにスクリーンに表示する前にいったんメモリー上に描いておく画像の
オブジェクトをオフスクリーン描画イメージと呼ぶことがあります。これの実体
はImageというクラスのオブジェクトです。Imageというクラスはjava.awtパッ
ケージにはいっています。

このオフスクリーン描画イメージを取り出すためのメソッドがcreateImage()で、
このメソッドの引数には、画像のサイズ(横幅, 高さ)を指定します。

そこで、次の行

--------------------------------------------------------
Image image = createImage(getSize().width, getSize().height);
--------------------------------------------------------

では、引数の横幅、高さにそれぞれgetSize().widthとgetSize().heightを
指定していますが、このgetSize()というメソッドはGUI部品(ここではJPanel)
のサイズを取得するメソッドで、サイズはDimension型(java.awtパッケージに
入っているDimensionクラス)のオブジェクトとして取り出されます。
そして、そのDimensionオブジェクトのwidthとheightというフィールドが
それぞれサイズの横幅と高さを値として持っています。
したがって、createImage(getSize().width, getSize().height)というメソッド
呼び出しをすれば、このJPanelのサイズと同じ横幅・高さを持つオフスクリーン描画
イメージを生成してくれるわけです。


ただし、Imageオブジェクト自身は描画のためのメソッドを持っていないので、
次の行

--------------------------------------------------------
Graphics2D g2dImage = (Graphics2D) image.getGraphics();
--------------------------------------------------------

のようにしてImageオブジェクトからGraphicsオブジェクト(ここではGraphics2D
にキャストしている)を取り出してGraphicsまたはGraphics2Dの描画メソッドを
使うという手順をふみます。

その後、DecoratedFreeCurveのインスタンスを生成してDecoratedShape型の変数
aShapeに代入

--------------------------------------------------------
aShape = new DecoratedFreeCurve();
--------------------------------------------------------

した後、

--------------------------------------------------------
         if (ptIndex > 9) {
            ((GeneralPath)aShape.getShape()).moveTo(pts[0].x, pts[0].y);
            for(int i = 3; i < ptIndex - 6; i += 9) {
               ((DecoratedFreeCurve)aShape).addCurve(pts[i].x, pts[i].y, pts[i+3].x, pts[i+3].y, pts[i+6].x, pts[i+6].y);
            }
         }
--------------------------------------------------------

というコードによって、マウスをドラッグしたときの軌跡の各点(pts[i])に渡って
aShapeに曲線を追加しています。

このうち先頭の

            ((GeneralPath)aShape.getShape()).moveTo(pts[0].x, pts[0].y);

というコードは、自由曲線の始点を設定するものです。このようにGeneralPathの
最初の始点は moveTo(x座標, y座標) というメソッドを使って指定します。

なお、マウスの検出能力の限界によって生じるギザギザを抑えるために、使用する
点を少し間引いています。(点の間隔を細かくし過ぎると、曲線がギクシャク
してしまいますので。)
addCurve()メソッドの引数に指定している配列のインデックスがi, i+3, i+6
というふうに飛び飛びに増えているのはそのためです。
また、その関係で、ptIndexの値が9を超えた時に初めてこの処理を開始するように
if文で制御しています。

この段階でも描かれる曲線にまだ少しギザギザが見られると思いますが、さらに
ギザギザを抑えるテクニックがありますので、今後の記事で組み込んでいきます。


その次の行

--------------------------------------------------------
aShape.setColor(lineColor);
aShape.setStroke(drawStroke);
--------------------------------------------------------

では、aShapeオブジェクトに描画の線の色と太さを記憶させています。

その次の

--------------------------------------------------------
if (aShape != null) {
   if (!mousePressed) {
      shapes.add(aShape);
      ptIndex = 0;
   }
   else {
      aShape.paintSelf(g2dImage);
   }
}
--------------------------------------------------------

においては、aShapeが空でない(自由曲線が描かれている)ことを条件に、
マウスのボタンが離されているときはshapesつまり今までに描いた自由曲線
を複数記憶しているArrayListオブジェクトに、現在描かれている自由曲線を
追加します。
つまりマウスをドラッグしてボタンを離すたびにshapesに自由曲線が追加さ
れることになります。
そしてそのあとptIndexを0にしていますが、これは配列ptsをリセットして、これ
からまた新しい自由曲線を受け入れていく状態にすることを意味します。
一方、マウスのボタンが押されているときはドラッグをしている途中を意味する
ので、aShape.paintSelf(g2dImage)の実行によって現在ドラッグして描いて
いる自由曲線を直ちにg2dImageに描画させます。

その次

--------------------------------------------------------
      for (int i = 0; i < shapes.size(); i++) {
         shapes.get(i).paintSelf(g2dImage);
      }
--------------------------------------------------------

では、これまでにshapesに記憶されたすべての自由曲線をg2dImageに描画さ
せています。

最後の行

--------------------------------------------------------
g2d.drawImage(image, 0, 0, getSize().width, getSize().height, null);
--------------------------------------------------------

では、g2dImageに描画されてメモリー上に記憶された画像をg2dに描画させています。
これはディスプレイ(スクリーン)に表示されることを意味します。

こうしてメモリー上に描き続けられた絵が完成してから一挙にスクリーンに表示さ
れるということになります。

なおdrawImage()メソッドの第一引数にはImage型のオブジェクトを指定しますので、
ここではg2dImageではなくimageを指定しています。image(Image型)はg2dImage
(Graphics2D型)の実体でしたね。
第二引数、第三引数には描画する位置のx座標とy座標を指定し、第四引数、第五引数
には描画する領域の横幅と高さを指定しますが、ここではx座標とy座標はどちらも0
を指定し、横幅と高さには先ほど説明したgetSize().widthとgetSize().heightを
指定しています。したがって、メモリー上に記憶された画像は、このJPanel全体に
描画されることになります。



さて、今の段階ではアプリケーションとしてはまだ不完全ですが、この時点で
DrawingPanelだけをテストすることもできます。
つまり、部品だけのテストもできるのです。

保管した後、パッケージ・エクスプローラーの中のJStudy1の配下のsrcの配下の
jp.co.flsi.lecture.cgの配下のDrawingPanel.javaを右クリックし、
「実行」→「Java Bean」を選択し、お絵描きのテストをしてみてください。
(ウインドウがへしゃげて表示されるはずなので、自分でウインドウのサイズを
(ウインドウのふちをドラッグすることによって)広げて調整して下さい。)

前回と同様にお絵描きができることを確認しましょう。さらに、このウイン
ドウを最小化した後、再度表示させてみると、前回のアプレットとは違って、
描いた絵がちゃんと再現されることがわかりますね。



では次に、線の色や太さを変えられるようにDrawingPanelのソース・コードを
編集しましょう。


今回はここまで。


(続く)



========================================================
◆ 02.文法解説 [アプレット]
========================================================

[アプレット(1)「サンドボックス・モデル」]

アプレットは前回も説明したように、Webページの中に組み込まれ、Webページと
いっしょにインターネットからダウンロードされてからWebブラウザーで実行さ
れるという形式のプログラムです。
インターネットからダウンロードされるプログラムにはウイルスのように危険な
ものが存在する可能性がありますので、Javaではアプレットの安全性を守るため
に、次のようなセキュリティー上の制限をもうけています。
・アプレットは、アプレットが実行されているコンピューター上のネイティブな
プログラムを実行することはできません。
・アプレットは、アプレットが実行されているコンピューター上のファイルを
読み書きすることはできません。
・アプレットは、ダウンロード元以外のコンピューターとは通信することはでき
ません。

(ただし、自分のパソコンにはいっているアプレットを自分のパソコン内で実行
しようとした場合はこのような制限はありません。)

これらの制限は基本的にはJVMがコントロールしています。セキュリティーを破る
ようなアプレットを開発してもJVMが阻止するのです。
このように、アプレットをJVMが隔離してそのコンピューターに悪さをできない
ようにするやり方を「サンドボックス・モデル(sandbox model)」と呼ぶことが
あります。
sandboxは直訳すると「砂箱」ですが、子供が遊ぶ砂場のことです。子供がいじく
り回せる場所がsandboxの中に限られるように、アプレットもいわばsandboxの中に
隔離されているというわけです。


(続く)



================================================
◆ 03.任意演習問題
================================================

DrawingPanelのPoint型の配列pts

--------------------------------------------------------
private Point[] pts = new Point[10000];
--------------------------------------------------------

をArrayListに置き替えるようにプログラムを変更してみてください。
やり方がわからない人は質問をお寄せください。


(次回に続く。)



┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
★ホームページ:
      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) 2011 Future Lifestyle Inc. 不許無断複製