応用プログラミング 第7回


この授業の目的

これまでに修得した基本機能をどのように統合化してインタラクティブなアプリケーションを構成するかに重点を置きます.

今回は,実用的な応用プログラムの例として物理シミュレーションをとりあげて,キャンバス(グラフィック描画領域),ダブルバッファ,ボタンによるイベント制御を統合化したアプリケーションの作り方を学びます.


出席メール

出席メールで「授業中の課題の実施」を確認します.メールを出した時刻,PCの場所(アドレス)を確認します.

to:jhoshino@esys.tsukuba.ac.jp
subject: 応用プログラミング 第7回.氏名,学籍番号
本文: なし(書かなくて結構です)

 


理論

キャンバスのサンプルプログラム

キャンバスでは,描画を行うことができる矩形のエリアを提供します.Graphicsクラスのメソッドを利用してグラフや図を作成できるので,アプリケーションを作るときには良く使います.

但し,アプレットクラスと両方同時に継承できないので,クラスを分離する必要があります.

/* 
	CanvasDemo.java
*/
import java.applet.*;
import java.awt.*;


public class CanvasDemo extends Applet {
  public void init() {
    MyCanvas myCanvas = new MyCanvas();  //MyCanvasオブジェクトを生成する
    setBackground(Color.lightGray);
    myCanvas.setSize(401, 150); //MyCanvasの領域を定義する
    add(myCanvas);
  }
}

class MyCanvas extends Canvas {

  public void paint(Graphics g) { //paintメソッドの中に描画ルーチンを書きます.

    // 軸を描画する
    g.setColor(Color.gray);
    Dimension d = getSize();
    g.drawRect(0, 0, d.width - 1, d.height - 1);
    g.drawLine(0, d.height/2, d.width, d.height/2);
    g.drawLine(d.width/2, 0, d.width/2, d.height - 1);

    // 波形を描画する
    g.setColor(Color.blue);
    double dx = 4 * Math.PI / d.width;   //Mathクラスの使い方も確認して下さい
    double x = -2 * Math.PI;
    int h = d.height;
    for (int i = 0; i < d.width - 1; i++) {
      int y1 = (int)((h - h * Math.sin(x)) / 2);
      int y2 = (int)((h - h * Math.sin(x + dx)) / 2);
      g.drawLine(i, y1, i + 1, y2);
      x += dx;
    }
  }
}

キャンバスとダブルバッファの併用

myCanvas.javaではダブルバッファリングの機能を,Canvas上で実装しています.書き方はほとんど同じですが,描画機能とボタンなどのユーザーインタフェースを併用するときによく使います.今後のアプリケーションプログラムの例でも,全てダブルバッファとキャンバスを併用する形で書かれています.

まず,第5回目に出てきたアプレットを使ったダブルバッファの書き方を復習しておいて下さい.

public class DoubleBuffer extends Applet implements Runnable {
  //ダブルバッファ用の変数の定義
  public void init() {
    // スレッドを開始する
    1) バッファを作成する
  }
  public void run() {
     // 再描画を要求する
     //update()メソッドの呼び出しをする
  }
  public void update(Graphics g) {
    //paint()メソッドの呼び出しをする
  }
  public void paint(Graphics g) {
    2) バッファを描画する    
3) ウィンドウへ転写する
} }

Canvasを使う場合は,Canvasを継承しているクラスに,Runnableインタフェースを実装します.


public class myCanvas extends Canvas implements Runnable {
  //ダブルバッファ用の変数の定義
  public void myCanvas() {   
    //コンストラクタの中でスレッドオブジェクトを生成します.
  }
  public void start() {
     1) startの中でバッファを作成する
 }
  public void run() {
     // 再描画を要求する
     //update()メソッドの呼び出しをする
  }
  public void update(Graphics g) {   
    //paint()メソッドの呼び出しをする
  }
  public void paint(Graphics g) {
    2) バッファを描画する    
3) ウィンドウへ転写する } }

 

mainApplet.javaはアプレット上にキャンバスを配置し,startメソッドでダブルバッファリングを開始する処理を行っています.
setBackgroundで背景色を指定しています.

/*
  mainApplet.java
*/
import java.applet.*;
import java.awt.*;
import java.awt.event.*;

/*
<applet code = "mainApplet" width = 660 height = 500>
</applet>
*/ public class mainApplet extends Applet{ myCanvas canvas; public void init(){ setLayout(null); //setBoundsでレイアウトを指定できるようにする setBackground(Color.lightGray); //背景色を指定 canvas = new myCanvas(); canvas.setBounds(10,10,640,480); //Applet上のCanvasの左上位置と大きさを指定する add(canvas); canvas.start(); } }
/*
  myCanvas.java
*/
import java.awt.*;
//ダブルバッファリングを備えたCanvasの作成
public class myCanvas extends Canvas implements Runnable{
  Thread th;
  Image buffer;
  Graphics bufferg;

  public myCanvas(){
    th = new Thread(this);
  }

  public void start(){
    Dimension d =  getSize();
    buffer = createImage(d.width,d.height);
    th.start();
  }
  public void run(){
    try{
      while(true){
        repaint();
        th.sleep(100);
      }
    }catch(Exception e){}
  }

  public void update(Graphics g){
    paint(g);
  }
  public void paint(Graphics g){    //painの中に描画ルーチンを書きます
      if(bufferg == null) bufferg = buffer.getGraphics();
      Dimension d = getSize();
      drawBackGround(bufferg,d);    //背景を塗りつぶす
      drawAxis(bufferg,d);          //軸を描画する
      g.drawImage(buffer,0,0,this);
  }
  public void drawBackGround(Graphics g,Dimension d){
      g.setColor(Color.white);
      g.fillRect(0,0,d.width,d.height);
  }
  public void drawAxis(Graphics g,Dimension d){
      g.setColor(Color.lightGray);
      g.drawRect(0,0,d.width-1,d.height-1);
      g.drawLine(0,d.height/2,d.width,d.height/2);
      g.drawLine(d.width/2,0,d.width/2,d.height-1);
  }
}


異なる点としてstartメソッドを実装しています.このstartメソッドはmainApplet.java上のsetBoundsメソッドを使用した際にキャンバスの大きさを縦幅640,横幅480と決めているので,その前に呼び出されるmyCanvasコンストラクタの中にstartメソッドの中身を書いた場合,getSizeメソッドでサイズを取得できない可能性を回避するために実装しています.

 


アプリケーションプログラムの例

キャンバス,ダブルバッファに,ボタンによるイベントの受付を加えると,様々なアプリケーションを書くことができます.ここでは,実用的な応用プログラムの例として物理シミュレーションをとりあげて,キャンバス(グラフィック描画領域),ダブルバッファ,ボタンによるイベント制御を統合化したアプリケーションの作り方を学びます.

以下の例では,4次ルンゲクッタ法を用いてばねの伸縮をシミュレーションを行っています.ルンゲクッタ法については 1学期の数値解析の講義でも扱っていると思うのでここでは深く触れません.詳しい説明は数値解析の書籍またはWeb上にもあるのでそちらを参照してください.この例題では詳しい原理を考慮せずに用いれるように作成してあります.ルンゲクッタ法は初期位置,初速度から運動方程式を用いてその後の動作を推定することができます.以下の例でabstractクラスを用いてルンゲクッタ法の概念を表現しています.このクラスを継承したSpringクラスのメソッド f 中に運動方程式から導かれる加速度を返す処理を書きます.この例はばねの伸縮を表しているで運動方程式はma = -kx(加速度a,質量m,ばね定数k,変位x)のmを1としてa = -kxで解いています.

/*
  RungeKutta.java    
*/
abstract class RungeKutta{  //ルンゲクッタ法
  double H;     //刻み幅
  double N;
  double x,dx,x2,dx2,t;
  double x0,dx0,t0;

  //刻み幅,初期振幅,初速度,開始時刻の初期化
  public RungeKutta(double H,double x0,double dx0,double t0){
    this.H  = H;
    this.x0  = x0;
    this.dx0 = dx0;
    this.t0  = t0;
  }
  abstract double f(double t,double x,double dx);

  public void init(){
    x  = x0;
    dx = dx0;
    t  = t0;
  }

  public void runge(){
     double k[];
     double v[];

     k = new double[4];
     v = new double[4];

      x2 = x;
      dx2 = dx;

      k[0] = H*dx;
      v[0] = H*f(t,x2,dx2);
      k[1] = H*(dx2+v[0]/2);
      v[1] = H*f(t+H/2,x2+k[0]/2,dx2+v[0]/2);
      k[2] = H*(dx2+v[1]/2);
      v[2] = H*f(t+H/2,x2+k[1]/2,dx2+v[1]/2);
      k[3] = H*(dx2+v[2]);
      v[3] = H*f(t+H,x2+k[2],dx2+v[2]);

      x  = x2+(k[0]+2*k[1]+2*k[2]+k[3])/6;
      dx = dx2+(v[0]+2*v[1]+2*v[2]+v[3])/6;

      t = t+H;
  }
}


class Spring extends RungeKutta{  //バネのシミュレーションプログラム
  double K = 1.0;   //ばね定数
  double L = 100; //ばねの長さ
  double W = 40;  //ばねの幅
  int N = 30;     //ばねの分割点の数
  int n_x[];      //ばねの分割点のx座標
  int n_y[];      //ばねの分割点のy座標

  public Spring(){
    super(0.1,10,40,0);//刻み幅,初期振幅,初速度,開始時刻の初期化
    init();
  }
  public double f(double t,double x,double dx){
    return -K*x;
  }
  //ばねの分割点の場所を計算する
  public void setPoint(){
    n_x = new int[N];
    n_y = new int[N];
    n_x[0] = 0;
    n_x[N-1] = (int)(L+x);
    n_y[0] = n_y[N-1] = 0;
    for(int i=1;i< N-1;i++){
      n_x[i] = (int)((i+1)*(L+x)/(N+1));
      n_y[i] = (int)(Math.pow(-1,i)*W);
    }
  }
}

上記のルンゲクッタ法をmyCanvas.javaの中で呼び出します.
run, start, paintなどのメソッドを書くところは同じで,その内部で何を呼び出すかが変更されているだけです.

/*
  myCanvas.java
*/
import java.awt.*;
//ダブルバッファリングを備えたCanvasの作成
public class myCanvas extends Canvas implements Runnable{
  Thread th;
  Image buffer;
  Graphics bufferg;
  Spring sp;
   boolean waitFlag;
  public myCanvas(Spring sp){
    this.sp = sp;
      waitFlag = false;
    th = new Thread(this);
  }
  public void start(){
    Dimension d =  getSize();
    buffer = createImage(d.width,d.height);
    th.start();
  }
  public void run(){  //<--スレッドで連続的に実行する部分
    try{
      while(true){
        if(!waitFlag){  //waitFlagはtrueの場合repaintメソッドとrungeメソッドを処理しないことで動作を止めることを意味しています.
          repaint();  //描画を行う
     sp.runge();  //rungeメソッドは刻み幅秒後の変位xの値を推定します.
     }
        th.sleep(100);
      }
    }catch(Exception e){}
  }

  public void update(Graphics g){
    paint(g);
  }
  public void paint(Graphics g){
      if(bufferg == null) bufferg = buffer.getGraphics();
      Dimension d = getSize();
      drawBackGround(bufferg,d);    //背景を塗りつぶす
      drawAxis(bufferg,d);          //軸を描画する
      sp.setPoint();      //setPointメソッドはばねの分割点を計算します.
       drawSpring(bufferg,d);
      g.drawImage(buffer,0,0,this);
  }


 //-------- 画面上にシミュレーション結果を描画するためのメソッド群 ---------------------
  public void drawSpring(Graphics g,Dimension d){ //drawSpringはキャンバスの中心を原点とするばねを描きます.
      g.setColor(Color.gray);
      for(int i=0;i< sp.N-1;i++)
        g.drawLine(d.width/2 + sp.n_x[i],d.height/2 - sp.n_y[i],
                        d.width/2 + sp.n_x[i+1],d.height/2 - sp.n_y[i+1]);
      g.drawLine(d.width/2 + sp.n_x[sp.N-1],d.height/2 - sp.n_y[sp.N-1],
                        d.width/2 + sp.n_x[sp.N-1]+20,d.height/2 - sp.n_y[sp.N-1]);
      g.setColor(Color.blue);
      g.fillOval(d.width/2 + sp.n_x[sp.N-1]-20+40,d.height/2 - sp.n_y[sp.N-1]-20,40,40);
  }
  public void drawBackGround(Graphics g,Dimension d){
      g.setColor(Color.white);
      g.fillRect(0,0,d.width,d.height);
  }
  public void drawAxis(Graphics g,Dimension d){
      g.setColor(Color.lightGray);
      g.drawRect(0,0,d.width-1,d.height-1);
      g.drawLine(0,d.height/2,d.width,d.height/2);
      g.drawLine(d.width/2,0,d.width/2,d.height-1);
  }
}

myCanvasクラス内のコンストラクタでthis.sp = spと記述していますが
これはコンストラクタで引数として得たSpringオブジェクトの参照をこのクラスで宣言したspに渡しています.
Cで言うポインタのような使い方をしています.Javaではポインタという言葉はでてきませんが オブジェクトの参照を渡すことができます.これによりmyCanvasクラスからインスタンス化したオブジェクトの持つspに処理を行った結果が基となるmainAppletクラスのオブジェクトspに反映されます.


mainApplet.javaでは,Buttonによる処理のstart(開始),stop(停止),init(初期化)を行っています.
actionPerformedメソッドはボタンをマウスでクリックすることで呼び出されます.

import java.applet.*;
import java.awt.*;
import java.awt.event.*;


/*
<applet code = "mainApplet" width = 860 height = 300>
</applet>
*/ public class mainApplet extends Applet implements ActionListener{ myCanvas canvas; Button b[]; Spring sp; public void init(){ setLayout(null); setBackground(Color.white); sp = new Spring(); canvas = new myCanvas(sp); canvas.setBounds(10,10,640,240); //Applet上のCanvasの位置と大きさを指定する add(canvas); canvas.start(); b = new Button[3]; b[0] = new Button("start"); b[1] = new Button("stop"); b[2] = new Button("init"); for(int i=0;i < 3;i++){ b[i].addActionListener(this); b[i].setBounds(660+i*40,120,40,20); b[i].addActionListener(this); add(b[i]); } } public void actionPerformed(ActionEvent e){ //開始と停止の制御 if(e.getSource() == b[0]){ canvas.waitFlag = false; }else if(e.getSource() == b[1]){ canvas.waitFlag = true; }else if(e.getSource() == b[2]){ sp.init(); } } }  

開始と停止は次のような仕組みで制御しています.

 

実習

物理シミュレーションのプログラムをコンパイルして機能を確認して下さい.

プログラムは次のリンクから保存できます:mainApplet.java, myCanvas.java, RungeKutta.java

2つ以上のjavaファイルを同時にコンパイルして実行するには次のように行うと便利です.

  1. 任意のディレクトリを作成します.
  2. 作成したディレクトリにjavaファイルを置きます.
  3. 作成したディレクトリに移動します.
  4. %javac  *.java とコマンドを入力すると同時にコンパイルできます.*はワイルドカードと呼ばれていますが,全てという意味があります.ここでは.javaという拡張子がついているものを全部コンパイルするという意味があります.
  5. このディレクトリにAppletクラスを継承しているjavaファイルまたはclassファイルのファイル名をhtmlのAppletタグの所に書きます.
  6. appletviewer 〜.htmlまたはnetscapeで開くと実行できます (ファイルが一つの場合と同様です).

少し違う例として,機能工学システム専攻火曜実験の鉄棒ロボットのシミュレーション例が
http://www.geocities.co.jp/SiliconValley-Oakland/5391/java/java.html
にあります.これは,2000年度に応用プログラミングの授業を受講した田中君によるシミュレーションプログラムです.


練習問題

1)キャンバスとダブルバッファを利用して画面内でボールが弾むアニメーションを作成します.ボールの大きさやキャンバスの領域は適当に設定して下さい.

ヒント:myCanvas.javaのpaintメソッドに描画機能を加えます.

2)startボタンとstopボタンを付けて,アニメーションの開始と停止を制御できるようにして下さい.

ヒント:

  • バネの例で使っているmainApplet.javaを応用します.initが選ばれたときには,ボール位置の初期化メソッドを呼び出します.
  • stopが選ばれたときは,canvas.waitFlagをtrueにします.myCanvasのrunメソッドの中で,if文で判定して,repaintメソッドを実行されないようにします.

3)プログラミング課題(自由課題)で制作予定のアプリの候補案を3つ程度挙げて、それぞれ2〜3行で説明して下さい。最終的に変わっても構いません。

提出先のアドレスはjhoshino@esys.tsukuba.ac.jpです.Subject欄に「応用プログラミング 第7回課題,氏名,学籍番号」を書いて下さい.本文にソースコード,実行結果,考察を記載して下さい.

締め切りは次の授業の開始時まで.ソースコードはメール本文へのコピーあるいは添付どちらでも結構です.

 


参考資料:レイアウトの作成


さまざまなレイアウトを作ることができます. ここではアプレットのレイアウトをnullに設定することで基盤となるPanel上に作るサブPanelにはsetBoundsメソッドを用いて場所を指定しています.各サブPanel上では用意されているレイアウトを設定しています.

/*
  LayoutDemo.java
*/
import java.applet.*;
import java.awt.*;
import java.awt.event.*;



public class LayoutDemo extends Applet{
  public void init() {
    Panel pn[] = new Panel[3];
    Button b[] = new Button[3];


    setLayout(null);    //Layoutにnullを指定することでアプレットのレイアウトを指定できる


    //Panelを3つ作って横に並べていく処理
    for(int i=0;i<3;i++){
      pn[i] = new Panel();
      pn[i].setBounds(i*100,0,100,100);
  
    /*public void setBounds(int x,int y,int width,int height)
        左上隅の新しい位置は x および y によって指定され,新しいサイズは width および height によって指定されます.
      */
      pn[i].setBackground(new Color(0.0f,0.0f,(float)(i+1)/3));
      add(pn[i]);
    }

    //左のパネル FlowLayoutの例 
    pn[0].setLayout(new FlowLayout(FlowLayout.LEFT, 4, 2));
    for (int i = 0; i < 6; i++) {
      pn[0].add(new Button("B" + i));
    }

    //真中のパネル BorderLayoutの例 
    pn[1].setLayout(new BorderLayout(5,5));
    Button bN = new Button("N");
    Button bS = new Button("S");
    Button bEa = new Button("E");
    Button bW = new Button("W");
    Button bCe = new Button("C");
    pn[1].add(bN, "North");
    pn[1].add(bS, "South");
    pn[1].add(bEa, "East");
    pn[1].add(bW, "West");
    pn[1].add(bCe, "Center");

    //右のパネル GridLayoutの例 
    pn[2].setLayout(new GridLayout(3, 2));
    Button bA = new Button("A");
    Button bB = new Button("B");
    Button bC = new Button("C");
    Button bD = new Button("D");
    Button bE = new Button("E");
    Button bF = new Button("F");
    pn[2].add(bA);
    pn[2].add(bB);
    pn[2].add(bC);
    pn[2].add(bD);
    pn[2].add(bE);
    pn[2].add(bF);
  }
}