この記事は42Tokyo Advent Calendar 2024 、2日目の記事です。
paphio (kitsuki)が担当します。アナログ・デジタル問わずゲーム制作が大好きです。
Javaのswingライブラリを用いて何度もゲーム開発を行ってきた者が、独学の知識を披露します。
記事を書くことが初めてな上、間違った理解をしている可能性もあるので、「ふ~ん」って感じであまり真に受けずに読んで下さい。
この記事を読み終わったら
シンプルなストップウォッチを作れるようになる、はずです。
swingとは?
Javaの標準ライブラリの一つで、Javaを用いてGUIアプリ開発を行う際によく使われます。
ほかにも、awtやJavaFXなどがありますが、awtはswingより古く、JavaFXは標準ライブラリから外されてオープンソース化したため、標準狂信者の僕からしたらswingがおすすめです。
(とは言いつつ最近はJavaFXを勉強中)
まず土台作り
今までJavaでCUIアプリを開発してきた皆さん。GUIアプリの仕組みはだいぶ変わります。
そもそもJavaを扱ったこともない皆さん。この記事は難しいかもしれません。
以下、とりあえず土台のサンプルコードを載せます。
import javax.swing.JFrame;
class Main extends JFrame {
public static void main(String[] args) {
new Main();
}
public Main() {
this.setVisible(true);
}
}
結果
何もないウィンドウが左上に表示されると思います。
これで、GUIアプリができました!終わり!
...冗談です。解説します。
まず前提として、public static ~
や、new
、extends
など、swingとは関係ない部分は説明しません。
こういったものを「おまじない」として覚えている皆さん。この記事は難しいかもしれません。
JFrame
これはウィンドウの部分を扱うクラスです。
GUIアプリ開発を行う際は、このクラスが欠かせません。
setVisible()
メソッドを用いて、ウィンドウを表示したり消したりします。
察しの良い方はお気づきかもしれませんが、わざわざ継承してMainクラスのインスタンス生成を行わなくとも、mainメソッドの中でJFrameクラスのインスタンス生成を行えば、もっと簡単に同じことができます。
ですが、僕はこちらの書き方のほうが好きです。
理由は...しっかりと考えたことはありませんが、管理しやすいからでしょうか。
テキスト表示!
このままだと、何もなくてつまらなさ過ぎますね。
ということで、今度はウィンドウ上に文章を表示します。
以下、サンプルコードです。
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);
}
}
結果
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
これからは、この文を追記することで、そのストレスから解放されます!
ボタンを追加!
ここまでやってみて、どうですか?
「ただ表示されるだけで、ユーザー側がなにも干渉できなくて、つまらん!」
という声が聞こえてきますね。
そんな皆さんに朗報です。
ここでは、ユーザー入力を受け取る方法で、一番簡単なものである、ボタンを追加します。
以下、サンプルコードです。
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.");
}
}
結果
ボタン押す前 | ボタン押した後 |
---|---|
![]() |
![]() |
無事ユーザーが干渉出来るようになりましたね。
(ゲーム制作への第一歩です!)
では、解説します。
JButton
ユーザーからクリック入力を受け取る際に便利なクラスです。
クリックされた時、
addActionListener()
メソッドによって渡されたインスタンスたちに対して、
actionPerformed()
メソッドを呼び出します。
この時に渡されるActionEvent
には、クリックされた対象や、何クリックか(左クリック、右クリック、etc)などの情報が詰まっています。本来はこの情報を用いて分岐させるのですが、今回は分ける必要が無いため無条件でlabel
のテキストを変更します。
この部分、難しいですね。当初理解するのに苦労した記憶があります。
ちなみに、addActionListener()
の引数にはActionListener
を実装したクラスが渡せるのですが、僕はよく上記のようにMain
クラスに実装させてthis
を渡すという手法を取っています。その方が一連の流れを一つのクラスにまとめられて見やすいし、privateメンバ変数も使えて便利なので。
JPanelに関する追加情報
panel.setLayout(null);
こうすることで、そのインスタンスのレイアウト方法を「無し」にできます。
無しにすると、コンポーネントをただ追加しただけでは自動配置してくれなくなりますが、setLocation()
(そのコンポーネントの左上の角の座標を指定するメソッド)やsetSize()
を用いることで、コンポーネントを自分の思うがままに自由に配置できます。
ゲーム制作においては自由に配置したいため、僕はだいたいいつもレイアウトを無しにしています。
(先程、レイアウトに詳しくないと述べましたが、これが主な原因です。)
JLabelに関する追加情報
label.setHorizontalAlignment(JLabel.CENTER);
こうすることで、文章の位置を中央にできます。
デフォルトでは左詰め(厳密には少し違う)になっています。
そして...完成
最後に、ストップウォッチっぽくするためにいろいろ加えます。
以下がサンプルコードです。
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();
}
}
結果
ふう、だいぶ長くなってしまいましたね。
swingに関する追加情報はあまりないのと説明するのがすごく面倒なので、解説は省略します。
このプログラムには、マジックナンバーやデータレースなど、問題が多々ありますが、そこら辺は皆さんで改善案を考えてみてください。
最後に
だいぶ説明が雑になってしまいましたが、ここまで来ればある程度swingを扱えるようになったはずです。
さらに他にも、JTextArea
やJScrollPane
、paintComponents()
、Graphics
など、便利アプリ制作やゲーム制作に使える機能がたくさんあります。
以上、Javaはサーバーサイドや(入門とかで)CUIアプリ開発のイメージがありますが、GUIアプリ開発もできるよ、というお話でした。
42Tokyoとは関係なく、趣味全開の記事でしたが、なんと42Tokyoの課題にはswingに関するものもあるみたいです。課題の手助けとなることを信じます。
(42Tokyoの皆さん、その課題は僕が一番最初にクリアしたいので、それまでもうしばらく辛抱してほしいです!)