Java道|マウス操作で描画する

押した場所に、しるしが現れる。
マウス操作で描画する仕組みを学ぶと、JavaのGUIアプリは「表示する画面」から「操作に応じて描き変わる画面」へ進化します。

これまでのGUIアプリケーションでは、ウィンドウを表示したり、ラベルやボタンを配置したり、画像を読み込んで表示したりしてきました。

Frame でウィンドウを作る。
Label で文字を表示する。
Button で操作できる部品を置く。
MouseListener でマウスが部品に入ったり出たりしたときの処理を書く。
Image と Graphics を使って画像を表示する。

ここまで学ぶと、JavaのGUI画面はかなり豊かになります。

しかし、さらに一歩進むと、ユーザーが操作した位置に応じて、画面そのものを描き変えられるようになります。

たとえば、画面のある場所をマウスで押す。
その位置をJavaが読み取る。
読み取った座標を保存する。
画面を描き直す。
保存した座標に丸や図形を描く。

この流れができるようになると、GUIアプリはただ情報を表示するだけではなく、ユーザーの操作に合わせて反応する画面になります。

鬼滅の刃風にたとえると、鬼殺隊本部の修行場にある大きな修行案内盤を、水月が指で押したとします。

すると、その押した場所に青白く光る「気のしるし」が現れる。
別の場所を押すと、今度はその場所にしるしが移る。
修行案内盤が、隊士の操作に合わせて反応している状態です。

今回のテーマは、この「押した場所に描く」仕組みです。

マウスイベントで押した位置を受け取り、その座標を使って paint の中で図形を描きます。
そして、イベント処理と描画処理をつなぐ重要な役割として repaint が登場します。

この記事では、具体的な例示プログラムとして Sample6.java を使い、マウスで押した場所にオレンジ色の気のしるしを描く流れを、鬼滅の刃風の世界観でやわらかく整理していきます。

マウス操作で描画するとは何か

マウス操作で描画するとは、ユーザーが画面を押した位置を調べ、その場所に図形や画像を描くことです。

通常の描画では、あらかじめ決めた座標に文字や図形を描きます。

たとえば、x が 30、y が 30 なら、その位置に図形を描きます。

int x = 30;
int y = 30;

しかし、マウス操作で描画する場合は、この x と y の値を、ユーザーがクリックした場所に合わせて変えます。

処理内容
マウスを押すユーザーが画面上の場所を指定する
座標を取得するMouseEvent から x座標、y座標を取り出す
変数に保存する取得した座標を x と y に入れる
repaint を呼ぶ画面を描き直すように依頼する
paint が動く最新の x と y を使って図形を描く

鬼滅の刃風にたとえると、修行場の床にある案内盤を水月が押すと、その押した場所の座標が記録されます。

そして記録係が「その位置に気のしるしを描き直してください」と案内盤に伝えます。

案内盤は最新の座標を見て、その場所に丸いしるしを描きます。

つまり、マウス操作で描画するポイントは、次の3つです。

ポイント役割
MouseEvent押した場所の情報を持つ
x と y描画する位置を保存する
repaint保存した位置で画面を描き直すきっかけを作る

この3つがつながることで、ユーザーの操作が画面の変化として反映されます。

図:マウス操作で描画する全体の流れ

↓クリックすると拡大表示されます。

この図が示していること

この図は、マウスで画面を押してから、その場所に図形が描かれるまでの流れを表しています。

Frame がマウス操作を受け取ります。

マウスが押されると MouseEvent が発生します。

mousePressed の中で getX と getY を使い、押された位置の座標を取り出します。

その座標を x と y に保存し、repaint を呼びます。

その後、paint が呼ばれ、fillOval を使って保存された座標に丸い図形を描きます。

図の要素意味
Frameマウスイベントを受け取る画面全体
mousePressedマウスが押されたときに呼ばれる処理
getX、getY押した位置の座標を取得する
x、y描画位置を保存する変数
repaint再描画を依頼する
paint実際に画面へ描画する
fillOval丸い図形を描く

この図から分かることは、マウス操作と描画処理は直接ひとつながりで動くのではなく、座標の保存と repaint を間に挟んでつながっているということです。

マウスで描画する例

ファイル名:Sample6.java

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

public class Sample6 extends Frame
{
    int x = 30;
    int y = 30;

    public static void main(String[] args)
    {
        Sample6 sm = new Sample6();
    }

    public Sample6()
    {
        super("気のしるしを描く");

        addWindowListener(new SampleWindowListener());
        addMouseListener(new SampleMouseAdapter());

        setSize(420, 320);
        setVisible(true);
    }

    public void paint(Graphics g)
    {
        // 見出しを描画する
        g.setColor(Color.black);
        g.drawString("画面を押すと、その場所に気のしるしが現れます", 60, 60);

        // 気のしるしを描画する
        g.setColor(Color.orange);
        g.fillOval(x, y, 20, 20);
    }

    class SampleWindowListener extends WindowAdapter
    {
        public void windowClosing(WindowEvent e)
        {
            System.exit(0);
        }
    }

    class SampleMouseAdapter extends MouseAdapter
    {
        public void mousePressed(MouseEvent e)
        {
            // 押した位置を保存する
            x = e.getX();
            y = e.getY();

            // 画面を再描画する
            repaint();
        }
    }
}

このプログラムでできること

このプログラムを実行すると、「気のしるしを描く」というタイトルのウィンドウが表示されます。

ウィンドウの中には、次の説明文が表示されます。

画面を押すと、その場所に気のしるしが現れます

そして、画面をマウスで押すと、その場所にオレンジ色の丸が描かれます。

最初は x が 30、y が 30 なので、左上寄りの位置に丸が描かれます。

その後、別の場所を押すと、その押した位置に丸が移動します。

できること内容
ウィンドウを表示するFrame を継承して画面を作る
マウス操作を受け取るaddMouseListener で MouseAdapter を登録する
押した位置を取得するMouseEvent の getX、getY を使う
座標を保存するx と y に代入する
画面を描き直すrepaint を呼ぶ
丸を描くpaint の中で fillOval を使う
色を変えるsetColor で黒やオレンジを指定する
閉じる操作に対応するWindowAdapter と windowClosing を使う

鬼滅の刃風にたとえると、鬼殺隊本部の修行案内盤に水月が触れると、その場所にオレンジ色の気のしるしが現れるアプリです。

押した場所が記録され、画面が描き直され、その場所に新しいしるしが出ます。

画面全体がマウスイベントの対象になる

今回のプログラムでは、ボタンではなく Frame に対してマウスイベントを登録しています。

addMouseListener(new SampleMouseAdapter());

この行は、Sample6 自身、つまり Frame に対して呼び出されています。

そのため、ウィンドウ全体でマウス操作を受け取る形になります。

前に学んだボタンイベントでは、ボタンがイベントの発生元でした。

bt.addActionListener(new SampleActionListener());

この場合、イベントの対象は Button です。

一方、今回の addMouseListener は、Frame に対して登録しているため、画面全体がマウスイベントの対象になります。

イベント登録イベントの対象
bt.addActionListener(...)ボタン
bt.addMouseListener(...)ボタン
addMouseListener(...)フレーム全体

鬼滅の刃風にたとえると、前のボタン操作は「修行開始札だけが反応する」仕組みでした。

今回のプログラムでは、「修行場の案内盤全体が反応する」仕組みです。

画面のどこを押したかを取得できるため、押した場所に図形を描けるようになります。

x と y は描画位置を保存する変数

このプログラムでは、クラスの中に x と y という変数を用意しています。

int x = 30;
int y = 30;

この2つは、丸を描く位置を保存するための変数です。

変数意味
x横方向の位置
y縦方向の位置

最初は、x が 30、y が 30 です。

そのため、アプリを起動した直後は、左上寄りの位置に丸が描かれます。

その後、マウスで画面を押すと、その位置の座標で x と y が上書きされます。

x = e.getX();
y = e.getY();

つまり、x と y は「最後に押された場所」を覚えるための変数です。

鬼滅の刃風にたとえると、水月が修行案内盤のどこを押したかを、記録係が巻物に書き留めているようなものです。

次に画面を描き直すとき、paint はその記録を見て、どこに気のしるしを描くかを決めます。

paint で説明文と丸を描く

画面に実際に描画しているのは paint メソッドです。

public void paint(Graphics g)
{
    // 見出しを描画する
    g.setColor(Color.black);
    g.drawString("画面を押すと、その場所に気のしるしが現れます", 60, 60);

    // 気のしるしを描画する
    g.setColor(Color.orange);
    g.fillOval(x, y, 20, 20);
}

この中では、2つの描画を行っています。

描画内容使うメソッド
説明文を描くdrawString
丸いしるしを描くfillOval

まず、色を黒に設定しています。

g.setColor(Color.black);

その後、説明文を描いています。

g.drawString("画面を押すと、その場所に気のしるしが現れます", 60, 60);

次に、色をオレンジに変えています。

g.setColor(Color.orange);

そして、x と y の位置に丸を描いています。

g.fillOval(x, y, 20, 20);

このように、Graphics を使うと、同じ paint の中で文字も図形も描けます。

鬼滅の刃風にたとえると、まず修行案内盤に黒い文字で説明を書き、そのあとオレンジ色の気のしるしを描いている状態です。

fillOval で丸い図形を描く

今回の図形描画の中心は fillOval です。

g.fillOval(x, y, 20, 20);

fillOval は、塗りつぶされた楕円を描くメソッドです。

引数の意味は次の通りです。

引数意味
x楕円を描き始める横位置
y楕円を描き始める縦位置
20楕円の幅
20楕円の高さ

幅と高さが同じなので、見た目は丸に近くなります。

今回のプログラムでは、この丸を「気のしるし」として表現しています。

コード描かれるもの
g.fillOval(x, y, 20, 20)x、y の位置に小さな丸を描く
g.fillOval(x, y, 40, 40)より大きな丸を描く
g.fillOval(x, y, 20, 40)縦長の楕円を描く

鬼滅の刃風にたとえると、fillOval は修行案内盤に丸い気の印を刻むための命令です。

押した場所にこの丸が描かれることで、ユーザーの操作が目に見える形で画面に残ります。

setColor で描画色を変える

図形や文字の色は setColor で指定します。

今回のプログラムでは、説明文を黒、丸をオレンジにしています。

g.setColor(Color.black);
g.drawString("画面を押すと、その場所に気のしるしが現れます", 60, 60);

g.setColor(Color.orange);
g.fillOval(x, y, 20, 20);

Graphics では、setColor で色を設定すると、その後に描くものがその色になります。

設定その後に描くもの
g.setColor(Color.black)黒で描かれる
g.setColor(Color.orange)オレンジで描かれる
g.setColor(Color.blue)青で描かれる
g.setColor(Color.red)赤で描かれる

つまり、paint の中では描く順番と色の設定が大切です。

今回の流れは次の通りです。

順番処理
1色を黒にする
2説明文を描く
3色をオレンジにする
4丸を描く

鬼滅の刃風にたとえると、まず黒い墨で説明文を書き、そのあとオレンジ色の光で気のしるしを描いているようなものです。

図:mousePressed で座標を取得し repaint する

↓クリックすると拡大表示されます。

この図が示していること

この図は、mousePressed の中で行われている処理を詳しく表しています。

ユーザーが画面を押すと MouseEvent が発生します。

MouseEvent には、押された場所の座標情報が入っています。

getX で横座標を取り出し、getY で縦座標を取り出します。

その値を x と y に保存したあと、repaint を呼びます。

repaint は画面を描き直してほしいという依頼です。

その結果、paint が呼ばれ、最新の x と y の位置に fillOval で丸が描かれます。

図の要素意味
マウスを押すmousePressed が呼ばれるきっかけ
MouseEvent e押した場所の情報を持つ
e.getX横座標を取得する
e.getY縦座標を取得する
x、y に保存次の描画位置を記録する
repaint再描画を依頼する
paint最新の位置に図形を描く

この図から分かることは、mousePressed は図形を直接描く場所ではなく、押した位置を記録し、再描画を依頼する場所だということです。

mousePressed で押した位置を取得する

マウスを押したときの処理は、SampleMouseAdapter クラスの中に書かれています。

class SampleMouseAdapter extends MouseAdapter
{
    public void mousePressed(MouseEvent e)
    {
        // 押した位置を保存する
        x = e.getX();
        y = e.getY();

        // 画面を再描画する
        repaint();
    }
}

mousePressed は、マウスボタンが押されたときに呼ばれるメソッドです。

今回の処理では、まず押された位置を取得しています。

x = e.getX();
y = e.getY();

MouseEvent の e には、マウス操作に関する情報が入っています。

その中から getX と getY を使って、クリックされた場所の座標を取り出します。

メソッド分かること
getX押された場所の横座標
getY押された場所の縦座標

これにより、ユーザーが画面のどこを押したのかをJavaが知ることができます。

鬼滅の刃風にたとえると、水月が修行案内盤のどの場所に触れたのかを、MouseEvent という通知札が正確に伝えてくれる状態です。

MouseAdapter を使う理由

今回のプログラムでは、MouseListener を直接実装するのではなく、MouseAdapter を継承しています。

class SampleMouseAdapter extends MouseAdapter

MouseListener を直接実装すると、mouseClicked、mousePressed、mouseReleased、mouseEntered、mouseExited の5つをすべて書く必要があります。

しかし、今回必要なのは mousePressed だけです。

そのため、MouseAdapter を使うと、必要なメソッドだけを書けます。

書き方特徴
MouseListener を実装する5つのメソッドをすべて書く必要がある
MouseAdapter を継承する必要なメソッドだけ書ける

今回のように、マウスを押したときだけ処理したい場合は、MouseAdapter が便利です。

鬼滅の刃風にたとえると、MouseListener はマウスのすべての動きを見張る正式な見張り係です。

MouseAdapter は、その中から必要な「押されたとき」だけを担当できる補助係のようなものです。

repaint の役割

このテーマで特に大切なのが repaint です。

repaint();

repaint は、画面をもう一度描き直してほしいときに呼び出すメソッドです。

ここで注意したいのは、repaint 自体が丸を描いているわけではないことです。

実際に丸を描くのは paint です。

repaint は、Javaに対して「画面を描き直してください」と依頼する役割を持っています。

メソッド役割
repaint再描画を依頼する
paint実際に画面に描画する

今回の流れは次のようになります。

順番起こること
1マウスを押す
2mousePressed が呼ばれる
3x と y が新しい座標に変わる
4repaint を呼ぶ
5paint が呼ばれる
6新しい x と y の位置に丸が描かれる

ここで重要なのは、x と y を変更しただけでは、画面の見た目がすぐに変わるとは限らないことです。

変数の値が変わっても、画面を描き直すタイミングが必要です。

そのために repaint を呼びます。

鬼滅の刃風にたとえると、記録係が「次のしるしはこの場所です」と巻物に書き換えただけでは、案内盤の表示はまだ変わりません。

支援隊士が案内盤に「最新の記録で描き直してください」と合図を送る必要があります。

その合図が repaint です。

paint と repaint の関係

paint と repaint は名前が似ているので混同しやすいです。

役割を分けると、かなり分かりやすくなります。

メソッド役割鬼滅の刃風のイメージ
paint実際に画面へ描く案内盤に文字やしるしを描く
repaint描き直しを依頼する案内盤へ再描画の合図を送る

repaint を呼ぶと、その結果として paint が呼ばれます。

ただし、paint を直接呼ぶのではなく、repaint を使って再描画を依頼するのが基本です。

repaint()
  ↓
Javaが再描画の必要を判断する
  ↓
paint(Graphics g) が呼ばれる
  ↓
drawString や fillOval で描く

GUIでは、画面は必要に応じて何度も描き直されます。

ウィンドウが表示されたとき、別のウィンドウに隠れて再び見えたとき、サイズが変わったとき、repaint が呼ばれたときなどに paint が呼ばれます。

だから、画面に表示したい内容は paint の中に書いておく必要があります。

変数の変更と画面更新は別のこと

初心者がつまずきやすいポイントは、変数を変えただけでは画面が自動的に変わるとは限らないことです。

今回のコードでは、mousePressed の中で x と y を変更しています。

x = e.getX();
y = e.getY();

これで、描画位置のデータは変わりました。

しかし、画面に表示されている丸を新しい位置に描き直すには、再描画が必要です。

そのため、次に repaint を呼びます。

repaint();
処理意味
x と y を変更する描画位置のデータを更新する
repaint を呼ぶ画面を描き直すように依頼する
paint が呼ばれる更新後のデータを使って描画する

鬼滅の刃風にたとえると、修行記録巻物の座標を書き換えるだけでは、案内盤のしるしはまだ古い位置のままです。

案内盤を描き直して、はじめて新しい位置にしるしが出ます。

この「データの更新」と「画面の更新」を分けて考えることが、とても大切です。

イベント処理と描画処理がつながる面白さ

今回の内容では、イベント処理と描画処理が組み合わさっています。

イベント処理は、ユーザーの操作を受け取ります。

描画処理は、画面に文字や図形を描きます。

repaint は、その間をつなぎます。

役割内容
mousePressedマウス操作を受け取る
getX、getY押された場所を調べる
x、y描画位置を保存する
repaint描き直しを依頼する
paint保存された座標を使って描画する
fillOval丸い図形を描く

鬼滅の刃風にたとえると、水月が修行案内盤を押すという行動が、気のしるしとしてその場に現れます。

操作と描画がつながるので、画面が生きているように感じられます。

この考え方が分かると、単なるボタン操作よりも一歩進んだGUI表現ができるようになります。

図:イベント処理と描画処理がつながる仕組み

↓クリックすると拡大表示されます。

この図が示していること

この図は、イベント処理と描画処理が repaint によってつながることを表しています。

左側のイベント処理では、mousePressed がマウス操作を受け取り、MouseEvent から座標を取得して x と y に保存します。

そのあと repaint を呼びます。

中央の repaint は、イベント処理と描画処理をつなぐ橋です。

右側の描画処理では、paint が呼ばれ、drawString で説明文を描き、fillOval で最新の x と y の位置に丸を描きます。

図の要素意味
mousePressedマウス操作を受け取るイベント処理
MouseEvent押した位置の情報
x、y描画位置の保存場所
repaint再描画を依頼する橋渡し
paint実際に画面を描く処理
fillOval気のしるしを描く処理

この図から分かることは、マウス操作で描画するプログラムでは、イベント処理と描画処理を分けて考え、その間を repaint でつなぐ必要があるということです。

この考え方を広げると何ができるか

今回の Sample6.java では、押した場所に丸を1つ描いています。

しかし、考え方を広げると、いろいろなGUI表現につながります。

応用例できること
クリックした場所に画像を表示する修行カードやキャラクター画像を出す
ドラッグした軌跡に線を描く簡単なお絵描きアプリを作る
クリック位置によって色を変える画面領域ごとに違う反応を作る
押すたびに複数の印を残す記録型の描画アプリにする
図形の大きさを変える押した場所や条件に応じて表現を変える
マウス位置を使って判定する当たり判定や選択処理につなげる

鬼滅の刃風にたとえると、今回は1つの気のしるしを出すだけでした。

でも、応用すれば、押した場所に修行札を出したり、マウスの動きに合わせて刀筋の軌跡を描いたり、複数の修行ポイントを地図の上に残したりできます。

今回の基本は、描画アプリやゲーム風の画面表現にもつながる大切な入口です。

コード全体の流れを整理する

Sample6.java の流れを、順番に整理してみます。

順番処理役割
1Sample6 が Frame を継承するウィンドウとして動けるようにする
2x と y を用意する丸を描く位置を保存する
3main で new Sample6 を実行するアプリを起動する
4コンストラクタが動く初期設定を行う
5addMouseListener を登録するフレームでマウス操作を受け取る
6setSize で大きさを決めるウィンドウサイズを設定する
7setVisible で表示する画面を表示する
8paint が呼ばれる説明文と最初の丸を描く
9マウスを押すmousePressed が呼ばれる
10getX、getY で座標を取得する押した場所を調べる
11x と y を更新する新しい描画位置を保存する
12repaint を呼ぶ再描画を依頼する
13paint が再び呼ばれる新しい位置に丸を描く

鬼滅の刃風にたとえると、最初に修行案内盤を設置し、初期位置に気のしるしを描きます。

その後、水月が案内盤を押すたびに、記録係が座標を保存し、支援隊士が案内盤へ再描画の合図を送り、新しい位置に気のしるしが描かれます。

はじめて学ぶときに押さえたい見方

マウス操作で描画するプログラムは、最初は少し流れが多く感じるかもしれません。

その場合は、次の4つの場所に分けて見ると分かりやすくなります。

見る場所確認すること
イベントを受け取る場所addMouseListener
位置を取得する場所mousePressed の getX、getY
位置を保存する場所x、y への代入
描画する場所paint の fillOval
画面更新を依頼する場所repaint

特に大切なのは、mousePressed の中で直接図形を描くのではなく、座標を保存して repaint を呼ぶという流れです。

図形を描く処理は paint にまとめます。

処理書く場所
押した位置を取得するmousePressed
描画位置を保存するmousePressed
再描画を依頼するmousePressed
実際に図形を描くpaint

この役割分担が見えると、GUI描画の流れがかなり理解しやすくなります。

この内容でつかんでおきたいこと

今回のテーマでいちばん大切なのは、マウスで押した位置を使って、その場所に描画できるということです。

そのためには、マウスイベント、座標取得、再描画、描画処理がつながる必要があります。

ポイント内容
Frame がソースになる画面全体でマウスイベントを受け取れる
MouseAdapter必要なマウスメソッドだけを書ける
mousePressedマウスを押したときに呼ばれる
MouseEventマウス操作の情報を持つ
getX押した場所の横座標を取得する
getY押した場所の縦座標を取得する
x、y描画する位置を保存する
repaint画面の再描画を依頼する
paint実際に画面を描く
Graphics描画のための道具
setColor描画色を指定する
drawString文字を描く
fillOval塗りつぶした楕円を描く

この仕組みが分かると、JavaのGUIは、ただ部品を置くだけの画面ではなく、ユーザーの動きに合わせて画面を描き変えられるものとして見えてきます。

鬼滅の刃風にたとえると、修行案内盤はもう静かな掲示板ではありません。

隊士が押した場所を覚え、そこに気のしるしを描き、次の操作に合わせて表示を変える、生きた修行装置になります。