広告

■□■□■□■□■□■□■□■□■□■□■□■□■□■□■
                      2012年03月25日

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

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


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


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

これまでに作ってきたお絵描きソフトは、図形を描くことはでき
ても、描いた図形を削除することはできません。したがって、
図形を間違えて描いてしまっても、その図形を消してから再度
描き直すといった作業ができません。

これでは不便ですから、今回は、一度描いた図形を削除できるように
機能を追加しましょう。

この削除機能は直列化の機能を使って保管したベクター・グラ
フィックスのファイルから図形を復元した場合にも使用できます。
ラスター・グラフィックスではこのような芸当はできませんので、
ここらへんもベクター・グラフィックスとラスター・グラフィックス
の違いを示す大きな特徴となります。


では、さっそく、描いた図形を削除できるようにプログラミングして
いきましょう。


[DrawingPanelの編集]
図形を削除するためには、まず、削除したい図形をクリックして選択し、
続いてメニューから「選択した図形の削除」というメニュー項目を選択
するという操作をするものとしましょう。
そのために図形の選択ができるモードを示すようにShapeEnum(enum型)
を拡張することにしましょう。

DrawingPanelのソース・コードの中の
--------------------------------------------------------
   public enum ShapeEnum {
      FREECURVE,
      LINE,
      RECTANGLE,
      ELLIPSE;
   }
--------------------------------------------------------

--------------------------------------------------------
   public enum ShapeEnum {
      SELECTION,
      FREECURVE,
      LINE,
      RECTANGLE,
      ELLIPSE;
   }
--------------------------------------------------------
のように編集して下さい。
つまり、ShapeEnumにSELECTIONという文字列を追加します。
このSELECTIONが図形の選択ができるモードを表すものとします。


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


削除したい図形を選択するためには、その図形をマウスでクリックすれば
よいのですが、そのときマウス・ポインターが示す座標にある図形が
shapesの何番目の要素であるのかを知りたい(後でその要素を削除するため)
ので、そのためのメソッドを用意しましょう。

(マウスで)示された点の座標にある図形が何番目の図形であるか
(図形のインデックス)を答えるメソッドを以下のようなコードで
--------------------------------------------------------
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;
}
--------------------------------------------------------
DrawingPanelのソース・コードの中に追加して下さい。

上記のコードの中でcontains()というメソッドは、Shapeインターフェースの
メソッドで、引数で指定された座標がその図形の境界内にあるかどうかを
trueかfalseで答えてくれるものです。(このメソッドが返してくるインデックス
をselectedShapeIndexに代入するように後でプログラミングします。)


また、選択された図形を削除するためのメソッドを以下のようなコードで
--------------------------------------------------------
public void removeShape() {
   if (selectedShapeIndex < 0) return;
   shapes.remove(selectedShapeIndex);
   selectedShapeIndex = -1;
}
--------------------------------------------------------
DrawingPanelのソース・コードの中に追加して下さい。

ここで呼び出しているArrayListの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()メソッドがオレンジ色の
点線(破線)で図形を描いてくれますので、選択した図形が他の図形とはっきり
区別できます。
上記のsetStroke()メソッドに指定している引数などの詳細については、API仕様
http://java.sun.com/javase/ja/6/docs/ja/api/index.html
などで、(BasicStrokeの使い方を)自分で調べてみてください。


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

DrawingPanel()コンストラクターの中の
--------------------------------------------------------
         @Override
         public void mousePressed(MouseEvent e) {
            startPoint = new Point(e.getX(), e.getY());
            mousePressed = true;
         }
--------------------------------------------------------
の部分を
--------------------------------------------------------
         @Override
         public void mousePressed(MouseEvent e) {
            if(shapeType == ShapeEnum.SELECTION) {
               selectedShapeIndex = searchShape(e.getX(), e.getY());
            }
            else {
               startPoint = new Point(e.getX(), e.getY());
               mousePressed = true;
            }
         }
--------------------------------------------------------
のように編集しましょう。

つまり、図形を選択するモードのときにマウスをクリックすると、
マウス・ポインターが示す位置にあった図形のインデックスが
selectedShapeIndexに代入されることになります。
また、この図形選択のモードのときは図形の作成は行いませんから、
余計な処理をしないように従来の処理部分はelseのブロックの中に
入れています。


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


同様に、
--------------------------------------------------------
         @Override
         public void mouseDragged(MouseEvent e) {
            if(shapeType == ShapeEnum.FREECURVE) {
               pts[ptIndex] = new Point(e.getX(), e.getY());
               ptIndex++;
            }
            else {
               endPoint = new Point(e.getX(), e.getY());
            }
            repaint();
         }
--------------------------------------------------------
の部分は
--------------------------------------------------------
         @Override
         public void mouseDragged(MouseEvent e) {
            if (shapeType == ShapeEnum.SELECTION) {
               selectedShapeIndex = searchShape(e.getX(), e.getY());
            }
            else if(shapeType == ShapeEnum.FREECURVE) {
               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, getSize().width, getSize().height, null);
   g2dImage.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

   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);
   }
   else if(shapeType != ShapeEnum.SELECTION && 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);
   }

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

   for (int i = 0; i < getShapes().size(); i++) {
      if (i == selectedShapeIndex) shapes.get(i).paintSelected(g2dImage);
      else getShapes().get(i).paintSelf(g2dImage);
   }
}
--------------------------------------------------------
変更された部分はわずか4行程度なんですが、説明するのが面倒くさく
なったのでpaintImage()メソッド全体を提示しました。
どこが変更された行か、わかりますね。(ヒント:ShapeEnum.SELECTION
やselectedShapeIndexがからんでいるところです。)わからない人は、
質問をお寄せください。



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


[DrawingToolPanelの編集]
まず最初に、図形を選択するモードにするためのボタンを貼り付けましょう。
DrawingToolPanelのDesignビューを開いて下さい。
PaletteからJToggleButtonを取り出し、「自由曲線」ボタンの上などの
空いているところに貼り付けましょう。
変数名(Variable)はselectionToggleButtonにしましょう。
また、textプロパティーの値を「選択」にしましょう。

他のボタンとの位置関係や間隔がいびつになっているでしょうから、054号など
を参照してきれいな配置に調整して下さい。
(例えば、「Space equally, vertically」ボタン)を使って上下方向に等間隔
割り付けを行うなど。)

続いて、DrawingToolPanelのSourceビューを開き、
DrawingToolPanel()コンストラクターの中の一番最後に
--------------------------------------------------------
shapeGroup.add(selectionToggleButton);
--------------------------------------------------------
という行を追加しましょう。
つまり、この「選択」ボタンを図形のボタンと同じグループに属させ
ます。

続いて、再度DrawingToolPanelのDesignビューに戻って、selectionToggleButton
(「選択」ボタン)を右クリックし、「Add event handler」→「アクション」→
「actionPerformed」を選択し、自動生成された下記コード
--------------------------------------------------------
      selectionToggleButton.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
         }
      });
--------------------------------------------------------

--------------------------------------------------------
      selectionToggleButton.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            drawingPanel.setShapeType(ShapeEnum.SELECTION);
         }
      });
--------------------------------------------------------
というふうに編集しましょう。



では次に、DrawingToolFrameに図形の削除のためのメニューを追加しましょう。


[DrawingToolPanelの編集]
DrawingToolFrameのDesignビューを開いて下さい。

Paletteの「Menu」の配下のJMenuを取り出し、DrawingToolFrameの
画面のメニュー・バーの「ファイル」メニューの右側(マウス・
ポインターを持っていくと赤い縦棒が現れる所)をクリックすること
によって貼り付けます。

変数名(Variable)はmenuEditにしておきましょう。

また、textプロパティーの値を「編集」にしておきましょう。

続いて、Paletteから「Menu」の配下のJMenuItemを取り出し、先ほどの
menuEdit(「編集」メニュー)の下の「(Add items here)」と表示されて
いる所(マウス・ポインターを持っていくと赤い横棒が現れる)をクリッ
クすることによって貼り付けます。
変数名(Variable)はmenuItemRemoveにしておきましょう。
また、そのtextプロパティーの値を「選択した図形の削除」にしておきましょう。


次に、menuItemRemove(「選択した図形の削除」メニュー項目)を右クリックして
「Add event handler」→「アクション」→「actionPerformed」を選択し、
自動生成されたソース・コード
--------------------------------------------------------
      menuItemRemove.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
         }
      });
--------------------------------------------------------

--------------------------------------------------------
      menuItemRemove.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            drawingToolPanel.getDrawingPanel().removeShape();
            repaint();
         }
      });
--------------------------------------------------------
に書き換えましょう。


はい、これで図形を削除する機能ができました。保管したあと、
DrawingToolFrameを起動してテストしてみてください。
自由曲線や矩形や楕円を描いて、それらを削除することについては
うまくいきますが、直線を描いても選択ができませんね。どうして
でしょうか。

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に等しいときにその直線(線分)を選択するように
すればいいのです。

したがって、DrawingPanelの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で答える演算子です。


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

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

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


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



今回はここまで。


(続く)


以上、今回は
┌───────────────────────────┐
・図形の選択の仕方
・図形の削除の仕方
・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) 2012 Future Lifestyle Inc. 不許無断複製