Deep Side of Java〜Java 言語再入門 第4回 〜 アプレット、スレッド、AWT

AWTの使い方

AWTプログラムの実例






AWTの使い方

AWTプログラムの実例

さて、実例としてお見せするのは、いわゆる「neco」〜マウスを画像が追いかける、というお楽しみプログラムである。このプログラムの動作は基本的に次の通りである。

  1. トップレベルであるFrame Widgetの中に Canvas Widgetが入っており、この Canvas Widgetは画像を表示する。

  2. Canvas Widgetはマウスの移動に関するイベントを受け取るが、自分では処理をせずに単にマウス位置を保持する。

  3. 別個に起動しているスレッドが、適当な間隔でマウス位置を Canvas Widgetにポーリングし、マウス位置に合わせて適切な位置に移動する画像を貼るように、Canvas Widgetに命令する。

  4. Canvas Widgetは適切な準備をして、repaint() を呼び出す。

  5. そうすると、マウスの移動に合わせて画像が貼り付けられる。

この時、背景画像、移動画像共に大きさは任意である。だから、ひょっとすると repaint() に手間がかかるかもしれない。そうすると、画像がチラつくことになる。これを避けるために、クリッピングを行う。クリッピングとは、repaint() の際に、書き換えるエリアを本当に書き換える必要がある部分だけに制限し、素早く再描画させることでチラツキをなくす手法である。

では、クラス構成は次の通り。

Neco.java
メインプログラム。Frame を継承し、トップレベルウィンドウとして機能する。また、引数から使われる画像を取得し、プログラムの準備をする。

ImageCanvas.java
Canvas クラスを継承し、画像に特化した Canvas として動作するWidgetの汎用的なクラス。ここでマウスに関するイベントは受け取るが、プログラムに固有の処理は何もしない。このプログラムでは背景画像を表示することに責任を持っている。

NecoImageCanvas.java
ImageCanvas クラスを継承し、移動画像が表示されて動くことに責任を持つ。クリッピングはこのクラスが担当する。

NecoMediator.java
Thread クラスを継承し、スレッドとして動作する。つまり、適当な間隔で ImageCanvas クラスが保持するマウス位置を取得し、NecoImageCanvas クラスに描画位置を指示する。だから移動画像の動き方はこのクラスが責任を持っている。

では具体的なコードを見て行こう。まずメインクラスである Neco.java である。

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;

public class Neco extends Frame {
    private NecoImageCanvas canvas;  /* 具象 Canvas クラス */
    private NecoMediator med;        /* スレッド */

    private static String backImage = null; /* 背景画像名 */
    private static String foreImage = null; /* 移動画像名 */
    private Image back = null;  /* 背景画像 */
    private Image fore = null;  /* 移動画像 */
    private int width = 300;    /* トップレベルのウィンドウ幅 */
    private int height = 250;   /* トップレベルのウィントウ高さ */


    public static void main( String [] args ) {
        if( args.length > 0 ) {
            backImage = args[0];
            if( args.length > 1 ) {
                foreImage = args[1];
            }
        }
        new Neco();
    }

    private Neco( ) {
        super( "Neco" );  /* ウィンドウタイトルをセット */

        /* 閉じるボタンで終れるようにする */
        addWindowListener( new WindowAdapter() {
                public void windowClosing( WindowEvent we ) {
                    System.exit( 0 );
                }
            } );

        med = new NecoMediator();

        /* 背景画像の準備 */
        if( backImage != null ) {
            back = loadImage( backImage );
        }
        if( back != null ) {
            width = back.getWidth( this );
            height = back.getHeight( this );
        }
        setSize( new Dimension( width, height ) );

        /* 移動画像の準備 */
        if( foreImage != null ) {
            fore = loadImage( foreImage );
        }

        /* 画像が異常ならば単に終了 */
        if( fore == null ) {
            System.out.println( "foreground image " + foreImage 
                                 + " cannot load" );
            System.exit( 1 );
        }
        if( back == null ) {
            System.out.println( "background image " + backImage 
                                 + " cannot load" );
            System.exit( 1 );
        }

        /* Canvas を用意して、ウィンドウを具現化する */
        canvas = new NecoImageCanvas( med, width, height );
        add( canvas );
        show();

        /* リアライズした後に実際の画像はセットした方が無難 */
        canvas.setBackImage( back );
        canvas.setForeImage( fore );
        /* スレッドの開始 */
        med.start();
    }

    /* 画像名から実際の画像をすぐに使えるかたちで準備する定型的な処理 */
    private Image loadImage( String name ) {
        Toolkit tk = Toolkit.getDefaultToolkit();
        Image ret = tk.getImage( name );
        if( ret != null ) {
            while( ! prepareImage( ret, this ) ) {
                int flag = checkImage( ret, this );
                if( (flag & ImageObserver.ERROR) != 0 ) {
                    return null;
                }
                try { 
                    Thread.sleep(200); 
                }  catch(InterruptedException e) { }
            }
        }
        return ret;
    }

}

2つ目は画像に特化した Canvas Widgetである ImageCanvas クラスである。

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

public class ImageCanvas extends Canvas implements MouseListener, 
                                               MouseMotionListener {
    private Image img   = null;  /* 背景画像 */
    private int width, height;   /* 画像の幅と高さ */
    private Dimension size;      /* Canvas クラスが正しくレイアウトされるように */
    private int adjust = 0;      /* 位置をズラして貼り付けることもできる */
    private int mouseX, mouseY;  /* マウスの位置を保持する */
    private boolean in = false;  /* 現在マウスがウィンドウ内にあるか? */

    public ImageCanvas( int w, int h ) {
        width = w; height = h;     /* サイズは先に決定しておく */
        addMouseListener( this );  /* イベントリスナを登録 */
        addMouseMotionListener( this );
    }

    public void setBackImage( Image i ) {
        /* このメソッドはウィンドウが具現化した後に呼ばれるので、実際の描画まで行う */
        img = i;  repaint();   
     }

    /* 描画の元締めである paint メソッド */
    public void paint( Graphics g ) {
        /* このクラスでは背景画像だけに専念する */
        g.drawImage( img, adjust, adjust, this );
    }

    public void imagePaint( Image i, int x, int y, Graphics g ) {
        /* 移動画像描画のためのインターフェイスである */
        g.drawImage( i, adjust + x, adjust + y, this );
    }

    public void update( Graphics g ) {
        paint( g );
    }

    /* 以下2つは Canvas クラスが正しくレイアウトされることを保証する */
    public Dimension getPreferredSize( ) {
          if( size == null ) {
               size = new Dimension( width + adjust * 2, 
                                     height + adjust * 2 );
          }
          return size;
     }

    public Dimension getMinimumSize( ) {
        return getPreferredSize();
    }

    /* マウスイベントからマウス位置を取得して保持する非公開メソッド */
    private void setMousePosition( MouseEvent e ) {
        in = true;
        mouseX = e.getX() - adjust;
        mouseY = e.getY() - adjust;
    }

    /* 外部から現在のマウス位置を取得するインターフェイス */
    public Point getMousePosition( ) {
        if( in ) {
            return new Point( mouseX, mouseY );
        } else {  /* 現在マウスがウィンドウ内になければ null を返す */
            return null;
        }
    }

    /* 以下 MouseListener, MouseMotionListener のインターフェイス */
    public void mouseDragged( MouseEvent e ) { 
        setMousePosition(e); 
    }

    public void mouseMoved( MouseEvent e ) { 
        setMousePosition(e); 
    }

    public void mouseEntered(MouseEvent e) { 
        setMousePosition(e); 
    }

    public void mouseExited(MouseEvent e) { 
        in = false; 
    }
    
    public void mousePressed(MouseEvent e) {}
    public void mouseReleased(MouseEvent e) {}
    public void mouseClicked(MouseEvent e) {}
}

3つ目はこのアプリケーション用に特化した Canvas である NecoImageCanvas クラスである。いわゆる「クリッピング」の動作はこのクラスに封じ込められており、このクラスを呼び出す側ではそれを意識する必要はない。

また、全体の動作の指揮はスレッドであるクラス(NecoMediator)がすべて動かしており、ImageCanvas クラスも NecoImageCanvas クラスも受動的にしか動かず、NecoMediator が動かすためのインターフェイスを装備しているだけである。このように主導的なクラスと受動的なクラスをうまく分けてやり、相互関係を単純化するデザインパターンが「Mediator」である。

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

public class NecoImageCanvas extends ImageCanvas {
    private NecoMediator med;     /* スレッド */

    private Point center = null;  /* 現在のマウス位置 */
    private Point prev = null;    /* 一つ前の時のマウス位置 */

    private Image fore = null;    /* 移動画像 */
    private int imageWidth, imageHeight;  /* 移動画像の幅と高さ */

    public NecoImageCanvas( NecoMediator nm, int w, int h ) {
        super( w, h );
        /* 全体の動作を指揮するのはスレッドのクラスである。だから、スレッドのクラスに対して、
        Canvas がその存在を通知してやる必要がある。このようにコンストラクタでスレッドの
        クラスを引数として渡し、そのスレッドクラスに自分を渡し直してやる。このように
        全体の動作を指揮する役割を1つのクラスに与えるデザインパターンを Mediator と
        呼ぶが、この Mediator での定型的な処理をしている。 
        筆者はこの定型処理を洒落で「名刺交換」と呼んでいる。要するに初対面のクラスが
        お互いに「こういう者です」と紹介しあうように、互いのインスタンスを相手側に
        登録させているのである。*/
        med = nm;  med.setImageCanvas( this );
    }

    /* 移動画像をセット */
    public void setForeImage( Image i ) {
        fore = i;
        imageWidth = fore.getWidth( this );
        imageHeight = fore.getHeight( this );
    }

    /* スレッドから呼ばれて、移動画像の位置をセットして再描画を促す */
    public void drawImage( Point p ) {
        if( p == null ) {
            center = null;
        } else {
            center = new Point( p.x - imageWidth / 2, 
                                p.y - imageHeight / 2 );
        }
        repaint();
    }

    /* さて、全景画像の paint() である。これは今のマウス位置 center と直前のマウス位置 
    prev の有効/無効に応じて、クリッピングの4通りの場合分けがある。 */
    public void paint( Graphics g ) {
        if( prev == null ) {
            if( center == null ) {
                /* prev も center も無効の場合は、念のために背景のみを再描画 */
                super.paint( g );
            } else {
                /* このケースは、マウスが新たにウィンドウに入った場合である */
                /* まず、背景画像をすべて描く */
                super.paint( g );
                /* その後、マウス位置に移動画像を描く。クリッピングは単純である */
                g.setClip( center.x, center.y, 
                           imageWidth, imageHeight );
                imagePaint( fore, center.x, center.y, g );
                prev = center;  /* 前の位置を保持 */
            }
        } else {
            /* このケースはマウスがウィンドウから出た直後である。だから、直前の
            クリップ位置で描かれた移動画像を消す。ゆえに背景画像だけの描画で良い。 */
            if( center == null ) {
                g.setClip( prev.x, prev.y, 
                           imageWidth, imageHeight );   
                super.paint( g );
                prev = null;
            } else {
                /* さて、一番重要なウィンドウ内でのマウスの移動である */
                if( ! center.equals( prev ) ) {
                    /* まず、直前に描かれた移動画像を背景によって消す */
                    g.setClip( prev.x, prev.y, 
                               imageWidth, imageHeight );
                    super.paint( g );
                    /* そして、新しい移動位置に描画する */
                    g.setClip( center.x, center.y, 
                               imageWidth, imageHeight );
                    imagePaint( fore, center.x, center.y, g );
                    prev = center; /* 前の位置を保持 */
                }
            }
        }
    }
}

さて最後はスレッドであり、全体を指揮する Mediator の NecoMediator クラスである。

import java.awt.*;
import java.io.*;

public class NecoMediator extends Thread {
    private NecoImageCanvas nic;  /* Canvas クラスを保持 */

    /* NecoImageCanvas クラスが自分を Mediator に登録するのに使う。
       言い換えれば、名刺交換用のインターフェイスである。 */
    public void setImageCanvas( NecoImageCanvas n ) { 
        nic = n; 
    }

    /* スレッド本体である。「追いかける」動作はここに記述されている */
    public void run( ) {
        int count = 0;  /* 追いかけるのを実現するために、4回単位で遅延する */
        int addX = 0;   /* 移動差分 */
        int addY = 0;
        Point now = null;  /* 前回の位置 */
        while( true ) {
            Point p = nic.getMousePosition();  /* マウス位置の取得 */
            /* やはり同様に、今と直前の有効/無効の場合分けがある */
            if( p == null ) {
                if( now == null ) {  /* マウスはウィンドウにいない */
                    /* NOP : なにもしない */
                } else {
                    /* マウスがウィンドウから出た */
                    nic.drawImage( null );  /* 消すように指示する */
                    now = null; count = 0;
                }
            } else {
                if( now == null ) {
                    /* マウスがウィンドウに入った */
                    nic.drawImage( p );  /* 移動画像をその位置に描く */
                    now = p; count = 0;
                } else {
                    /* 4回単位で追いつくようにするが、*/
                    if( ++count >= 4 ) count = 0;  
                    /* ちょっとだけオーバーランするギミック付き */
                    if( count == 0 ) {             
                        addX = (p.x - now.x) * 10 / 38; 
                        addY = (p.y - now.y) * 10 / 38; 
                    }
                    /* ちょっとだけウロウロするギミック */
                    addX = (addX < 10)?(addX * 12 / 10):(addX);
                    addY = (addY < 10)?(addY * 12 / 10):(addY);
                    now = new Point( now.x + addX, now.y + addY );
                    nic.drawImage( now );  /* 描く! */
                }
            }
            try {  /* さて、0.1 秒毎にこの処理は実行されるようにする */
                Thread.sleep( 100 );
            } catch( InterruptedException e ) { }
        }
    }

}



copyright by K.Sugiura, 1996-2006