カスタムコンポーネント

カスタムコンポーネント

これまでの講義 (グラフィカルユーザインターフェース, イベント処理) で扱った GUI コンポーネントには、主に次のようなものがあった。

その他、様々な GUI コンポーネントが Swing には用意されており、それらを貼り付けたり組み合わせたりすることによって、ほとんどのアプリケーションを作成することができる。

しかし、中には既存の GUI コンポーネントを組み合わせるだけでは作成できないコンポーネントも存在する。

これらのように特殊なコンポーネントを使用するには、自分で新しいコンポーネントを用意する必要がある。今回は、このようなコンポーネントを作成する方法を紹介する。

直線を描く

最初の例として、直線を表示する簡単なコンポーネントを作成する。


package j2.lesson12.example;

import java.awt.Graphics;

import javax.swing.JComponent;
import javax.swing.JFrame;

public class LinesSample extends JComponent {
    
    protected void paintComponent(Graphics g) {
        // ごみを全て消す
        g.clearRect(0, 0, getWidth(), getHeight());
        
        // (10, 100) から (200, 100) に線を引く
        g.drawLine(10, 100, 200, 100);
        
        // (100, 10) -> (100, 200) に線を引く
        g.drawLine(100, 10, 100, 200);
        
        // (30, 120) -> (180, 20) に線を引く
        g.drawLine(30, 120, 180, 20);
    }
    
    // このコンポーネントを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame("lines sample");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        // クライアントエリアに、このコンポーネントを貼り付ける
        frame.getContentPane().add(new LinesSample());
        frame.setSize(250, 250);
        frame.setVisible(true);
    }
}

全ての Swing のコンポーネントは、javax.swing.JComponent の子クラスである。今回は新しい Swing のコンポーネントを作成したいので、JComponent を継承したクラスを作成している。

まず、

protected void paintComponent(Graphics g) { ..

というメソッドは、このコンポーネントを描画する際に、ウィンドウから自動的に呼び出されるメソッドである。このメソッドを上書き (オーバーライド) することにより、コンポーネントに描画される内容を書き換えることができる。

「Graphics g」という引数は、コンポーネントの描画領域を表すキャンバスと、そこに様々なものを描画するためのペンのようなものをカプセル化したものである (java.awt.Graphics)。このオブジェクトに対して様々なメッセージを送る (インスタンスメソッドを呼び出す) ことにより、コンポーネントの見た目を設定することができる。

次の、

// ごみを全て消す
g.clearRect(0, 0, getWidth(), getHeight());

では、Graphics オブジェクトを使用してコンポーネント上に残っているごみを全て消している。getWidth(), getHeight() はそれぞれ JComponent が持っているメソッドで、このコンポーネントの画面上の幅と高さを取得できる。

clearRect メソッドは、(左上の点のx座標, 左上のy座標, 幅, 高さ) の順に引数を指定することにより、その範囲の長方形内を背景と同じ色で塗りつぶす (上の例ではコンポーネントに描かれているものを全てクリアする)。

プログラミングの世界で二次元座標を扱う場合は、数学で二次元座標を扱う場合と軸の取り方が少々異なる。


図のように、原点は領域の左上隅にあり、x 座標の値は左から右に、y 座標は上から下に、それぞれ大きくなる。

コンポーネントをクリアした後には、drawLine メソッドでコンポーネントに直線を描いている。

// (10, 100) から (200, 100) に線を引く
g.drawLine(10, 100, 200, 100);

// (100, 10) -> (100, 200) に線を引く
g.drawLine(100, 10, 100, 200);

// (30, 120) -> (180, 20) に線を引く
g.drawLine(30, 120, 180, 20);

drawLine メソッドは、(開始点のx座標、開始点のy座標、終了点のx座標、終了点のy座標) の順に引数を指定することにより、開始点と終了点を結ぶ直線を描くことができる。

最後に、main メソッドを用意してこのコンポーネントをウィンドウのクライアントエリアに貼り付けて表示する。

// このコンポーネントを表示してみる
public static void main(String[] args) {
    JFrame frame = new JFrame("lines sample");
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    // クライアントエリアに、このコンポーネントを貼り付ける
    frame.getContentPane().add(new LinesSample());
    frame.setSize(250, 250);
    frame.setVisible(true);
}

JComponent クラスを継承しているため、frame.getContentPane().add の引数インスタンスを指定することによって、通常のコンポーネントと同様にクライアントエリアに貼り付けることができる。

文字列を表示する

単に文字列を表示するコンポーネントならば javax.swing.JLabel で事足りるが、直線と文字列を同時に表示する場合などには自前で表示させる必要がある。


package j2.lesson12.example;

import java.awt.Graphics;

import javax.swing.JComponent;
import javax.swing.JFrame;

public class HelloComponent extends JComponent {
    
    protected void paintComponent(Graphics g) {
        g.clearRect(0, 0, getWidth(), getHeight());
        
        // x=50, y=30 に "Hello, world!" と表示させる
        g.drawString("Hello, world!", 50, 30);
    }
    
    // このコンポーネントを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame("HelloComponent");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.getContentPane().add(new HelloComponent());
        frame.setSize(200, 100);
        frame.setVisible(true);
    }
}

drawString メソッドは、(表示する文字列, 表示する位置のx座標, 表示する位置のx座標) の順に引数を指定することにより、指定した位置から文字列を描画することができる。

ただし、開始位置は文字列の左上の座標ではなく、ベースラインの左端の座標を指定する。


アセンダラインではなくベースラインを指定するため、指定した位置よりも少し上に文字が表示される。なお、g, j, p, q, y などの文字は、ベースラインをはみ出して描画される (ディセンダラインに沿う) ので、指定した y 座標より下にはみ出すことがあることに注意すること。

図形を描く

直線や文字列の他に、様々な図形を描くようなメソッドが用意されている。


package j2.lesson12.example;

import java.awt.Graphics;

import javax.swing.JComponent;
import javax.swing.JFrame;

public class ShapesSample extends JComponent {
    
    protected void paintComponent(Graphics g) {
        // ごみを全て消す
        g.clearRect(0, 0, getWidth(), getHeight());
        
        // (20, 20) を左上とした 幅120, 高さ100 の長方形
        g.drawRect(20, 30, 120, 100);
        
        // (100, 100) を左上とした 幅120, 高さ70 の楕円
        g.drawOval(100, 100, 120, 70);
    }
    
    // このコンポーネントを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame("shapes sample");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.getContentPane().add(new ShapesSample());
        frame.setSize(250, 250);
        frame.setVisible(true);
    }
}

drawRect は draw rectangle (長方形を描け) の略で、その名前の通りに長方形を描くことができる。(左上の点のx座標, 左上のy座標, 幅, 高さ) の順に引数を指定することにより、その範囲の長方形の輪郭線をコンポーネントに描画する。

drawOval は draw oval (楕円を描け) という意味を持つメソッドである。(左上の点のx座標, 左上のy座標, 幅, 高さ) の順に引数を指定することにより、その範囲の長方形に内接する楕円をコンポーネントに描画する。

他にも様々な図形を描くメソッドが用意されているので、java.awt.Graphics を参照のこと。

図形を塗りつぶす

先ほどの draw~ は輪郭線を描くメソッドであったが、図形を塗りつぶすメソッドも用意されている。


package j2.lesson12.example;

import java.awt.Graphics;

import javax.swing.JComponent;
import javax.swing.JFrame;

public class FillSample extends JComponent {
    
    protected void paintComponent(Graphics g) {
        // ごみを全て消す
        g.clearRect(0, 0, getWidth(), getHeight());
        
        // (20, 20) を左上とした 幅120, 高さ100 の長方形
        g.fillRect(20, 30, 120, 100);
        
        // (100, 100) を左上とした 幅120, 高さ70 の楕円
        g.fillOval(100, 100, 120, 70);
    }
    
    // このコンポーネントを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame("shapes sample");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.getContentPane().add(new FillSample());
        frame.setSize(250, 250);
        frame.setVisible(true);
    }
}

fillRect, fillOval はそれぞれ drawRect, drawOval の輪郭線内部を塗りつぶすメソッドである。

描画時の色を変更する

描画時の色は、黒以外にも様々な色を使用することができる。


package j2.lesson12.example;

import java.awt.Color;
import java.awt.Graphics;

import javax.swing.JComponent;
import javax.swing.JFrame;

public class ColorSample extends JComponent {
    
    protected void paintComponent(Graphics g) {
        // ごみを全て消す
        g.clearRect(0, 0, getWidth(), getHeight());
        
        // 緑色の文字列
        g.setColor(Color.GREEN);
        g.drawString("Hello", 160, 50);
        
        // 赤色の長方形
        g.setColor(Color.RED);
        g.fillRect(20, 30, 120, 100);
        
        // 青色の円
        g.setColor(Color.BLUE);
        g.fillOval(100, 100, 100, 100);
        
        // (R,G,B) = (0x99, 0x00, 0xcc) - 紫色の直線
        g.setColor(new Color(0x99, 0x00, 0xcc));
        g.drawLine(0, 0, 250, 250);
    }
    
    // このコンポーネントを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame("color sample");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.getContentPane().add(new ColorSample());
        frame.setSize(250, 250);
        frame.setVisible(true);
    }
}

g.setColor の引数に java.awt.Color のインスタンスを渡すことによって、それ以降の描画の色を変更することができる。

java.awt.Color には基本的な色を表す Color クラスのインスタンスが、クラスフィールドとして宣言されている。

上記のクラスフィールドを使用することによって、基本的な色はわざわざ用意することなく使用することができる。

また、Color のコンストラクタに様々な値を渡すことによって、新しい色を作ることができる。上記以外の色を使用する場合には「new Color(r, g, b)」のように、光の三原色 - red, blue, green - のそれぞれの強さを 0 ~ 255 で指定してやれば様々な色を作成することができる。

// (R,G,B) = (0x99, 0x00, 0xcc) - 紫色の直線
g.setColor(new Color(0x99, 0x00, 0xcc));
g.drawLine(0, 0, 250, 250);

このように色を表す方法を、RGB カラーモデルと呼ぶ。生成される色についての議論はここでは行わないため、興味があれば調べてみよ。

画像を貼り付ける

もともと用意された画像を、コンポーネント内に描画することもできる。


package j2.lesson12.example;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;

import javax.swing.JComponent;
import javax.swing.JFrame;

public class ImageSample extends JComponent {
    
    protected void paintComponent(Graphics g) {
        // ごみを全て消す
        g.clearRect(0, 0, getWidth(), getHeight());
        
        // イメージを読み込むために、Toolkit クラスを使う
        Toolkit tk = Toolkit.getDefaultToolkit();
        
        // 画像ファイルを取得する。
        Image image = tk.getImage("U:/duke.gif");
        
        // (50, 50) を左上として画像を貼り付ける
        g.drawImage(image, 50, 50, this);
    }
    
    // このコンポーネントを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame("image sample");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.getContentPane().add(new ImageSample());
        frame.setSize(200, 200);
        frame.setVisible(true);
    }
}

画像を扱うには、java.awt.Toolkit という、GUI 処理で使える様々な道具が用意されたクラスを使うと便利である。このクラスは抽象クラスであり、そのままではインスタンス化することができない。getDefaultToolkit() メソッドを呼び出すことにより、そのOSに適した子クラスのインスタンス を取得することができる。

// イメージを読み込むために、Toolkit クラスを使う
Toolkit tk = Toolkit.getDefaultToolkit();

そして、Toolkit クラスの getImage(String filename) メソッドを呼び出すと、指定したファイル名を持つ画像が java.awt.Image 型として返される。

// 画像ファイルを取得する。
Image image = tk.getImage("U:/duke.gif");

この Image 型のオブジェクトは、Graphics のメソッド drawImage でコンポーネント上に描画することができる。

// (50, 50) を左上として画像を貼り付ける
g.drawImage(image, 50, 50, this);

drawImage には (描画する画像, 左上のx座標, 左上のy座標, this) と引数を指定する。最後の this を指定すると、画像が読み込み中 (Toolkit を使うと読み込みつつ他のプログラムを実行する) の場合でその時点では描画できなかった場合に、読み込み終わり次第その場所を再度描画してくれるため、確実に画像を表示することができて便利である。

上記のプログラムは、paintComponent メソッドが呼び出されるたびに画像を読み込むため、あまり効率がよくない。paintComponent メソッドは、様々な場面で呼び出される。

このような場合に何度も画像を読み込まないために、次のようにプログラムを書くこともできる。

package j2.lesson12.example;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;

import javax.swing.JComponent;
import javax.swing.JFrame;

public class ImageSample2 extends JComponent {
    
    private final Image image;
    
    public ImageSample2() {
        // コンストラクタで早めに読み出しておく
        Toolkit tk = Toolkit.getDefaultToolkit();
        this.image = tk.getImage("U:/duke.gif");
    }
    
    protected void paintComponent(Graphics g) {
        // ごみを全て消す
        g.clearRect(0, 0, getWidth(), getHeight());
        
        // コンストラクタで読み出した画像を貼り付ける
        g.drawImage(this.image, 50, 50, this);
    }
    
    // このパネルを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame("image sample 2");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.getContentPane().add(new ImageSample2());
        frame.setSize(200, 200);
        frame.setVisible(true);
    }
}

上記ならば、コンストラクタで一度だけ読み込むだけである。

推奨サイズを指定する

これまでのプログラムでは

frame.getContentPane().add(new HelloComponent());

のように、クライアントエリア全体を覆うようなコンポーネントの配置を行っていた。

そのため、このコンポーネントが持つサイズというのを意識しなくても問題は発生しなかったが、BorderLayout などを使用して BorderLayout.CENTER 以外の場所に配置した場合、「コンポーネントの推奨サイズ」という情報が使用され、自動的にレイアウトが決定される。

これまでに作成したコンポーネントを、BorderLayout.SOUTH などに配置すると次のように表示されてしまう (BorderLayout.CENTER にはボタンを一つ配置している)。


コンポーネントの推奨サイズを指定していないため、コンポーネントが表示されていない。このサイズを指定するには、次のようにプログラムを書き換えればよい。

package j2.lesson12.example;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;

import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;

public class HelloComponent extends JComponent {
    
    protected void paintComponent(Graphics g) {
        g.clearRect(0, 0, getWidth(), getHeight());
        
        // x=50, y=30 に "Hello, world!" と表示させる
        g.drawString("Hello, world!", 50, 30);
    }
    
    // 推奨サイズを返すメソッドを上書き
    public Dimension getPreferredSize() {
        // new java.awt.Dimension(width, height)
        return new Dimension(200, 60);
    }
    
    // このコンポーネントを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame("HelloComponent");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.getContentPane().add(new JButton(), BorderLayout.CENTER);
        frame.getContentPane().add(new HelloComponent(), BorderLayout.SOUTH);
        frame.setSize(200, 100);
        frame.setVisible(true);
    }
}

コンポーネントには、その推奨サイズを返すメソッドとして getPreferredSize() というメソッドがある。これを上書き (オーバーライド) して、自分の好きなサイズを返すことにより、コンポーネントの推奨サイズを変更することができる。

上記を実行すると、次のようにコンポーネントが表示される。


なお、戻り値に使用している java.awt.Dimension は、幅と高さを表すためのクラスである。

また、この推奨サイズは javax.swing.JScrollPane でも使用する。ここでサイズを指定しないと、いくら大きな画像を描画してもスクロールすることができなくなってしまう。

低レベルイベント

これまでに使用してきたイベントは java.awt.event.ActionEvent のみであった。

ActionEvent は、「ボタンがマウスによってクリックされる」「メニューがマウスによってクリックされる」「テキストフィールドでキーボードのEnterを押す」などの条件で発生する。これらは全て、コンポーネントがキーボードやマウスの状態を感知して、ActionEventを生成している

つまり、コンポーネントはキーボードやマウスの状態を感知することができる。ここでは、そのような低レベルなイベントを処理する方法を紹介する。

キーボード

キーボードのイベントを処理する場合にも、これまでと同様にイベント委譲モデルを使用する。そのため、~Event, ~Listener, add~Listener という考え方は ActionEvent を扱う際と同様である。

キーボードのイベントを受信するリスナは、java.awt.event.KeyListener であり、下記のようなイベントハンドラを持つ。

イベントを表すオブジェクトは java.awt.event.KeyEvent である。

例として、キーボードを操作した際にコンソールへ情報を表示するリスナを作成する。

package j2.lesson12.example;

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class StdoutKeyListener implements KeyListener {

    // キーがタイプされた際に呼び出される
    public void keyTyped(KeyEvent e) {
        System.out.println(e.getKeyChar() + "がタイプされました");
    }

    // キーが押された際に呼び出される
    public void keyPressed(KeyEvent e) {
        String key = KeyEvent.getKeyText(e.getKeyCode());
        System.out.println(key + "が押されました");
    }

    // キーが離された際に呼び出される
    public void keyReleased(KeyEvent e) {
        String key = KeyEvent.getKeyText(e.getKeyCode());
        System.out.println(key + "が離されました");
    }
}

keyType メソッド内で使用している KeyEvent の getKeyChar() は、実際にタイプされた文字を取得できる。これは char 型で返されるため、そのまま表示して問題ない。

また、keyPressed, keyReleased メソッド内で使用している getKeyCode() は、押されたキーを表す数値を取得でき、KeyEvent.getKeyText メソッドで、その数値を文字列に変換できる。

これをコンポーネントに登録するには、コンポーネントの addKeyListener メソッドを呼び出せばよい。

package j2.lesson12.example;

import javax.swing.JFrame;

public class KeyEventSample {
    
    public static void main(String[] args) {
        JFrame frame = new JFrame("key event sample");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        // JComponent を継承するクラスを一つインスタンス化する
        HelloComponent c = new HelloComponent();
        
        // コンポーネントがフォーカスを受け取れるように設定する
        c.setFocusable(true);
        
        // コンポーネントに KeyListener を設定する
        c.addKeyListener(new StdoutKeyListener());
        
        frame.getContentPane().add(c);
        frame.setSize(200, 200);
        frame.setVisible(true);
    }
}

重要なのが、setFocusable(boolean) メソッドで、このメソッドの引数に true を指定することにより、コンポーネントをキーボードの操作対象に指定することができる。その後、addKeyListener メソッドに先ほど作成したリスナを渡せば、「キーボードを操作した際にコンソールに情報を表示するコンポーネント」を作成できる。

このプログラムを実行し、日本語で「こんにちは」と入力すると、次のように表示される。

半角が離されました
Kが離されました
Oが離されました
Nが離されました
Nが離されました
Nが離されました
Iが離されました
Tが離されました
Iが離されました
Hが離されました
Aが離されました
こがタイプされました
んがタイプされました
にがタイプされました
ちがタイプされました
はがタイプされました
Enterが離されました

日本語入力環境では、日本語変換システムがキーボードの処理の一部を取り上げて自分で処理してしまうため、keyPressed メソッドが呼び出されない。また、変換後にEnterボタンを押すと、一気に入力した文字がタイプされる。

キーイベントの例

他の例として、「タイプされた文字列をコンポーネントに描画する」というプログラムを紹介する。

まずは、リスナを作成する。

package j2.lesson12.example;

import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

public class TypedListener extends KeyAdapter {
    
    private final KeyTypedViewer viewer;
    
    public TypedListener(KeyTypedViewer viewer) {
        this.viewer = viewer;
    }
    
    public void keyTyped(KeyEvent e) {
        this.viewer.append(e.getKeyChar());
    }
}

リスナのうち一部のハンドラだけを使用したい場合は、~Listener を実装するのではなく、~Adapter を継承するのがよい。~Adapter は ~Listener を実装し、それぞれのイベントハンドラで「何も行わない」という実装をしている。つまり、この ~Adapter を継承し、必要なメソッドだけオーバーライドすれば、オーバーライドしたハンドラに好きな処理を書いてそれ以外のハンドラは何もしないハンドラとして定義できる。

上記クラスは、KeyAdapter クラスを継承しているため、KeyListener として使用することができる。

次に、このリスナを使用したコンポーネントを作成する。

package j2.lesson12.example;

import java.awt.Graphics;

import javax.swing.JComponent;
import javax.swing.JFrame;

public class KeyTypedViewer extends JComponent {
    
    private String draw;
    
    public KeyTypedViewer() {
        this.draw = "";

        // コンストラクタでリスナを登録する
        this.setFocusable(true);
        this.addKeyListener(new TypedListener(this));
    }
    
    protected void paintComponent(Graphics g) {
        g.clearRect(0, 0, getWidth(), getHeight());
        // タイプされた文字列を表示する
        g.drawString(this.draw, 50, 30);
    }

    // タイプされた文字列を保存する
    public void append(char keyChar) {
        this.draw = this.draw + keyChar;
        
        // 保存が終わったら再描画してもらう
        repaint();
    }
    
    // このコンポーネントを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame("key typed viewer");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.getContentPane().add(new KeyTypedViewer());
        frame.setSize(200, 100);
        frame.setVisible(true);
    }
}

コンストラクタでは、自分自身の setFocusable メソッドと addKeyListener メソッドを呼び出し、キーボードの操作を可能にしている。

append はリスナから呼び出されるメソッドで、タイプされた文字を保存した後に repaint() メソッドによってコンポーネントを再描画している。この repaint メソッドを呼び出すと、自動的に Graphics オブジェクトを作成して、paintComponent メソッドを呼び出してくれる。

このプログラムを実行し、ウィンドウ表示後に「Java プログラミング」と入力すると、下記のような画面が表示される。


マウス

キーボードと同様に、マウスの情報も取得することができる。

マウスのイベントを表すクラスは java.awt.event.MouseEvent の一種類であるが、このイベントを受信するリスナは2種類ある。

マウスボタン

マウスボタンのイベントを受信するリスナは、java.awt.event.MouseListener であり、下記のようなイベントハンドラを持つ。

イベントを表すオブジェクトは java.awt.event.MouseEvent である。

例として、マウスを操作した際にコンソールへ情報を表示するリスナを作成する。

package j2.lesson12.example;

import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

public class StdoutMouseListener implements MouseListener {

    public void mouseClicked(MouseEvent e) {
        Point point = e.getPoint();
        System.out.println("マウスがクリックされました: " + point.x + ", " + point.y);
    }

    public void mousePressed(MouseEvent e) {
        System.out.println("マウスボタンが押されました");
    }

    public void mouseReleased(MouseEvent e) {
        System.out.println("マウスボタンが離されました");
    }

    public void mouseEntered(MouseEvent e) {
        System.out.println("カーソルが領域内に入りました");
    }

    public void mouseExited(MouseEvent e) {
        System.out.println("カーソルが領域内から抜けました");
    }
}

mouseClicked メソッド内で使用されている getPoint メソッドは、マウスの現在位置を返すメソッドである。戻り値の型は java.awt.Point 型で、インスタンスフィールドの x, y を参照することによって位置を特定できる。

これをコンポーネントに登録するには、コンポーネントの addMouseListener メソッドを呼び出せばよい。

package j2.lesson12.example;

import javax.swing.JFrame;

public class MouseEventSample {
    
    public static void main(String[] args) {
        JFrame frame = new JFrame("mouse event sample");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        // JComponent を継承するクラスを一つインスタンス化する
        HelloComponent c = new HelloComponent();
        
        // コンポーネントに MouseListener を設定する
        c.addMouseListener(new StdoutMouseListener());
        
        frame.getContentPane().add(c);
        frame.setSize(200, 200);
        frame.setVisible(true);
    }
}

キーボードと違い、setFocusable メソッドを呼び出す必要はない。

マウスカーソルの動き

マウスカーソルのイベントを受信するリスナは、java.awt.event.MouseMotionListener であり、下記のようなイベントハンドラを持つ。

イベントを表すオブジェクトは、こちらも java.awt.event.MouseEvent である。

例として、マウスカーソルを操作した際にコンソールへ情報を表示するリスナを作成する。

package j2.lesson12.example;

import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;

public class StdoutMouseMotionListener implements MouseMotionListener {

    public void mouseDragged(MouseEvent e) {
        Point p = e.getPoint();
        System.out.println("(" + p.x + ", " + p.y + ") をドラッグ中");
    }

    public void mouseMoved(MouseEvent e) {
        Point p = e.getPoint();
        System.out.println("(" + p.x + ", " + p.y + ") を移動中");
    }
}

これをコンポーネントに登録するには、コンポーネントの addMouseMotionListener メソッドを呼び出せばよい。

package j2.lesson12.example;

import javax.swing.JFrame;

public class MouseMotionEventSample {
    
    public static void main(String[] args) {
        JFrame frame = new JFrame("mouse motion event sample");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        // JComponent を継承するクラスを一つインスタンス化する
        HelloComponent c = new HelloComponent();
        
        // コンポーネントに MouseListener を設定する
        c.addMouseMotionListener(new StdoutMouseMotionListener());
        
        frame.getContentPane().add(c);
        frame.setSize(200, 200);
        frame.setVisible(true);
    }
}

マウスイベントの例

例として、マウスのクリックとドラッグのイベントを受信し、その情報をコンポーネント内に描画するプログラムを作成する。

まずは、リスナを作成する。

package j2.lesson12.example;

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

// MouseListener, MouseMotionListener の両方を実装する
public class MouseSensor implements MouseListener, MouseMotionListener {

    private final MouseViewer viewer;
    
    public MouseSensor(MouseViewer viewer) {
        this.viewer = viewer;
    }

    public void mouseClicked(MouseEvent e) {
        // MouseViewer の clicked(Point) にクリックされた位置を渡す
        this.viewer.clicked(e.getPoint());
    }

    public void mouseDragged(MouseEvent e) {
        // MouseViewer の dragged(Point) にドラッグされた位置を渡す
        this.viewer.dragged(e.getPoint());
    }

    // clicked, dragged 以外のイベントは無視する
    public void mousePressed(MouseEvent e) {}
    public void mouseReleased(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}
    public void mouseMoved(MouseEvent e) {}
}

クリックされた際のイベントは MouseListener, ドラッグされた際のイベントは MouseMotionListener が受信するため、両方のインターフェースを実装したクラスを用意している。

次に、このリスナを使用したコンポーネントを作成する。

package j2.lesson12.example;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;

import javax.swing.JComponent;
import javax.swing.JFrame;

public class MouseViewer extends JComponent {
    
    private Point lastDragged;
    private Point lastClicked;
    
    public MouseViewer() {
        // 情報を初期化
        this.lastClicked = null;
        this.lastDragged = null;
        
        // リスナを登録
        MouseSensor sensor = new MouseSensor(this);
        this.addMouseListener(sensor);
        this.addMouseMotionListener(sensor);
    }
    
    protected void paintComponent(Graphics g) {
        g.clearRect(0, 0, getWidth(), getHeight());

        // 前回の最後にクリックされた場合
        if (this.lastClicked != null) {
            Point p = this.lastClicked;
            // 赤い円を描く
            g.setColor(Color.RED);
            g.fillOval(p.x - 6, p.y - 6, 12, 12);
        }
        
        if (this.lastDragged != null) {
            Point p = this.lastDragged;
            // 青い小さな円を描く
            g.setColor(Color.BLUE);
            g.fillOval(p.x - 4, p.y - 4, 8, 8);
        }
    }
    
    // クリックされた位置を保存する
    public void clicked(Point p) {
        this.lastClicked = p;
        
        // 保存が終わったら再描画してもらう
        repaint();
    }

    // ドラッグされた位置を保存する
    public void dragged(Point p) {
        this.lastDragged = p;
        
        // 保存が終わったら再描画してもらう
        repaint();
    }

    // このコンポーネントを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame("mouse viewer");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.getContentPane().add(new MouseViewer());
        frame.setSize(200, 200);
        frame.setVisible(true);
    }
}

最後にクリック/ドラッグされた位置を記憶し、それぞれの位置を中心とした円を表示するだけのプログラムである。