JavaのJFrame・Canvasクラスの使い方と応用

#はじめに
JFrame・Canvasを最後に使ったのは、大学の卒業研究のときです。
久々にJavaを使ったら、なかなか言うことを聞いてくれない…:sweat_drops:

Javaの使い方をどんどん忘れてしまっているので、
リハビリ代わりにJFrame・Canvasについてを備忘録として書き残したいと思います:writing_hand:
<br>
#今回作る物
Jframe・Canvasクラスを使って、簡単なお絵かきアプリを作りたいと思います。
完成品は、画像のようなものになります。:art:
![Screenshot 2018-11-27 at 10.06.48.png](https://qiita-image-store.s3.amazonaws.com/0/308385/1a7c8648-9e0b-175e-1a5f-1824860b36b9.png)
<br>
#Step1: JFrameを用意する
まず、アプリを表示するウィンドウを作りましょう。
ここでは、JFrameを使います。
使い方はこんな感じ。

```java:Step1.java
package paintApp;

import javax.swing.JFrame;

public class Step1 {
	static int w = 800, h = 600;

	public static void main(String[] args) {
		run();
	}

	public static void run() {
		// JFrameのインスタンスを生成
		JFrame frame = new JFrame("お絵かきアプリ");
		// ウィンドウを閉じたらプログラムを終了する
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// ウィンドウのサイズ・初期位置
		frame.setSize(w, h);
		frame.setLocationRelativeTo(null);
		// setBounds(x, y, w, h);

		// ウィンドウを表示
		frame.setVisible(true);
	}

}
```

簡単にフレームが作れちゃいました:v:
![Screenshot 2018-11-27 at 09.49.39.png](https://qiita-image-store.s3.amazonaws.com/0/308385/294e2b17-3243-43ca-a105-f415677a5070.png)
<br>
#Step2: Canvasを用意する
次は、実際に絵を描くキャンバスを用意します。
JavaにはCanvasクラスという便利なクラスが用意されています。
フレームに追加してみましょう。

```java:Step2.java
package paintApp;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Step2 {
	static int w = 800, h = 600;

	public static void main(String[] args) {
		run();
	}

	public static void run() {
		// JFrameのインスタンスを生成
		JFrame frame = new JFrame("お絵かきアプリ");
		// ウィンドウを閉じたらプログラムを終了する
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// ウィンドウのサイズ・初期位置
		frame.setSize(w, h);
		frame.setLocationRelativeTo(null);
		// setBounds(x, y, w, h);

		// PaintCanvasのインスタンスを生成
		PaintCanvas canvas = new PaintCanvas();

		// フレームに追加
		JPanel pane = new JPanel();
		frame.getContentPane().add(pane);

		canvas.setPreferredSize(new Dimension(w, h));
		pane.add(canvas);

		// ウィンドウを表示
		frame.setVisible(true);
	}

	// キャンバスクラス
	static class PaintCanvas extends Canvas {

		public PaintCanvas() {
			// キャンバスの背景を白に設定
			setBackground(Color.white);
		}

		public void paint(Graphics g) {
		}

	}

}
```

真っ白なキャンバスが現れました。
![Screenshot 2018-11-27 at 09.50.11.png](https://qiita-image-store.s3.amazonaws.com/0/308385/13d70ee7-a1fb-905c-3ccb-51ae4384bf8d.png)
ここに絵を描けるようにしていきましょう。
<br>
#Step3: マウス操作を検知する
”マウスがクリックされた”、”ドラッグされた”といった事象を「イベント」と言います。
このイベントを取得させることができる便利なクラスが用意されています。
[MouseListener](https://docs.oracle.com/javase/jp/8/docs/api/java/awt/event/MouseListener.html)と[MouseMotionListener](https://docs.oracle.com/javase/jp/8/docs/api/java/awt/event/MouseMotionListener.html)というクラスです。
MouseListenerとMouseMotionListenerの違いは、取得できるイベントの種類です。
使いたいイベントの種類に合わせて使い分けましょう。
今回は、クリック・ドラッグ・ムーブを利用したいので、どちらも使います。
<br>
**取得できるイベント**

| MouseListener | MouseMotionListener |
|:-:|:-:|
| mouseClicked | mouseDragged |
| mouseEntered | mouseMoved |
| mouseExited |  |
| mousePressed |  |
| mouseReleased |  |

これらを、キャンバスに追加していきましょう。

```java:Step3.java
package paintApp;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Step3 {
	static int w = 800, h = 600;

	public static void main(String[] args) {
		run();
	}

	public static void run() {
		// JFrameのインスタンスを生成
		JFrame frame = new JFrame("お絵かきアプリ");
		// ウィンドウを閉じたらプログラムを終了する
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// ウィンドウのサイズ・初期位置
		frame.setSize(w, h);
		frame.setLocationRelativeTo(null);
		// setBounds(x, y, w, h);

		// PaintCanvasのインスタンスを生成
		PaintCanvas canvas = new PaintCanvas();

		// フレームに追加
		JPanel pane = new JPanel();
		frame.getContentPane().add(pane);

		canvas.setPreferredSize(new Dimension(w, h));
		pane.add(canvas);

		// ウィンドウを表示
		frame.setVisible(true);
	}

	// キャンバスクラス
	static class PaintCanvas extends Canvas implements MouseListener,
			MouseMotionListener {

		public PaintCanvas() {
			// MouseListener・MouseMotionListenerを設定
			addMouseListener(this);
			addMouseMotionListener(this);
			// キャンバスの背景を白に設定
			setBackground(Color.white);
		}

		public void paint(Graphics g) {
		}

		@Override
		public void mouseDragged(MouseEvent e) {
			System.out.println(e);
		}

		@Override
		public void mouseMoved(MouseEvent e) {
		}

		@Override
		public void mouseClicked(MouseEvent e) {
		}

		@Override
		public void mousePressed(MouseEvent e) {
		}

		@Override
		public void mouseReleased(MouseEvent e) {
		}

		@Override
		public void mouseEntered(MouseEvent e) {
		}

		@Override
		public void mouseExited(MouseEvent e) {
		}

	}

}
```

このように、マウス操作に反応しているのがわかります。:mouse_three_button:
![Screenshot 2018-11-27 at 09.51.09.png](https://qiita-image-store.s3.amazonaws.com/0/308385/bcd3007c-aba4-331a-d9d8-d9b401186241.png)
<br>
#Step4: マウスに合わせて絵を描く
…これで舞台は整いました。
ここからは取得したマウスイベントを使って、絵を描き込む処理を作りましょう。

内容は、マウスでドラッグされたときに、開始点と終了点の座標を取得し、その間を線で結ぶ…
これだけです:ok_hand:

もう少し詳しく説明すると、MouseDraggedイベントが発生されたら、その時点でのマウスの座標を変数に保存。
再び同じイベントが発生したら、前の処理で保存した座標を、開始点として別の変数に保存し、現在の座標を上書きしています。こうして開始点、終了点を更新しながら、線を描いています。

これで、マウスがドラッグされるたびに少しずつ線が伸びてきて、あたかもマウスの軌跡をそのまま表示しているように見える、というわけです。

ちなみに、この線を描くときに、いちいち線のスタイルを両端が”丸”のものに設定しています。
こうしないと、両端が四角い線になり、線と線のつなぎ目がギザギザした、見栄えの悪い絵になってしまうためです。

```java:Step4.java
package paintApp;

import java.awt.BasicStroke;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Step4 {
	static int w = 800, h = 600;

	public static void main(String[] args) {
		run();
	}

	public static void run() {
		// JFrameのインスタンスを生成
		JFrame frame = new JFrame("お絵かきアプリ");
		// ウィンドウを閉じたらプログラムを終了する
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// ウィンドウのサイズ・初期位置
		frame.setSize(w, h);
		frame.setLocationRelativeTo(null);
		// setBounds(x, y, w, h);

		// PaintCanvasのインスタンスを生成
		PaintCanvas canvas = new PaintCanvas();

		// フレームに追加
		JPanel pane = new JPanel();
		frame.getContentPane().add(pane);

		canvas.setPreferredSize(new Dimension(w, h));
		pane.add(canvas);

		// ウィンドウを表示
		frame.setVisible(true);
	}

	// キャンバスクラス
	static class PaintCanvas extends Canvas implements MouseListener,
			MouseMotionListener {

		// 描画内容を保持するBufferedImage
		private BufferedImage cImage = null;
		// cImageに描画するためのインスタンス
		private Graphics2D g2d;

		// 線の開始座標・終了座標
		private int x, y, xx, yy;
		// 描画モードＯＲ消しゴムモード
		private int type;
		// 線の太さ
		public int width = 1;
		// 線の色
		public Color c = Color.black;

		public PaintCanvas() {
			// 座標を初期化
			x = -1;
			y = -1;
			xx = -1;
			yy = -1;
			type = 0;

			// MouseListener・MouseMotionListenerを設定
			addMouseListener(this);
			addMouseMotionListener(this);

			// キャンバスの背景を白に設定
			setBackground(Color.white);

			// 描画内容を保持するBufferedImageを生成
			cImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
			g2d = (Graphics2D) cImage.getGraphics();
			// BufferedImageの背景も白にする
			g2d.setColor(Color.WHITE);
			g2d.fillRect(0, 0, w, h);

			// 描画
			repaint();
		}

		public void paint(Graphics g) {
			// 描画モード（線分を描画）
			if (type == 1) {
				if (x >= 0 && y >= 0 && xx >= 0 && yy >= 0) {
					BasicStroke stroke = new BasicStroke(width,
							BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
					g2d.setStroke(stroke);
					g2d.setColor(c);
					g2d.drawLine(xx, yy, x, y);
				}
				// 消しゴムモード
			} else if (type == 2) {
				if (x >= 0 && y >= 0 && xx >= 0 && yy >= 0) {
					// 両端が丸い線分に設定
					BasicStroke stroke = new BasicStroke(50.0f,
							BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
					g2d.setStroke(stroke);
					g2d.setColor(Color.white);
					g2d.drawLine(xx, yy, x, y);
				}
			}

			// 描画内容をキャンバスにも反映
			g.drawImage(cImage, 0, 0, null);
		}

		@Override
		public void mouseDragged(MouseEvent e) {
			// 押されているボタンを検知
			if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
				// 左ボタンクリック　（描画モード）
				type = 1;
			}
			if ((e.getModifiers() & MouseEvent.BUTTON2_MASK) != 0) {
				// 中央ボタンクリック
			}
			if ((e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0) {
				// 右ボタンクリック　（消しゴムモード）
				type = 2;
			}

			// 過去の座標を開始座標に設定
			xx = x;
			yy = y;

			// 新しい座標を終了座標に設定
			Point point = e.getPoint();
			x = point.x;
			y = point.y;

			// 再描画
			repaint();
		}

		@Override
		public void mouseMoved(MouseEvent e) {
			// ドラッグが終了したら座標を初期化
			x = -1;
			y = -1;
			xx = -1;
			yy = -1;
			type = 0;
		}

		@Override
		public void mouseClicked(MouseEvent e) {
		}

		@Override
		public void mousePressed(MouseEvent e) {
			Point point = e.getPoint();
			x = point.x;
			y = point.y;
		}

		@Override
		public void mouseReleased(MouseEvent e) {
		}

		@Override
		public void mouseEntered(MouseEvent e) {
		}

		@Override
		public void mouseExited(MouseEvent e) {
		}

	}

}
```

マウスでドラッグすると、そこに線を描きます。
一気にそれっぽくなりました！
![Screenshot 2018-11-27 at 09.52.19.png](https://qiita-image-store.s3.amazonaws.com/0/308385/18a9cba3-e075-5a02-ff1f-9ea8652546c9.png)
<br>
#完成！
見事にお絵かきアプリが完成しました！

画像では、キャンバスクリア・線の太さ変更・線の色変更の３機能が追加してあります。
これだけでも十分お絵描きできます。

これは簡易版なので、ここに機能を追加していけば、もっと本格的なお絵かきアプリにすることも出来ますよ:smiley:
![Screenshot 2018-11-27 at 10.06.48.png](https://qiita-image-store.s3.amazonaws.com/0/308385/3f82ad32-6d27-9aad-2292-300ff6985435.png)
最終的なコードはこちらです。

```java:PaintApp.java
package paintApp;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class PaintApp {
	static int w = 800, h = 600;

	public static void main(String[] args) {
		run();
	}

	public static void run() {
		// JFrameのインスタンスを生成
		JFrame frame = new JFrame("お絵かきアプリ");
		// ウィンドウを閉じたらプログラムを終了する
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// ウィンドウのサイズ・初期位置
		frame.setSize(w, h);
		frame.setLocationRelativeTo(null);
		// setBounds(x, y, w, h);

		// PaintCanvasのインスタンスを生成
		PaintCanvas canvas = new PaintCanvas();

		// フレームに追加
		JPanel pane = new JPanel();
		frame.getContentPane().add(pane, BorderLayout.CENTER);
		JPanel paneB = new JPanel();
		frame.getContentPane().add(paneB, BorderLayout.NORTH);

		canvas.setPreferredSize(new Dimension(w, h));
		pane.add(canvas);

		/* 追加機能 */
		// 全消去
		JButton clear = new JButton("CLEAR");
		clear.addActionListener(new ClearListener(canvas));
		paneB.add(clear);

		// 線の太さ調節
		JSlider slider = new JSlider(1, 50, 1); // 最小値、最大値、初期値
		slider.addChangeListener(new SliderListener(canvas)); // 割り込み処理用
		paneB.add(slider);

		// 線の色変更
		String[] combodata = { "BLACK", "RED", "BLUE", "GREEN" };
		JComboBox combo = new JComboBox(combodata);
		combo.addActionListener(new ComboListener(canvas));
		paneB.add(combo);

		// ウィンドウを表示
		frame.setVisible(true);
	}

	// キャンバスクラス
	static class PaintCanvas extends Canvas implements MouseListener,
			MouseMotionListener {

		// 描画内容を保持するBufferedImage
		private BufferedImage cImage = null;
		// cImageに描画するためのインスタンス
		private Graphics2D g2d;

		// 線の開始座標・終了座標
		private int x, y, xx, yy;
		// 描画モードＯＲ消しゴムモード
		private int type;
		// 線の太さ
		public int width = 1;
		// 線の色
		public Color c = Color.black;

		public PaintCanvas() {
			// 座標を初期化
			x = -1;
			y = -1;
			xx = -1;
			yy = -1;
			type = 0;

			// MouseListener・MouseMotionListenerを設定
			addMouseListener(this);
			addMouseMotionListener(this);

			// キャンバスの背景を白に設定
			setBackground(Color.white);
			// 描画内容を保持するBufferedImageを生成
			cImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
			g2d = (Graphics2D) cImage.getGraphics();
			// BufferedImageの背景も白にする
			g2d.setColor(Color.WHITE);
			g2d.fillRect(0, 0, w, h);

			// 描画
			repaint();
		}

		// キャンバスをクリア
		public void clear() {
			g2d.setColor(Color.WHITE);
			g2d.fillRect(0, 0, w, h);
			repaint();
		}

		// 線の太さ変更
		public void setStroke(int n) {
			width = n;
		}

		// 線の色変更
		public void setColorCombo(String color) {
			if (color.equals("BLACK")) {
				c = Color.black;
			} else if (color.equals("RED")) {
				c = Color.red;
			} else if (color.equals("BLUE")) {
				c = Color.blue;
			} else if (color.equals("GREEN")) {
				c = Color.green;
			}
		}

		public void paint(Graphics g) {
			// 描画モード（線分を描画）
			if (type == 1) {
				if (x >= 0 && y >= 0 && xx >= 0 && yy >= 0) {
					BasicStroke stroke = new BasicStroke(width,
							BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
					g2d.setStroke(stroke);
					g2d.setColor(c);
					g2d.drawLine(xx, yy, x, y);
				}
				// 消しゴムモード
			} else if (type == 2) {
				if (x >= 0 && y >= 0 && xx >= 0 && yy >= 0) {
					// 両端が丸い線分に設定
					BasicStroke stroke = new BasicStroke(50.0f,
							BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
					g2d.setStroke(stroke);
					g2d.setColor(Color.white);
					g2d.drawLine(xx, yy, x, y);
				}
			}

			// 描画内容をキャンバスにも反映
			g.drawImage(cImage, 0, 0, null);
		}

		@Override
		public void mouseDragged(MouseEvent e) {
			// 押されているボタンを検知
			if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
				// 左ボタンクリック　（描画モード）
				type = 1;
			}
			if ((e.getModifiers() & MouseEvent.BUTTON2_MASK) != 0) {
				// 中央ボタンクリック
			}
			if ((e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0) {
				// 右ボタンクリック　（消しゴムモード）
				type = 2;
			}

			// 過去の座標を開始座標に設定
			xx = x;
			yy = y;

			// 新しい座標を終了座標に設定
			Point point = e.getPoint();
			x = point.x;
			y = point.y;

			// 再描画
			repaint();
		}

		@Override
		public void mouseMoved(MouseEvent e) {
			// ドラッグが終了したら座標を初期化
			x = -1;
			y = -1;
			xx = -1;
			yy = -1;
			type = 0;
		}

		@Override
		public void mouseClicked(MouseEvent e) {
		}

		@Override
		public void mousePressed(MouseEvent e) {
			Point point = e.getPoint();
			x = point.x;
			y = point.y;
		}

		@Override
		public void mouseReleased(MouseEvent e) {
		}

		@Override
		public void mouseEntered(MouseEvent e) {
		}

		@Override
		public void mouseExited(MouseEvent e) {
		}

	}

	// クリアボタン用
	static class ClearListener implements ActionListener {

		PaintCanvas canvas;

		public ClearListener(PaintCanvas canvas) {
			super();
			this.canvas = canvas;
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			canvas.clear();
		}

	}

	// スライダー用
	static class SliderListener implements ChangeListener {

		PaintCanvas canvas;

		public SliderListener(PaintCanvas canvas) {
			super();
			this.canvas = canvas;
		}

		@Override
		public void stateChanged(ChangeEvent e) {
			JSlider source = (JSlider) e.getSource();
			int fps = (int) source.getValue();
			canvas.setStroke(fps);
		}

	}

	// コンボボックス用
	static class ComboListener implements ActionListener {

		PaintCanvas canvas;

		public ComboListener(PaintCanvas canvas) {
			super();
			this.canvas = canvas;
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			JComboBox source = (JComboBox) e.getSource();
			String color = (String) source.getSelectedItem();
			canvas.setColorCombo(color);
		}

	}

}
```

<br>
#さいごに
今回はじめてこういった場所に投稿したので、至らない部分等多くあると思いますが、
少しでも皆様のお役に立てれば幸いです。

最後まで読んでくださり、ありがとうございました！