■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
                      2007年07月15日

    楽しいJava講座 - 初心者から達人へのパスポート
                  vol.062

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



[このメールマガジンは、画面を最大化して見てください。]


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


これまでに作ってきたお絵描きソフトは、図形を描くことはでき
ても間違って描いてしまった図形を削除することができませんで
した。
というわけで、今回は、一度描いた図形を削除できるように機能
を追加しましょう。


その前に、自由曲線のギザギザがまだ取り除かれていませんでし
たね。
というわけで、先に、自由曲線のギザギザを取り除くようにプロ
グラムを改良しておきましょう。

これまでは、自由曲線を小さな直線(線分)を多数連ねることに
よって表現してきましたが、これは正確には曲線ではなく、折れ線
です。

この連ねていた直線を曲線に置き換えることによって、ギザギザ
を取り除くことにします。

ところで、複数の点を線で結んでいったものをパス(pathまたは
geometric path:幾何学的パス(pathは本来、経路とか軌道という
意味))と呼ぶことがあり、java.awt.geomパッケージの中にはパス
を表現するGeneralPathというクラスがあります。

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

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

では、GeneralPathを使って自由曲線を表現するクラスDecoratedFreeCurve
をjp.co.flsi.lecture.cgパッケージの中に作りましょう。ソース・
コードは下記のようにします。Eclipseを起動して作成してください。
--------------------------------------------------------
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によって定義済みです。
したがって、DecoratedFreeCurveはDecoratedLinesに比べて、
メンバーが大幅に減っています。

また、DecoratedLinesが持っていた、線分を追加するためのメソッド
addLine()は、ここでは曲線を追加するメソッドaddCurve()に代わって
います。直線(線分)の代わりに曲線(ベジェ曲線)を使うので、
メソッド名もそれにふさわしい名前に変えたのです。
そして、曲線(ベジェ曲線)を追加するためには、GeneralPathの
curveTo(x1, y1, x2, y2, x3, y3)というメソッドを使用します。
このメソッドを使用すると、その時点までに描いているパスの末尾に、
点(x3, y3)までつながるベジェ曲線が追加されますが、そのときの曲が
り具合は点(x1, y1)と点(x2, y2)によって制御されます(曲線は点(x1, y1)
と点(x2, y2)の近くを曲がりますが、それらの点を通過するわけでは
ありません)。

こうしてどんどんと曲線を追加していくと、DecoratedLinesのときよりも
なめらかな自由曲線ができます。


では、これまでのDecoratedLinesをDecoratedFreeCurveに置き換える
べく、GraffitiPanelのソース・コードを編集しましょう。
GraffitiPanelのエディターを開いて、そのソース・コードの中で
DecoratedLinesと書かれている部分をことごとくDecoratedFreeCurveに
書き換えましょう。「検索/置換」の機能(メニュー・バーの「編集」→
「検索/置換」またはCtrl+F)を使うと便利です。

そうすると、エラーになる行が出てきますね。paintImage()メソッドの中の
--------------------------------------------------------
for(int loopIndex = 0; loopIndex < ptIndex - 1; loopIndex++) {
   aDecoLines.addLine(new Line2D.Float(pts[loopIndex].x, pts[loopIndex].y, pts[loopIndex+1].x, pts[loopIndex+1].y));
}
--------------------------------------------------------
という行です。
このうちの真ん中の行のaddLine()というメソッド名にエラーのマーク
(赤いアンダー・ライン)がついていますが、このメソッド呼び出しを
DecoratedFreeCurveのaddCurve()の呼び出しに書き換えなければなりま
せんね。
この3行を以下のように書き換えましょう。
--------------------------------------------------------
if (ptIndex > 9) {
   ((GeneralPath)aDecoLines.getShape()).moveTo(pts[0].x, pts[0].y);
   for(int i = 3; i < ptIndex - 6; i += 9) {
      aDecoLines.addCurve(pts[i].x, pts[i].y, pts[i+3].x, pts[i+3].y, pts[i+6].x, pts[i+6].y);
   }
}
--------------------------------------------------------

GeneralPathの最初の始点は上記のようにmoveTo()メソッドを使って指定
します。

なお、マウスの検出能力の限界によって生じるギザギザを抑えるために、
使用する点を少し間引いています。
addCurve()メソッドの引数に指定している配列のインデックスがi, i+3, i+6
というふうに飛び飛びに増えているのはそのためです。


ついでに、もう少しなめらかな感じを増すように別の工夫を加えてみま
しょう。

GraffitiJFrameのソース・コードを開き、getStrokeList()メソッドの中
の以下の行
--------------------------------------------------------
switch (strokeList.getSelectedIndex()){
case 0:
   getGraffitiPanel().setDrawStroke(new BasicStroke(1f));
   break;
case 1:
   getGraffitiPanel().setDrawStroke(new BasicStroke(2f));
   break;
case 2:
   getGraffitiPanel().setDrawStroke(new BasicStroke(5f));
   break;
}
--------------------------------------------------------
を次のように書き換えてみましょう。
--------------------------------------------------------
switch (strokeList.getSelectedIndex()){
case 0:
   getGraffitiPanel().setDrawStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
   break;
case 1:
   getGraffitiPanel().setDrawStroke(new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
   break;
case 2:
   getGraffitiPanel().setDrawStroke(new BasicStroke(5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
   break;
}
--------------------------------------------------------
ここで、BasicStroke()コンストラクターの引数に指定している
BasicStroke.CAP_ROUNDというのは、線の端を丸くすることを意味
し、BasicStroke.JOIN_ROUNDは線と線のつなぎ部分に丸みをおびさせる
ことを意味します。
(丸くする以外にもいくつかの指定が可能ですが、必要な人はAPIリファ
レンスなどで調べてみてください。)


ソース・コードを保管したら、一度GraffitiJFrameを実行してテストして
みてください。


さて、GraffitiPanelのpaintImage()メソッドのソース・コードをもう一度
見てみましょう。

--------------------------------------------------------
DecoratedShape aShape = null;
DecoratedFreeCurve aDecoLines = null;
--------------------------------------------------------
という2つの行のうちaDecoLinesという変数は、DecoratedFreeCurveが
DecoratedShapeのサブクラスになったためにaShapeで代用してもかまわない
はずです。

というわけで、まず、
--------------------------------------------------------
DecoratedFreeCurve aDecoLines = null;
--------------------------------------------------------
の行を削除してみましょう。すると、aDecoLinesの場所がことごとくエラーに
なるはずです。そのaDecoLinesをことごとくaShapeに置換しましょう。
「検索/置換」の機能(メニュー・バーの「編集」→「検索/置換」またはCtrl+F)
を使うと便利です。

すると、また別の行がエラーになりますね。
一つ目は、
--------------------------------------------------------
aShape.addCurve(pts[i].x, pts[i].y, pts[i+3].x, pts[i+3].y, pts[i+6].x, pts[i+6].y);
--------------------------------------------------------
の行です。
この行は、
--------------------------------------------------------
((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);
--------------------------------------------------------
というふうにキャストを使って修正しましょう。

次は、
--------------------------------------------------------
freeCurves.add(aShape);
--------------------------------------------------------
の行です。
DecoratedFreeCurveはDecoratedShapeのサブクラスですから、freeCurves
ではなくshapesに追加すればすみます。したがって、この行を
--------------------------------------------------------
shapes.add(aShape);
--------------------------------------------------------
に書き換えましょう。

そうすると
--------------------------------------------------------
if (aShape != null) {
   if (!mousePressed) {
      shapes.add(aShape);
      ptIndex = 0;
   }
   else {
      aShape.paintSelf(g2dImage);
   }
}
if (aShape != null) {
   if (!mousePressed) {
      shapes.add(aShape);
   }
   else {
      aShape.paintSelf(g2dImage);
   }
}
--------------------------------------------------------
という、重複したロジックのコードができてしまって、変ですね。
これらの行は、
--------------------------------------------------------
if (aShape != null) {
   if (!mousePressed) {
      shapes.add(aShape);
      ptIndex = 0;
   }
   else {
      aShape.paintSelf(g2dImage);
   }
}
--------------------------------------------------------
というふうに変えて、重複をなくしましょう。

また、その下の
--------------------------------------------------------
for (DecoratedFreeCurve lines : freeCurves) {
   lines.paintSelf(g2dImage);
}
--------------------------------------------------------
という行は不要になりました(もはや、freeCurvesに追加されるもの
は何もありませんから)。したがって、削除してください。


念のため、現時点のpaintImage()メソッドのソース・コード全体を
下に提示しておきます。
--------------------------------------------------------
public void paintImage(Image image) {
   Graphics2D g2dImage = (Graphics2D) image.getGraphics();
   g2dImage.drawImage(getJpegImage(), 0, 0, 300, 200, null);
     
   g2dImage.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

   DecoratedShape aShape = null;

   if(freeCurveFlag) {
      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);
   }
   else if(endPoint != null) {
      if (lineFlag) {
         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(ellipseFlag) {
            aShape = new DecoratedEllipse(tempStartPoint.x, tempStartPoint.y, drawWidth, drawHeight);
         }
         else if(rectFlag) {
            aShape = new DecoratedRectangle(tempStartPoint.x, tempStartPoint.y, drawWidth, drawHeight);
         }
      }
      aShape.setColor(lineColor);
      aShape.setStroke(drawStroke);
   }

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

   for (DecoratedShape shape : shapes) {
      shape.paintSelf(g2dImage);
   }
}
--------------------------------------------------------


そうすると、
--------------------------------------------------------
private Vector<DecoratedFreeCurve> freeCurves = new Vector<DecoratedFreeCurve>();
--------------------------------------------------------
というフィールドも不要になりましたので、削除しておきましょう。

だいぶソース・コードがすっきりしてきましたね。


ここで、ソース・コードを保管したら、再度GraffitiJFrameを実行して
テストしてみてください。



では、次に、一度描いた図形を削除できるように機能を追加しましょう。


[GraffitiPanelの編集]
図形を削除するためには、その前に削除したい図形を選択しておく必要が
あります。
そのために図形の選択ができるモードを示す変数selectionFlagを用意して
おきましょう。
下記のフィールド定義
--------------------------------------------------------
private boolean selectionFlag = false;
--------------------------------------------------------
をGraffitiPanelのソース・コードの中の
--------------------------------------------------------
private boolean freeCurveFlag = true;
--------------------------------------------------------
の行の上にでも挿入しておいてください。

またselectionFlagに対するsetterとして
--------------------------------------------------------
public void setSelectionFlag(boolean b) {
   selectionFlag = b;
}
--------------------------------------------------------
をsetFreeCurveFlag()メソッドの上にでも挿入しておいてください。

次に、GraffitiPanelのソース・コードの中の
--------------------------------------------------------
private Vector<DecoratedShape> shapes = new Vector<DecoratedShape>();
--------------------------------------------------------
の行の下に、
--------------------------------------------------------
int selectedShapeIndex = -1;
--------------------------------------------------------
というフィールドを挿入しましょう。
これは、選択された図形がshapesの何番目の要素であるか(そのインデックス)
を示すためのフィールドとします。また、このフィールドの値が-1のときは、
図形が選択されていないことを意味するものとします。

また、(マウスで)示された点の座標にある図形が何番目の図形であるか
(図形のインデックス)を答えるメソッド
--------------------------------------------------------
public int searchShape(double x, double y) {
   for (int i = 0; i < shapes.size(); i++) {
      if (shapes.get(i).getShape().contains(x, y)) {
         return i;
      }
   }
   return -1;
}
--------------------------------------------------------
も追加しましょう。
上記のコードの中でcontains()というメソッドは、Shapeインターフェースの
メソッドで、引数で指定された座標がその図形の境界内にあるかどうかを
trueかfalseで答えてくれるものです。(このメソッドが返してくるインデックス
をselectedShapeIndexに代入するように後でプログラミングします。)

また、選択された図形を削除するメソッド
--------------------------------------------------------
public void removeShape() {
   if (selectedShapeIndex < 0) return;
   shapes.remove(selectedShapeIndex);
   selectedShapeIndex = -1;
}
--------------------------------------------------------
も追加しましょう。ここで呼び出しているVectorのremove()メソッドは、
指定したインデックスの要素を削除してくれます。


[DecoratedShapeの編集]
次に、選択された図形を他の図形と区別して描画するために下記のような
メソッド
--------------------------------------------------------
public void paintSelected(Graphics2D g2d) {
   g2d.setColor(Color.ORANGE);
   float dash[] = {10.0f};
   g2d.setStroke(new BasicStroke(2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1.0f, dash, 0.0f));
   g2d.draw(getShape());
}
--------------------------------------------------------
をDecoratedShapeのソース・コードの中に挿入してください。
このpaintSelected()メソッドは、オレンジ色の点線(破線)で図形を描き
ます。詳しくは、APIリファレンスなどで、BasicStrokeの使い方を調べて
ください。


[GraffitiPanelの編集]
再度GraffitiPanelに戻って、今度はマウス操作関連のメソッドの編集を
しましょう。

initialize()メソッドの中の
--------------------------------------------------------
public void mousePressed(java.awt.event.MouseEvent e) {
   startPoint = new Point(e.getX(), e.getY());
   mousePressed = true;
}
--------------------------------------------------------
の部分を
--------------------------------------------------------
public void mousePressed(java.awt.event.MouseEvent e) {
   if (selectionFlag) {
      selectedShapeIndex = searchShape(e.getX(), e.getY());
   }
   else {
      startPoint = new Point(e.getX(), e.getY());
      mousePressed = true;
   }
}
--------------------------------------------------------
のように編集しましょう。
つまり、selectionFlagがtrue(図形選択のモード)のときにマウスを
クリックすると、マウス・ポインターが示す位置にあった図形のインデッ
クスがselectedShapeIndexに代入されることになります。
また、図形選択のモードのときは図形の作成は行いませんから、余計な
処理をしないように従来の処理部分はelseのブロックの中に入れていま
す。

同様に、
--------------------------------------------------------
public void mouseReleased(java.awt.event.MouseEvent e) {
   endPoint = new Point(e.getX(), e.getY());
   mousePressed = false;
   repaint();
}
--------------------------------------------------------
の部分を
--------------------------------------------------------
public void mouseReleased(java.awt.event.MouseEvent e) {
   if (!selectionFlag) {
      endPoint = new Point(e.getX(), e.getY());
      mousePressed = false;
   }
   repaint();
}
--------------------------------------------------------
のように編集しましょう。

同様に、
--------------------------------------------------------
public void mouseDragged(java.awt.event.MouseEvent e) {
   if(freeCurveFlag) {
      pts[ptIndex] = new Point(e.getX(), e.getY());
      ptIndex++;
   }
   else {
      endPoint = new Point(e.getX(), e.getY());
   }
   repaint();
}
--------------------------------------------------------
の部分は
--------------------------------------------------------
public void mouseDragged(java.awt.event.MouseEvent e) {
   if (selectionFlag) {
      selectedShapeIndex = searchShape(e.getX(), e.getY());
   }
   else if (freeCurveFlag) {
      pts[ptIndex] = new Point(e.getX(), e.getY());
      ptIndex++;
   }
   else {
      endPoint = new Point(e.getX(), e.getY());
   }
   repaint();
}
--------------------------------------------------------
のように編集しましょう。

これで、図形選択のモードにおいて、マウスをクリックしたりドラッグ
したりすると、そこにあった図形が選択されることになります。


では、paintImage()メソッドの中も編集しましょう。
paintImage()メソッドを下記のように書き換えてください。
--------------------------------------------------------
public void paintImage(Image image) {
   Graphics2D g2dImage = (Graphics2D) image.getGraphics();
   g2dImage.drawImage(getJpegImage(), 0, 0, 300, 200, null);
     
   g2dImage.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

   DecoratedShape aShape = null;

   if(freeCurveFlag) {
      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);
   }
   else if(!selectionFlag && endPoint != null) {
      if (lineFlag) {
         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(ellipseFlag) {
            aShape = new DecoratedEllipse(tempStartPoint.x, tempStartPoint.y, drawWidth, drawHeight);
         }
         else if(rectFlag) {
            aShape = new DecoratedRectangle(tempStartPoint.x, tempStartPoint.y, drawWidth, drawHeight);
         }
      }
      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++) {
      if (i == selectedShapeIndex) shapes.get(i).paintSelected(g2dImage);
      else shapes.get(i).paintSelf(g2dImage);
   }
}
--------------------------------------------------------
変更された部分はわずか4行程度なんですが、説明するのが面倒くさく
なったのでpaintImage()メソッド全体を提示しました。
どこが変更された行か、わかりますね。(ヒント:selectionFlagや
selectedShapeIndexがからんでいるところです。)わからない人は、
質問をお寄せください。


では、次にGraffitiJFrameのほうを編集しましょう。

[GraffitiJFrameの編集]
まず最初にselectionFlagをtrue(図形選択のモード)にするためのボタン
を貼り付けましょう。
パレットからJToggleButtonを取り出し、「自由曲線」ボタンの上などの
空いているところに貼り付けましょう。Bean NameはselectionToggleButton
にしましょう。また、textプロパティーの値を「選択」にしましょう。

続いて、initialize()メソッドの中の一番下に
--------------------------------------------------------
shapeGroup.add(getSelectionToggleButton());
--------------------------------------------------------
という行を追加しましょう。
つまり、この「選択」ボタンを図形のボタンと同じグループに属させ
ます。

続いて、「Java Beans」ビューの中のselectionToggleButtonを右クリ
ックし、「Events」→「actionPerformed」を選択し、自動生成された
下記コード
--------------------------------------------------------
selectionToggleButton.addActionListener(new java.awt.event.ActionListener() {
   public void actionPerformed(java.awt.event.ActionEvent e) {
      System.out.println("actionPerformed()"); // TODO Auto-generated Event stub actionPerformed()
   }
});
--------------------------------------------------------

--------------------------------------------------------
selectionToggleButton.addActionListener(new java.awt.event.ActionListener() {
   public void actionPerformed(java.awt.event.ActionEvent e) {
      getGraffitiPanel().setSelectionFlag(true);
      getGraffitiPanel().setFreeCurveFlag(false);
      getGraffitiPanel().setLineFlag(false);
      getGraffitiPanel().setRectFlag(false);
      getGraffitiPanel().setEllipseFlag(false);
   }
});
--------------------------------------------------------
というふうに編集しましょう。

なお、今回は「自由曲線」「直線」「矩形」「楕円」の各ボタンと同じ
グループにこの「選択」ボタンを追加したのですから、「自由曲線」
「直線」「矩形」「楕円」の各ボタンの同様のメソッドについても編集が
必要になります。
したがって、
(1) getFreeCurveToggleButton()メソッドの中のactionPerformed()メソッドの中
(2) getLineToggleButton()メソッドの中のactionPerformed()メソッドの中
(3) getRectangleToggleButton()メソッドの中のactionPerformed()メソッドの中
(4) getEllipseToggleButton()メソッドの中のactionPerformed()メソッドの中
に、すべて
--------------------------------------------------------
getGraffitiPanel().setSelectionFlag(false);
--------------------------------------------------------
という行を追加してください。

(VEエディターの画像がおかしくなったら、いったんEclipseを(保管して
から)終了し、再度Eclipseを起動しなおしてください。)


では、次に図形の削除のためのメニューを追加しましょう。
パレットからJMenuを取り出し、(「Java Beans」ビューを最大化して)
「Java Beans」ビューの中のjJMenuBarをクリックします。
Bean NameはjMenuEditにしておきましょう。また、そのtextプロパティー
の値を「編集」にしておきましょう。

続いて、パレットからJMenuItemを取り出し、(「Java Beans」ビューを
最大化して)「Java Beans」ビューの中のjMenuEditをクリックします。
Bean NameはjMenuItemRemoveにしておきましょう。また、そのtextプロパ
ティーの値を「削除」にしておきましょう。

次に、)「Java Beans」ビューの中のjMenuItemRemoveを右クリックして
「Events」→「actionPerformed」を選択し、自動生成されたソース・
コード
--------------------------------------------------------
jMenuItemRemove.addActionListener(new java.awt.event.ActionListener() {
   public void actionPerformed(java.awt.event.ActionEvent e) {
      System.out.println("actionPerformed()"); // TODO Auto-generated Event stub actionPerformed()
   }
});
--------------------------------------------------------

--------------------------------------------------------
jMenuItemRemove.addActionListener(new java.awt.event.ActionListener() {
   public void actionPerformed(java.awt.event.ActionEvent e) {
      getGraffitiPanel().removeShape();
      repaint();
   }
});
--------------------------------------------------------
に書き換えましょう。


はい、これで図形を削除する機能ができました。保管したあと、
GraffitiJFrameを起動してテストしてみてください。
自由曲線や矩形や楕円を描いて、それらを削除することについては
うまくいきますが、直線を描いても選択ができませんね。どうして
でしょうか。
APIリファレンスでLine2Dのcontains()メソッドを調べてみてくだ
さい。うまくいかない理由がわかりますね。Line2Dのcontains()メソ
ッドはfalseしか返さないのです。

「そんな無意味なメソッド作るなよ」と、つい突っ込みたくなります
が、contains()メソッドはShapeインターフェースのメソッドなので必ず
実装する必要があり、かつ、直線の場合はたとえ指定した点が線の上に
乗っかっていても「contains(含む)」という言葉の意味にあわないた
め、falseしか返さないようにしているのです。
containsという言葉を使うと、図形が領域(面積的な広がり)を持って
いて、その領域の中に指定された点がはいっている、というようなイメー
ジになるからです。

実は、Line2Dについては、contains()メソッドの代わりに
ptSegDist(double x, double y)というメソッドを使う必要があります。
(このメソッド名のうち、ptはpoint(点)の略、segはsegment(線分)の略、
distはdistance(距離)の略であり、点と線分との距離という意味から
このメソッド名がつけられています。)
このメソッドは引数に指定した点と線分の間の距離を返しますので、
その距離がほぼ0に等しいときにその直線(線分)を選択するように
すればいいのです。

したがって、GraffitiPanelのsearchShape()メソッドを下記のように
修正しましょう。
--------------------------------------------------------
public int searchShape(double x, double y) {
   for (int i = 0; i < shapes.size(); i++) {
      if (shapes.get(i).getShape().contains(x, y)) {
         return i;
      }
      else if (shapes.get(i) instanceof DecoratedLine) {
         if(((Line2D) shapes.get(i).getShape()).ptSegDist(x, y) < 2.0) return i;
      }
   }
   return -1;
}
--------------------------------------------------------
ここで、instanceofというのは左辺のオブジェクトが右辺のクラスの
インスタンスであるかどうかをtrueかfalseで答える演算子です。


はい、保管したあと、再度GraffitiJFrameを起動してテストしてみて
ください。

今度は、直線もちゃんと選択できるようになりましたね。

(なお、今回の方法では、ラスター・グラフィックスのファイルから開いた
場合の絵の削除はできませんね。おわかりだとは思いますが、念のため。)


何か、わからないところがありましたら、下記のWebページまで質問を
お寄せください。


今回はここまで。


(続く)


以上、今回は
┌───────────────────────────┐
・GeneralPathの使い方
・BasicStrokeで線の端の形状や線と線のつなぎ部分の形状を指定する方法
・BasicStrokeで破線を指定する方法
・instanceof演算子
└───────────────────────────┘
を学習しました。
では、また来週。




========================================================
◆ 02.文法解説 [アプリケーションのパラメーター]
========================================================

[アプリケーションのパラメーター]

アプリケーションの起動時にも、パラメーター(引数)を渡すことが
できます。

Eclipseで自動生成されるmain()メソッドを見ると、
--------------------------------------------------------
public static void main(String[] args)
--------------------------------------------------------
というようにString型の配列の引数を指定していますね。
このようにmain()メソッドにString型の配列の引数を指定することは
Javaのルールの一つですが、この引数は、コマンド行から渡されたパ
ラメーターを受け取るために使用されます。

例えば、コマンド行(コマンドプロンプト)に
--------------------------------------------------------
java  Class01  param01  param02
--------------------------------------------------------
というふうに入力したとすると、Class01のmain()メソッドの引数に
パラメーターparam01とparam02が渡されます。
これらは、たとえば
--------------------------------------------------------
String s1 = args[0];
String s2 = args[1];
--------------------------------------------------------
というふうにして変数s1とs2にそれぞれの値を代入してやるとs1には
文字列"param01"が、s2には文字列"param02"がはいることになります。

なお、たとえパラメーターに数字を指定してもmain()メソッドには
あくまで文字列として渡されますので、数字として扱いたいときには、
IntegerのparseInt()メソッドなどを使ってint型などに変換してやる
必要があります。

下にプログラム例を挙げます。

--------------------------------------------------------
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JFrame;

public class BoundBallJFrame extends JFrame implements Runnable {
   private int height = 0;
   private int duration = 0;
   private int y = 0;
  
   public BoundBallJFrame(int h, int t) {
      super();
      height = h;
      duration = t;
      setSize(250, 250);
      setTitle("Bounding Ball");
   }

   public void run() {
      boolean droppingFlag = true;
      int timeLeft = duration * 10;
      int t = 0;
      while(timeLeft > 0) {
         repaint();
         try {
            Thread.sleep(100);
         }
         catch(InterruptedException e) {
           
         }
         timeLeft--;
         if (y > height || y < 0) {
            droppingFlag = !droppingFlag;
         }
         if (droppingFlag) {
            t++;
         }
         else {
            t--;
         }
         y = t * t;
      }
   }

   public void paint(Graphics g) {
      super.paint(g);
      g.setColor(Color.YELLOW);
      g.fillOval(100, y + 200 - height, 15, 15);
   }

   public static void main(String[] args) {
      if (args.length != 2) {
         System.out.print("エラー:パラメーターの指定に間違いがあります。");
         System.exit(1);
      }
      int h = 0;
      int t = 0;
      try {
         h = Integer.parseInt(args[0]);
         t = Integer.parseInt(args[1]);
      }
      catch (NumberFormatException e) {
         System.out.print("エラー:パラメーターが数字ではありません。");
         System.exit(2);
      }
      if (h < 0 || h > 170) h = 170;
      if (t < 0) t = 5;
      BoundBallJFrame frame = new BoundBallJFrame(h, t);
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.setVisible(true);
      Thread aThread = new Thread(frame);
      aThread.start();
   }

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

このプログラムは、バウンドするボールのアニメーションですが、
一つ目のパラメーターでボールの高さを指定し、二つ目のパラメー
ターでアニメーションの実行時間を(秒単位で)指定するようにし
ています。


なお、Eclipseでアプリケーションを実行する場合は、パラメーター
は次のようにして指定します。

(1) パッケージ・エクスプローラーの中で実行したいアプリケーション
のクラスを右クリックし、「実行」→「構成および実行」を選択します。

(2) 「構成および実行」ウインドウの中に「メイン」、「引数」、・・・
といったタブが並んでいるので、そのうちの「メイン」タブをクリック
し、「プロジェクト」欄と「メイン・クラス」欄の値が正しいことを
確認します。違っていたら、正しい値に変更してください。

(3) 「引数」タブをクリックし、「プログラムの引数」欄にパラメー
ターを入力します。パラメーターが複数ある場合は間にスペースを入れ
て入力します。
たとえば、上のBoundBallJFrameに対するパラメーターは、
150  10
というふうに入力します。

(4) 「実行」ボタンをクリックするとアプリケーションが実行されます。


なお、「構成および実行」ウインドウは実行時の設定内容を記憶して
しまいますので、その後の通常のアプリケーションの起動でも以前の
設定内容で実行されてしまいます。必要に応じて、「構成および実行」
ウインドウの設定内容を適時変更してください。



(続く)



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

上記の[アプリケーションのパラメーター]のプログラム例を実際に作成
して実行し、動作を確認してください。



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