モノクロ2値お絵かき その1


実行中の画面

戻る


 まず手始めにモノクロ2値お絵描きツールを作ってみることにします。それも3度に分けて旧MSX版ACE-DRAWのDRAW、LUPE、COPY&PASTEの各コマンドごとに章を分けて記述する予定です。

DRAWコマンドの実装

 最初は線を引いたり消しゴムで消したりする機能を実装します。(MSX版のDRAWコマンドにあったFROM-P、FROM-N/R機能はアニメのフレームコントロールができてから実装することにし、今回はパスします)

 線の引き方として「自由曲線モード」「単直線モード」「ポリ直線モード」の3種類の描画モードがあります。モード切り替えは画面下部に並んでいる3つのボタンで行います。
 MSX版にはなかった単直線モードが追加されました。また、単直線及びポリ直線モードで仮描画線が出るようになったので線の位置の目安が付けやすくなりました。
「自由曲線モード」は、マウスの左ボタンを押しながらマウスを動かすとカーソルの軌跡が黒いドットで残ります。「単直線モード」は線の引き始めでマウスの左ボタンを押し、そのままボタンを離さずに引き終わりの点まで移動してボタンを離すと2点間を結ぶ直線を描きます。「ポリ直線モード」は、画面上で左ボタンをクリックするとクリックされた点を次々直線で結んでいきます。

 右ボタンは「消しゴム」で、カーソル付近の小さい領域を白いドットで消します。右ボタンを押すとカーソルの形が変わるようにしたかったのですが、今回そこまで手が回りませんでした。最終バージョンまでにはなんとか実現したいと思います。

 さらに、MSX版ではDRAWモードにはなかった(COPY&PASTEのみにあった)undo機能と、カーソル座標の表示が追加されました。

 以下がプログラムリストです。コメントを見れば大体何をやっているのかわかると思います。
// Java版AceDrawのためのステップ
// 「モノクロ2値お絵かき その1」McOekaki1.java
// Copyright(c)2006 A.Hiramatsu

import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;

public class McOekaki1 {
  private final int imgWidth = 320;
  private final int imgHeight = 240;

  private byte[] mono = {0,(byte)255};
  private IndexColorModel cmMono = new IndexColorModel(1,2,mono,mono,mono);
  private BufferedImage img = new BufferedImage(imgWidth,imgHeight,
                                      BufferedImage.TYPE_BYTE_INDEXED,cmMono);

  private Graphics2D imgGfx;

  private JPanel grpPnl;
  private JLabel infoLbl;
  private JToggleButton freeBtn;
  private JToggleButton snglBtn;
  private JToggleButton polyBtn;
  private JButton undoBtn;

  private boolean drawing = false;
  private boolean dragging = false;
  private boolean erasing = false;
  private boolean tempdraw = false;
  private boolean inXORmode = false;
  private int drawLatX,drawLatY;
  private int drawCurX,drawCurY;
  private enum DrawMode {none,free,sngl,poly};
  private DrawMode drawmode;

  private byte[] buffundo = new byte[imgWidth*imgHeight];
  private byte[] buffredo = new byte[imgWidth*imgHeight];
  private boolean canundo,canredo;

  private void setUndo() {
    img.getRaster().getDataElements(0,0,		// 画面イメージを
                      imgWidth,imgHeight,buffundo);	// undoバッファに保存
    canundo=true;					// 「undo可能だがまだ一度もundoしてない」
    canredo=false;					// 状態にする
  }

  private void drawTempLine(int x,int y) {
    if((drawLatX != x) || (drawLatY != y)) {	// 長さが0でなければ
      imgGfx.drawLine(drawLatX,drawLatY,x,y);	// 仮描画する
      drawCurX=x;				// 仮描画位置更新
      drawCurY=y;
      tempdraw=true;				// 仮描画済み
    }
  }

  private void cancelLine() {
    if(drawmode.equals(DrawMode.sngl) ||	// 仮描画するのは単直線かポリ直線のみ
       drawmode.equals(DrawMode.poly)) {

      if(tempdraw) {				// 仮描画線を引いていたら
        if((drawLatX != drawCurX) ||
           (drawLatY != drawCurY)) {

          imgGfx.drawLine(drawLatX,drawLatY,	// 仮描画線を消す
                          drawCurX,drawCurY);
        }
      }
    }
    tempdraw=false;
  }

  private void toXORmode() {
    imgGfx.setColor(Color.WHITE);
    imgGfx.setXORMode(Color.BLACK);
    inXORmode = true;
  }

  private void exitXORmode() {
    if(inXORmode) {
      imgGfx.setPaintMode();
    }
    inXORmode = false;
  }

  MouseListener grpMLsn = new MouseAdapter() {		// マウスボタンイベント
    public void mousePressed(MouseEvent e) {
      int x = e.getX();
      int y = e.getY();

      if(e.getButton()==MouseEvent.BUTTON1) {		// 左ボタン押された
        if(!erasing) {					// 消しゴム使用中でなければ

          setUndo();
          if(drawmode.equals(DrawMode.free)) {		// 自由曲線モード
            drawing=true;
            dragging=false;
            drawLatX=x;					// 現在位置を最後の位置とする
            drawLatY=y;

          } else if(drawmode.equals(DrawMode.sngl)) {	// 単直線モード
            drawing=true;
            drawLatX=x;					// 現在位置を最後の位置とする
            drawLatY=y;
            drawCurX=x;					// 仮描画用位置
            drawCurY=y;
            toXORmode();				// 仮描画のためにXORモードにする

          } else if(drawmode.equals(DrawMode.poly)) {	// ポリ直線モード
            if(drawing) {				// すでに描画中なら
              if((drawLatX != x) || (drawLatY != y)) {
                cancelLine();				// 仮描画した線を消す
                exitXORmode();				// XORモードを抜ける
                imgGfx.setColor(Color.BLACK);
                imgGfx.drawLine(drawLatX,drawLatY,x,y);	// 最後に打った点から線を引く
                grpPnl.repaint();
              }
            }
            toXORmode();				// 仮描画のためXORモードにする
            drawing=true;				// 描画中にする
            drawLatX=x;					// 最後の位置を更新
            drawLatY=y;
            drawCurX=x;					// 仮描画用位置
            drawCurY=y;
          } else {
          }
        }

      } else if(e.getButton()==MouseEvent.BUTTON3) {	// 右ボタン押された
        if(drawing) {					// 描画中で
          if(drawmode.equals(DrawMode.poly)) {		// なおかつポリ直線モードなら
            cancelLine();				// 仮描画した線を消す
            exitXORmode();				// XORモードを抜ける
            drawing = false;				// 描画中を解除
            grpPnl.repaint();
          }
        } else {					// 描画中でなければ
          setUndo();
          imgGfx.setColor(Color.WHITE);			// 消しゴム
          imgGfx.drawLine(x-1,y-2,x+1,y-2);
          imgGfx.fillRect(x-2,y-1,5,3);
          imgGfx.drawLine(x-1,y+2,x+1,y+2);
          grpPnl.repaint();
          erasing = true;				// 消しゴム使用中にする
        }
      }
    }

    public void mouseReleased(MouseEvent e) {
      int x = e.getX();
      int y = e.getY();

      if(e.getButton()==MouseEvent.BUTTON1) {		// 左ボタン離された

        if(drawmode.equals(DrawMode.free)) {		// 自由曲線モード
          if(drawing && !dragging) {
            img.setRGB(drawLatX,drawLatY,0);		// ドラッグしてなければ1点だけ打つ
            grpPnl.repaint();
          }
          drawing=false;
          dragging=false;

        } else if(drawmode.equals(DrawMode.sngl)) {	// 単直線モード
          if(drawing) {
            cancelLine();				// 仮描画した線を消す
            exitXORmode();				// XORモードを抜ける
            if((drawLatX != x) || (drawLatY != y)) {	// 線の長さが0でなければ
              imgGfx.setColor(Color.BLACK);
              imgGfx.drawLine(drawLatX,drawLatY,
                              drawCurX,drawCurY);	// 正式に線を引く
            }
            grpPnl.repaint();
          }
          drawing=false;
        } else {
        }

      } else if(e.getButton()==MouseEvent.BUTTON3) {	// 右ボタン離された
        erasing=false;					// 消しゴム使用中を解除
      } else {
      }
    }

    public void mouseExited(MouseEvent e) {		// マウスが描画領域を出た
      infoLbl.setText("");
    }
  };

  MouseMotionListener grpMmLsn = new MouseMotionAdapter() {	// マウス移動イベント
    public void mouseDragged(MouseEvent e) {
      int x = e.getX();
      int y = e.getY();

      infoLbl.setText( "(" + Integer.toString(x) +
                       "," + Integer.toString(y) + ")" );	// 座標表示

      if(drawing) {

        if(drawmode.equals(DrawMode.free)) {		// 自由曲線モード
          if((drawLatX != x) || (drawLatY != y)) {	// カーソルが動いていたら
            imgGfx.setColor(Color.BLACK);
            imgGfx.drawLine(drawLatX,drawLatY,x,y);	// 線を引く
            drawLatX=x;					// 最後の位置を更新
            drawLatY=y;
            dragging=true;
            grpPnl.repaint();
          }
        } else if(drawmode.equals(DrawMode.sngl) ||	// 単直線モード または
                  (drawmode.equals(DrawMode.poly) &&	// ポリ直線モードで描画中なら
                                         drawing)) {
          cancelLine();					// 仮描画線を消す
          drawTempLine(x,y);				// 新しい仮描画線を書く
          grpPnl.repaint();
        } else {
        }

      } else if(erasing) {				// 消しゴム使用中
        imgGfx.setColor(Color.WHITE);			// 消しゴム
        imgGfx.drawLine(x-1,y-2,x+1,y-2);
        imgGfx.fillRect(x-2,y-1,5,3);
        imgGfx.drawLine(x-1,y+2,x+1,y+2);
        grpPnl.repaint();
      }
    }

    public void mouseMoved(MouseEvent e) {
      int x = e.getX();
      int y = e.getY();

      infoLbl.setText( "(" + Integer.toString(x) +
                       "," + Integer.toString(y) + ")" );	// 座標表示

      if(drawmode.equals(DrawMode.poly)) {		// ポリ直線モードで
        if(drawing) {					// 描画中なら
          cancelLine();					// 仮描画線を消す
          drawTempLine(x,y);				// 新しい仮描画線を描く
          grpPnl.repaint();
        }
      }
    }
  };

  MouseListener undoBtnLsn = new MouseAdapter() {	// undoボタンイベント
    public void mousePressed(MouseEvent e) {
      cancelLine();
      exitXORmode();

      if(canredo) {						// redo可(一度undoしている)
        if(canundo) {						// undo可なら
          img.getRaster().setDataElements(0,0,			// undoバッファの内容を
                            imgWidth,imgHeight,buffundo);	// 画面に復帰
        } else {
          img.getRaster().setDataElements(0,0,			// undoバッファの内容を
                            imgWidth,imgHeight,buffredo);	// 画面に復帰
        }
        canundo = !canundo;					// undo可をトグル
      } else {
        if(canundo) {						// 描画後最初のundo
          img.getRaster().getDataElements(0,0,			// 現在の画面イメージを
                            imgWidth,imgHeight,buffredo);	// redoバッファに保存
          img.getRaster().setDataElements(0,0,			// undoバッファの内容を
                            imgWidth,imgHeight,buffundo);	// 画面に復帰
          canundo=false;
          canredo=true;						// redo可にする
        }
      }
      grpPnl.repaint();
      drawing=false;
      dragging=false;
      erasing=false;
    } 
  };

  MouseListener freeBtnLsn = new MouseAdapter() {	// freeボタン
    public void mousePressed(MouseEvent e) {
      cancelLine();
      exitXORmode();
      grpPnl.repaint();
      drawmode=DrawMode.free;
      drawing=false;
      dragging=false;
      erasing=false;
    } 
  };

  MouseListener snglBtnLsn = new MouseAdapter() {	// 単直線ボタン
    public void mousePressed(MouseEvent e) {
      cancelLine();
      exitXORmode();
      grpPnl.repaint();
      drawmode=DrawMode.sngl;
      drawing=false;
      dragging=false;
      erasing=false;
    } 
  };

  MouseListener polyBtnLsn = new MouseAdapter() {	// ポリ直線ボタン
    public void mousePressed(MouseEvent e) {
      cancelLine();
      exitXORmode();
      drawmode=DrawMode.poly;
      drawing=false;
      dragging=false;
      erasing=false;
    } 
  };

  private void doProg() {
    drawmode = DrawMode.none;
    imgGfx=img.createGraphics();
    imgGfx.setColor(Color.WHITE);
    imgGfx.fillRect(0,0,imgWidth,imgHeight);
    grpPnl.repaint();
    grpPnl.addMouseListener(grpMLsn);
    grpPnl.addMouseMotionListener(grpMmLsn);
    undoBtn.addMouseListener(undoBtnLsn);
    freeBtn.addMouseListener(freeBtnLsn);
    snglBtn.addMouseListener(snglBtnLsn);
    polyBtn.addMouseListener(polyBtnLsn);
  }

  private void makeFrame() {
    JFrame.setDefaultLookAndFeelDecorated(true);
    JFrame frm = new JFrame("モノクロ2値お絵かき その1");
    frm.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    JPanel toolPnl = new JPanel(new GridLayout(1,3,2,0));
    freeBtn =new JToggleButton("free");
    snglBtn =new JToggleButton("single");
    polyBtn =new JToggleButton("poly");
    toolPnl.add(freeBtn);
    toolPnl.add(snglBtn);
    toolPnl.add(polyBtn);
    ButtonGroup toolBtns = new ButtonGroup();
    toolBtns.add(freeBtn);
    toolBtns.add(snglBtn);
    toolBtns.add(polyBtn);


    JPanel btnPnl = new JPanel(new BorderLayout(8,0));
    undoBtn = new JButton("undo");
    btnPnl.add(undoBtn,BorderLayout.WEST);
    btnPnl.add(toolPnl,BorderLayout.CENTER);

    grpPnl = new JPanel() {
      public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(img,0,0,this);
      }
    };

    grpPnl.setPreferredSize(new Dimension(imgWidth,imgHeight));

    JPanel infoPnl = new JPanel(new BorderLayout());
    infoPnl.setPreferredSize(new Dimension(imgWidth,14));
    infoLbl=new JLabel();
    infoLbl.setFont(new Font("Monospaced",Font.PLAIN,10));
    infoPnl.add(infoLbl,BorderLayout.WEST);

    JPanel grpBasePnl = new JPanel(new BorderLayout());
    grpBasePnl.add(grpPnl,BorderLayout.CENTER);
    grpBasePnl.add(infoPnl,BorderLayout.SOUTH);

    JPanel mainPnl = new JPanel(new BorderLayout(0,4));
    mainPnl.add(grpBasePnl,BorderLayout.CENTER);
    mainPnl.add(btnPnl,BorderLayout.SOUTH);
    mainPnl.setBorder(new EmptyBorder(4,4,4,4));

    frm.getContentPane().add(mainPnl);
    frm.pack();
    frm.setVisible(true);
  }

  public static void main(final String[] args) {
    McOekaki1 myProg = new McOekaki1();
    myProg.makeFrame();
    myProg.doProg();
  }
}



愚痴コーナー

 Javaで自作ツールを作るのは結構楽しいのですが、Javaに慣れていないと結構戸惑うことも少なくありませんでした。Javaというシステムはかなり大きいので、覚えることが多いのは仕方ないのかもしれません。Javaに慣れてしまい、多少理不尽な点も「そういうものなんだ」と割りきればかなり楽になります。

 まず、BufferedImage、Raster、カラーモデル、サンプルモデルなどの関係がSunのJavaドキュメントを読んでもなかなか把握できず困りました。また配列変数とBufferedImageとの間でデータをやり取りするのに似たようなメソッドがたくさんあり、どれを使えばいいのか悩みました(BufferedImage.getRGB()とかRaster.getDataElements()とかRaster.getPixels()とか、あるいはDataBufferなんてクラスを使わなきゃいけないのか? とか)。試行錯誤でなんとかここまでたどり着きましたが・・・

 また、TYPE_BYTE_INDEXED や TYPE_BYTE_BINARYの BufferedImage に描画する際、「パレット何番の色で描け」みたいな指定はできないようです。あくまでも Color.WHITE とか Color.BLACK とか指定するか、またはRGB値を直接指定するしかないようです(すると近い色のパレット番号が自動的に選ばれる)。この仕様だと、たとえばパレット7番と15番に同じ色を割り当てておいて、見た目同じ色に見えても違う意味を持たせる(ゲームソフトなどで当たり判定のあるなしなど)ような使い方をするとき困ってしまうと思うのですが・・・

 それと、最初はモノクロ2値画像だからTYPE_BYTE_BINARYを使おうと思いましたが、なぜか正しく描画できず(Javaのバグ?)結構時間を無駄にしました。結局TYPE_BYTE_BINARYは諦め、TYPE_BYTE_INDEXEDを使ってメモリ上では1ドット=1バイトとして扱うことにしました。最終的にファイルに書きこむ時点でに1ドット=1ビットにしようと思います。

 あと、Delphiなんかと比べると、ウインドウにGUIパーツを並べるのがかなり面倒くさいです。まだパネル数枚とボタン数個で、それほど複雑なGUIにはなっていないにも関わらず、makeFrame()の記述は結構煩雑です。