画面のちらつきをなくす

MouseDrawを実行してみると,画面の書き直しが気になると思う.ここでは, 画面のちらつきをなくすためのいくつかのテクニックを説明する.

オフスクリーンイメージ(offscreen)の作成と描画

Frameなどの GUI 部品に描画する際に,paint の度に変化する部分が少ない 場合は,オフスクリーンイメージ(画面外の仮想的なスクリーン)に書き込んで おいてdrawImageで一度に書き込むことによって,描画コマンドを呼び出す回 数を減らすことができる.

オフスクリーンイメージは GUI部品がスクリーン上に描画されている時に, 「createImage(幅,高さ)」を呼び出すことによって,作ることができる.コ ンストラクタの中で createImageを呼んでも スクリーン上にない時は null (オブジェクトが存在しないことを表す値)を返す.

これまでのところで,スクリーン上にあることが保証されているのは,メソッ ドpaint の中である.paintの中で,最初に呼ばれた時は20x20のオフスクリー ンイメージを作り正方形を2つ書き(100,100)からdrawImageを実行 し,2回目以降は,そのオフスクリーンイメージを書くだけというプログラム を見てみる.

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

 // キー入力をおこなうので,インタフェースKeyListenerの実装とする
public class OffScreen extends Frame implements KeyListener{
   // Frameに表示するのと同じ内容を保持する Image 型の変数 imageの宣言
  Image image;
    // コンストラクタ
  public OffScreen(){
     // 親クラスであるFrameクラスのコンストトラクタの呼び出し
    super("OffScreen");
     // キーボードイベントの受取り手を自分自身にする.
    addKeyListener(this);
  }
  public static void main(String [] args){
     // OffScreenクラスのインスタンスを作る
    OffScreen frame=new OffScreen();
     // サイズを300x300に設定
    frame.setSize(300,300);
     // 表示する.
    frame.setVisible(true);
  }
  public void paint(Graphics g){
      // image に対してこれまでに代入が実行されたか?
    if(image==null){ 
        // 20x20のサイズのオフスクリーンイメージを作成
        // imageに値を入れるので, 次回はif文は不成立する.
      image=createImage(20,20); 
        // image に書き込むための Graphics オブジェクトを得る
      Graphics g1=image.getGraphics(); 
        // imageに着込む際の色を黒に設定
      g1.setColor(Color.black);
        // (0,0)から幅20高さ20で長方形(この場合はたまたま正方形)を塗り潰し
      g1.fillRect(0,0,20,20);
        // imageに着込む際の色を白に設定
      g1.setColor(Color.white);
        // (0,0)から幅10高さ10で長方形(この場合はたまたま正方形)を塗り潰し
      g1.fillRect(0,0,10,10);
    }
      // image の内容を (100,100)からウィンドウに描画
    g.drawImage(image,100,100,this);
  }
  public void keyPressed(KeyEvent e){
    int key=e.getKeyChar();
    if(key=='q') System.exit(0);
  }
  public void keyReleased(KeyEvent e){}
  public void keyTyped(KeyEvent e){}
}

ダブルバッファリング

オフスクリーンイメージは,描画コマンドを減らすだけではなく画面のちらつ きをなくすのにも使われる.MouseDraw を実行させて,マウスを動かすと,ち らつきが目立つと思う.これはpaintを実行中,たとえばバックグラウンドを 表示して,線をまだ引かないうちに画面が更新されると(コンピュータ画面の 更新はテレビと比べて頻繁で1秒間に60回以上),一瞬線がない状態が生ずるか らである.

このちらつきを生じさせないために,次に表示しようとする画面全体をオフス クリーンイメージで作成しておいて,drawImageで表示する方法が用いられる. これをダブルバッファリングと呼ぶ.ダブルバッファリングは, Component クラスのupdate メソッドをオーバーライドすることで実現できる.

  // AWTを使うため,
import java.awt.*;
  // イベント駆動関係のクラスを用いるため
import java.awt.event.*;

  // 線分のクラスを定義する.
class Line{
  // 始点,終点のX座標,Y座標を int で保持する.
  public int start_x,start_y,end_x,end_y;
  // Lineのコンストラクタ
  public Line(int x1,int x2,int x3,int x4){
   start_x=x1;
   start_y=x2;
   end_x=x3;
   end_y=x4;
  }
}

 // インタフェースの実装では 「implements インタフェース名」と書く
 // 複数書く場合は,カンマで区切る
class MouseDraw extends Frame implements KeyListener, MouseListener, MouseMotionListener{
   // Lineの配列を保持する変数 linesの宣言
  public Line[] lines;
   // linesのどこまで使われているかを示す変数 lineCountの宣言
  int lineCount;
   // マウスをドラッグ中かどうかを示す boolean型(真偽値)の変数draggingの宣言
  boolean dragging;
   // 表示する色を保持する変数
  Color lineColor;
   // コンストラクタの宣言
  public MouseDraw(String title){
     // 親クラス Frameのコンストラクタの宣言
    super(title);
     // 大きさ10のLineの配列を作成してlinesに代入
    lines=new Line[10];
     // linesは何も使われていない
    lineCount=0;
     // ドラッグ中ではない
    dragging=false;
     // 線の色は黒に
    lineColor=Color.black;
      // GUI部品と,Event Listenerを関連づける
    addKeyListener(this);
    addMouseListener(this);
    addMouseMotionListener(this);
  }
  public static void main(String args[]) {
     // MouseDrawのインスタンスを作成 frameに代入
    MouseDraw frame=new MouseDraw("MouseDraw");
     // サイズを 600x400に設定
    frame.setSize(600,400);
     // 表示する.
    frame.setVisible(true);
  }
  Image offScreenImage;
  Graphics offScreenGraphics;
  public void update(Graphics g){
    if(offScreenImage==null){
      offScreenImage=createImage(600,400); // オフスクリーンイメージを600x400のサイズで作成
      offScreenGraphics=offScreenImage.getGraphics(); // オフスクリーンイメージに描画するための Graphics オブジェクト
    }
    paint(offScreenGraphics); // 次の画面のイメージを作る.
    g.drawImage(offScreenImage,0,0,this); // イメージを本物のスクリーンに書き込む
  }
   // offScreenImageの書き直しをする際に呼ばれる
  public void paint(Graphics g){
    int i;
      // 白で(0,0)-(600,400)を塗り潰す
    g.setColor(Color.white);
    g.fillRect(0,0,600,400);
      // 色を設定
    g.setColor(lineColor);
      // lineCount個,linesに登録されているLineを描画する
    for(i=0;i< lineCount;i++){
      g.drawLine(lines[i].start_x,lines[i].start_y,
                 lines[i].end_x,lines[i].end_y);
    }
     // マウスをドラッグ中の時は
    if(dragging){
       // 赤い色で
      g.setColor(Color.red);
       // lines[lineCount] を描画する.
      g.drawLine(lines[i].start_x,lines[i].start_y,
                 lines[i].end_x,lines[i].end_y);
    }
  }
   // KeyListenerを実装するためのメソッド
  public void keyPressed(KeyEvent e){
     // イベントからキーのコードを取り出す
    int key=e.getKeyChar();
     // デバッグ用の表示
    System.out.println("keyPressed("+e+","+key+")");
     // 入力が 'q'の時は終了する
    if(key=='q') System.exit(0);
  }
   // 要らないイベントに対応するメソッドも中身は空で書いておく必要がある.
  public void keyReleased(KeyEvent e){}
  public void keyTyped(KeyEvent e){}
   // MouseListenerを実装するためのメソッド
  public void mousePressed(MouseEvent e){
     // 押された時のマウスカーソルの位置を得る
    int mx=e.getX(),my=e.getY();
     // デバッグ用の表示
    System.out.println("mousePressed("+e+","+mx+","+my+")");
     // 配列linesのlineCount番目に線分を登録
    lines[lineCount]=new Line(mx,my,mx,my);
     // ドラッグ中であることを示す
    dragging=true;
     // 再表示をおこなう
    repaint();
  }
   // マウスのボタンが離された時のイベント
  public void mouseReleased(MouseEvent e){
     // マウスカーソルの位置を得る
    int mx=e.getX(),my=e.getY();
     // デバッグ用の表示
    System.out.println("mouseUp("+e+","+mx+","+my+")");
     // 配列linesのlineCount番目の始点を変更
    lines[lineCount].end_x=mx;
    lines[lineCount].end_y=my;
     // lineCountを増やす
    lineCount++;
    dragging=false;
     // 再表示をおこなう
    repaint();
  }
  public void mouseClicked(MouseEvent e){}
  public void mouseEntered(MouseEvent e){}
  public void mouseExited(MouseEvent e){}
   // MouseMotionListenerを実装するためのメソッド
  public void mouseDragged(MouseEvent e){
     // マウスカーソルの位置を得る
    int mx=e.getX(),my=e.getY();
     // デバッグ用の表示
    System.out.println("mouseDrag("+e+","+mx+","+my+")");
     // 配列linesのlineCount番目の始点を変更
    lines[lineCount].end_x=mx;
    lines[lineCount].end_y=my;
     // 再表示をおこなう
    repaint();
  }
  public void mouseMoved(MouseEvent e){}
}
だけである.これを(オフスクリーンイメージの大きさを変更する必要はある が)加えることにより,多くのGUI プログラムはちらつきを押さえることがで きる.

しかし,センターの環境(Java VMの走るマシンが SPARC版 Solaris 2.6で, 表示はNC-OS上のX Window server )ではdrawImageのオーバヘッドが大きいた め,マウスの動きに追従してくれないようだ.


updateの置き換えと,書き換えの必要な領域の指定

ちらつきの最大の原因は,paintメソッドが呼ばれる前に背景色でウィンドウ を塗り潰す点にある.これは,標準のupdateの中でおこなっているので,これ を以下のように置き換えて単純に paintを呼ぶようにするだけで,ちらつきを かなり押さえることができる.
  public void update(Graphics g){
    paint(g); // 次の画面のイメージを作る.
  }
これでちらつきをかなり押さえることができるが,さらに押さえる方法とし て, repaintの際にクリッピング範囲を指定することにより, 再描画する範囲を指定する方法がある.

MouseDrawのプログラムでいうと,MousePressed, MouseDragged, MouseReleased から呼ばれるrepaintでは再描画する必要があるのは,描画中 の線分の前書かれていた範囲と新たに書く範囲の和集合部分だけである.した がって,以下のように書けば良い.

  // AWTを使うため,
import java.awt.*;
  // イベント駆動関係のクラスを用いるため
import java.awt.event.*;

  // 線分のクラスを定義する.
class Line{
  // 始点,終点のX座標,Y座標を int で保持する.
  public int start_x,start_y,end_x,end_y;
  // Lineのコンストラクタ
  public Line(int x1,int x2,int x3,int x4){
   start_x=x1;
   start_y=x2;
   end_x=x3;
   end_y=x4;
  }
}

 // インタフェースの実装では 「implements インタフェース名」と書く
 // 複数書く場合は,カンマで区切る
class MouseDraw extends Frame implements KeyListener, MouseListener, MouseMotionListener{
   // Lineの配列を保持する変数 linesの宣言
  public Line[] lines;
   // linesのどこまで使われているかを示す変数 lineCountの宣言
  int lineCount;
   // マウスをドラッグ中かどうかを示す boolean型(真偽値)の変数draggingの宣言
  boolean dragging;
   // 表示する色を保持する変数
  Color lineColor;
   // コンストラクタの宣言
  public MouseDraw(String title){
     // 親クラス Frameのコンストラクタの宣言
    super(title);
     // 大きさ10のLineの配列を作成してlinesに代入
    lines=new Line[10];
     // linesは何も使われていない
    lineCount=0;
     // ドラッグ中ではない
    dragging=false;
     // 線の色は黒に
    lineColor=Color.black;
      // GUI部品と,Event Listenerを関連づける
    addKeyListener(this);
    addMouseListener(this);
    addMouseMotionListener(this);
  }
  public static void main(String args[]) {
     // MouseDrawのインスタンスを作成 frameに代入
    MouseDraw frame=new MouseDraw("MouseDraw");
     // サイズを 600x400に設定
    frame.setSize(600,400);
     // 表示する.
    frame.setVisible(true);
  }
  public void update(Graphics g){
    paint(g);
  }
  public void paint(Graphics g){
    int i;
      // 白で(0,0)-(600,400)を塗り潰す
    g.setColor(Color.white);
    g.fillRect(0,0,600,400);
      // 色を設定
    g.setColor(lineColor);
      // lineCount個,linesに登録されているLineを描画する
    for(i=0;i< lineCount;i++){
      g.drawLine(lines[i].start_x,lines[i].start_y,
                 lines[i].end_x,lines[i].end_y);
    }
     // マウスをドラッグ中の時は
    if(dragging){
       // 赤い色で
      g.setColor(Color.red);
       // lines[lineCount] を描画する.
      g.drawLine(lines[i].start_x,lines[i].start_y,
                 lines[i].end_x,lines[i].end_y);
    }
  }
   // KeyListenerを実装するためのメソッド
  public void keyPressed(KeyEvent e){
     // イベントからキーのコードを取り出す
    int key=e.getKeyChar();
     // デバッグ用の表示
    System.out.println("keyPressed("+e+","+key+")");
     // 入力が 'q'の時は終了する
    if(key=='q') System.exit(0);
  }
   // 要らないイベントに対応するメソッドも中身は空で書いておく必要がある.
  public void keyReleased(KeyEvent e){}
  public void keyTyped(KeyEvent e){}
   // MouseListenerを実装するためのメソッド
  public void mousePressed(MouseEvent e){
     // 押された時のマウスカーソルの位置を得る
    int mx=e.getX(),my=e.getY();
     // デバッグ用の表示
    System.out.println("mousePressed("+e+","+mx+","+my+")");
     // 配列linesのlineCount番目に線分を登録
    lines[lineCount]=new Line(mx,my,mx,my);
     // ドラッグ中であることを示す
    dragging=true;
     // 再表示をおこなう
    repaint(mx,my,1,1);
  }
   // マウスのボタンが離された時のイベント
  public void mouseReleased(MouseEvent e){
     // マウスカーソルの位置を得る
    int mx=e.getX(),my=e.getY();
     // デバッグ用の表示
    System.out.println("mouseUp("+e+","+mx+","+my+")");
     // 配列linesのlineCount番目の始点を変更
    int oldx=lines[lineCount].end_x;
    int oldy=lines[lineCount].end_y;
    lines[lineCount].end_x=mx;
    lines[lineCount].end_y=my;
     // lineCountを増やす
    dragging=false;
     // 再表示をおこなう
    int minx=Math.min(oldx,Math.min(mx,lines[lineCount].start_x));
    int maxx=Math.max(oldx,Math.max(mx,lines[lineCount].start_x));
    int miny=Math.min(oldy,Math.min(my,lines[lineCount].start_y));
    int maxy=Math.max(oldy,Math.max(my,lines[lineCount].start_y));
    lineCount++;
    repaint(minx,miny,maxx-minx+1,maxy-miny+1);
  }
  public void mouseClicked(MouseEvent e){}
  public void mouseEntered(MouseEvent e){}
  public void mouseExited(MouseEvent e){}
   // MouseMotionListenerを実装するためのメソッド
  public void mouseDragged(MouseEvent e){
     // マウスカーソルの位置を得る
    int mx=e.getX(),my=e.getY();
     // デバッグ用の表示
    System.out.println("mouseDrag("+e+","+mx+","+my+")");
     // 配列linesのlineCount番目の始点を変更
    int oldx=lines[lineCount].end_x;
    int oldy=lines[lineCount].end_y;
    lines[lineCount].end_x=mx;
    lines[lineCount].end_y=my;
     // 再表示をおこなう
    int minx=Math.min(oldx,Math.min(mx,lines[lineCount].start_x));
    int maxx=Math.max(oldx,Math.max(mx,lines[lineCount].start_x));
    int miny=Math.min(oldy,Math.min(my,lines[lineCount].start_y));
    int maxy=Math.max(oldy,Math.max(my,lines[lineCount].start_y));
    repaint(minx,miny,maxx-minx+1,maxy-miny+1);
  }
  public void mouseMoved(MouseEvent e){}
}
クリッピングをおこなうというテクニックはオフスクリーンイメージと組み 合わせた場合も有効である.しかし,画面の変更のあった範囲の設定を間違え ると,画面が正しく書き変わらないので注意が必要となる.
次に進む