swingと遊ぼう

この記事は[42Tokyo Advent Calendar 2024](https://qiita.com/advent-calendar/2024/42tokyo) 、2日目の記事です。
paphio (kitsuki)が担当します。アナログ・デジタル問わずゲーム制作が大好きです。
Javaのswingライブラリを用いて何度もゲーム開発を行ってきた者が、独学の知識を披露します。
記事を書くことが初めてな上、間違った理解をしている可能性もあるので、「ふ～ん」って感じであまり真に受けずに読んで下さい。

## この記事を読み終わったら
シンプルなストップウォッチを作れるようになる、はずです。

## swingとは？
Javaの標準ライブラリの一つで、Javaを用いてGUIアプリ開発を行う際によく使われます。
ほかにも、awtやJavaFXなどがありますが、awtはswingより古く、JavaFXは標準ライブラリから外されてオープンソース化したため、標準狂信者の僕からしたらswingがおすすめです。
~~(とは言いつつ最近はJavaFXを勉強中)~~

## まず土台作り
今までJavaでCUIアプリを開発してきた皆さん。GUIアプリの仕組みはだいぶ変わります。
そもそもJavaを扱ったこともない皆さん。この記事は難しいかもしれません。
以下、とりあえず土台のサンプルコードを載せます。

``` java
import javax.swing.JFrame;

class Main extends JFrame {
    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        this.setVisible(true);
    }
}
```
### 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949729/47a7f0c6-2e26-20e5-8125-327a562e8a6c.png)

何もないウィンドウが左上に表示されると思います。
これで、GUIアプリができました！終わり！
...冗談です。解説します。
まず前提として、`public static ~`や、`new`、`extends`など、swingとは関係ない部分は説明しません。
こういったものを「おまじない」として覚えている皆さん。この記事は難しいかもしれません。
#### JFrame
これはウィンドウの部分を扱うクラスです。
GUIアプリ開発を行う際は、このクラスが欠かせません。
`setVisible()`メソッドを用いて、ウィンドウを表示したり消したりします。
<br>
察しの良い方はお気づきかもしれませんが、わざわざ継承してMainクラスのインスタンス生成を行わなくとも、mainメソッドの中でJFrameクラスのインスタンス生成を行えば、もっと簡単に同じことができます。
ですが、僕はこちらの書き方のほうが好きです。
理由は...しっかりと考えたことはありませんが、管理しやすいからでしょうか。

## テキスト表示！
このままだと、何もなくてつまらなさ過ぎますね。
ということで、今度はウィンドウ上に文章を表示します。
以下、サンプルコードです。
``` java
import javax.swing.JFrame;
// 追加部分
import javax.swing.JPanel;
import javax.swing.JLabel;

class Main extends JFrame {
    // 追加部分
    private JPanel panel;
    private JLabel label;

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

    public Main() {
        // 追加部分
        panel = new JPanel();
        label = new JLabel("Hello, world!");
        panel.add(label);
        this.add(panel);
        this.setSize(200, 200);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        this.setVisible(true);
    }
}
```
### 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949729/de358d57-cd12-448c-9d6f-f18807e21052.png)
ついに文字が映りましたね。では解説します。
#### JPanel
swingの世界ではJPanel, JLabelなど、`javax.swing.JComponent`を継承したクラスを「コンポーネント」と呼ぶのですが、JPanelはコンポーネントをまとめるコンテナの役割を持ったクラスです。通常、JFrameに直接追加せず、JPanelをいったん挟みます。
JFrame, JPanelなど、`java.awt.Container`を継承したクラスは、`add()`メソッドを用いてコンポーネントを自身に追加できます。追加されたコンポーネントは、指定されたレイアウトによって配置されます。
レイアウトについては、いくつか種類があるのですが、あまり詳しくないので説明を割愛します。
気になる方はググってみてください。(BoxLayout, FlowLayout, ...etc)
#### JLabel
基本的に文章を表示するためのクラスです。
`new JLabel("Hello, world!")`のように、コンストラクタの引数に文字列を渡すことで、その文字列を表示させることができます。また、`setText()`メソッドを用いることで、あとで表示する文字列を変更することができます。
#### JFrameに関する追加情報
`this.setSize(200, 200);`
これは自信が描画される際のサイズを指定するメソッドです。
第一引数が幅、第二引数が高さを表します。
このメソッドはほかにもJPanelやJLabelなど、`java.awt.Component`を継承したクラスが使えます。
`this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);`
これはウィンドウの×ボタンをクリックしたとき、自動的にプログラムを終了させるよう設定するメソッドです。
これまでのサンプルコードを動かした人は、ウィンドウの×ボタンを押してもプログラムが終了せず、イライラしませんでしたか？少なくとも僕はそうでしたw
これからは、この文を追記することで、そのストレスから解放されます！

## ボタンを追加！
ここまでやってみて、どうですか？
「ただ表示されるだけで、ユーザー側がなにも干渉できなくて、つまらん！」
という声が聞こえてきますね。
そんな皆さんに朗報です。
ここでは、ユーザー入力を受け取る方法で、一番簡単なものである、ボタンを追加します。
以下、サンプルコードです。
``` java
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JLabel;
// 追加部分
import javax.swing.JButton;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

// 追加部分
class Main extends JFrame implements ActionListener {
    private JPanel panel;
    private JLabel label;
    // 追加部分
    private JButton button;

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

    public Main() {
        panel = new JPanel();
        label = new JLabel("Hello, world!");
        // 追加部分
        button = new JButton("click");
        panel.setLayout(null);
        label.setHorizontalAlignment(JLabel.CENTER);
        label.setSize(200, 150);
        button.setLocation(50, 200);
        button.setSize(100, 50);
        button.addActionListener(this);
        panel.add(button);

        panel.add(label);
        this.add(panel);
        this.setSize(200, 300);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }

    public void actionPerformed(ActionEvent e) {
        label.setText("The button was clicked.");
    }
}
```
### 結果
| ボタン押す前 | ボタン押した後 |
|:-:|:-:|
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949729/20477767-68e0-953a-2ef6-64697ba1e95d.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949729/cdbb125c-4713-1b74-ba03-961501438490.png) |

無事ユーザーが干渉出来るようになりましたね。
(ゲーム制作への第一歩です！)
では、解説します。
#### JButton
ユーザーからクリック入力を受け取る際に便利なクラスです。
クリックされた時、
`addActionListener()`メソッドによって渡されたインスタンスたちに対して、
`actionPerformed()`メソッドを呼び出します。
この時に渡される`ActionEvent`には、クリックされた対象や、何クリックか(左クリック、右クリック、etc)などの情報が詰まっています。本来はこの情報を用いて分岐させるのですが、今回は分ける必要が無いため無条件で`label`のテキストを変更します。
この部分、難しいですね。当初理解するのに苦労した記憶があります。
ちなみに、`addActionListener()`の引数には`ActionListener`を実装したクラスが渡せるのですが、僕はよく上記のように`Main`クラスに実装させて`this`を渡すという手法を取っています。その方が一連の流れを一つのクラスにまとめられて見やすいし、privateメンバ変数も使えて便利なので。

#### JPanelに関する追加情報
`panel.setLayout(null);`
こうすることで、そのインスタンスのレイアウト方法を「無し」にできます。
無しにすると、コンポーネントをただ追加しただけでは自動配置してくれなくなりますが、`setLocation()`(そのコンポーネントの左上の角の座標を指定するメソッド)や`setSize()`を用いることで、コンポーネントを自分の思うがままに自由に配置できます。
ゲーム制作においては自由に配置したいため、僕はだいたいいつもレイアウトを無しにしています。
(先程、レイアウトに詳しくないと述べましたが、これが主な原因です。)

#### JLabelに関する追加情報
`label.setHorizontalAlignment(JLabel.CENTER);`
こうすることで、文章の位置を中央にできます。
デフォルトでは左詰め(厳密には少し違う)になっています。

## そして...完成
最後に、ストップウォッチっぽくするためにいろいろ加えます。
以下がサンプルコードです。
``` java
import java.awt.Font;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.math.BigDecimal;
import java.math.RoundingMode;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

class Main extends JFrame implements ActionListener {
	private JPanel panel;
	private JLabel label;
	private JButton measure;
	private JButton reset;
	private long count = 0;
	private boolean isMeasuring = false;

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

	public Main() {
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setTitle("ストップウォッチ");
		this.setResizable(false);

		panel = new JPanel();
		panel.setLayout(null);

		label = new JLabel();
		label.setBounds(0, 0, 200, 150);
		label.setHorizontalAlignment(JLabel.RIGHT);
		label.setFont(new Font(null, Font.PLAIN, 30));

		measure = new JButton("start");
		measure.setBounds(25, 175, 70, 50);
		measure.addActionListener(this);

		reset = new JButton("reset");
		reset.setBounds(250 - 25 - 70, 175, 70, 50);
		reset.addActionListener(this);

		panel.add(label);
		panel.add(measure);
		panel.add(reset);

		this.setVisible(true);

		Insets in = getInsets();
		this.setSize(in.left + in.right + 250, in.top + in.bottom + 250);

		updateTime();
		this.add(panel);
		repaint();

		new Thread(new Runnable() {
			private long startTime = System.currentTimeMillis();
			private long beforeCount = count;

			public void run() {
				while (true) {
					if (!isMeasuring) {
						startTime = System.currentTimeMillis();
						beforeCount = count;
					} else {
						count = beforeCount + (System.currentTimeMillis() - startTime);
						updateTime();
					}
					try {
						Thread.sleep(30);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
	}

	public void updateTime() {
		label.setText(new BigDecimal((double) count / 1000).setScale(1, RoundingMode.DOWN).toString() + " s");
		this.repaint();
	}

	public void start() {
		isMeasuring = true;
		measure.setText("stop");
		this.repaint();
	}

	public void stop() {
		isMeasuring = false;
		measure.setText("start");
		this.repaint();
	}

	public void reset() {
		stop();
		count = 0;
		updateTime();
	}

	public void actionPerformed(ActionEvent e) {
		if (e.getSource().equals(measure)) {
			if (!isMeasuring)
				start();
			else
				stop();
		} else if (e.getSource().equals(reset))
			reset();
	}
}
```
#### 結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949729/fc60997d-650b-87fa-a178-59ee889ee33b.png)

ふう、だいぶ長くなってしまいましたね。
swingに関する追加情報はあまりない~~のと説明するのがすごく面倒な~~ので、解説は省略します。
このプログラムには、マジックナンバーやデータレースなど、問題が多々ありますが、そこら辺は皆さんで改善案を考えてみてください。

## 最後に
だいぶ説明が雑になってしまいましたが、ここまで来ればある程度swingを扱えるようになったはずです。
さらに他にも、`JTextArea`や`JScrollPane`、`paintComponents()`、`Graphics`など、便利アプリ制作やゲーム制作に使える機能がたくさんあります。
<br>
以上、Javaはサーバーサイドや(入門とかで)CUIアプリ開発のイメージがありますが、GUIアプリ開発もできるよ、というお話でした。
42Tokyoとは関係なく、趣味全開の記事でしたが、なんと42Tokyoの課題にはswingに関するものもあるみたいです。課題の手助けとなることを信じます。
(42Tokyoの皆さん、その課題は僕が一番最初にクリアしたいので、それまでもうしばらく辛抱してほしいです！)
### Javaって、最高！ ！ ！ ！