11. デザインパターン(2/x)
前回は、JavaによるGUIを備えたアプリケーションをSwingを利用して作成した。
JavaのGUIアプリケーションの歴史:
AWT Abstract Window Toolkit Javaの登場当初から存在するGUIフレームワーク。
OSに依存したGUIの描画を行う。Look&Feel(見た目)は、WindowsならWindowsアプリ、MacならMacアプリになる。
Swing AWTを拡張して機能の追加とLook&Feelの統一を実現した。Look&Feelを差し替え(アプリケーションのスキン変更)が可能。Swing(Wikipedia)
JavaFX GUIのレイアウトなど画面デザインついてはXMLとCSSを用いて記述し、アプリケーションのコードから分離したもの。JavaFX用のXMLファイルであるFXMLファイルは、Scene Buiderなどツールを利用して作成可能。
※GUIを専用ツールでコード作成なしに構築可能
※従来のSwingと併用して、コードによるGUIの作成も可能。SwingコンポーネントのJavaFXへの埋め込み
関連記事:
初めてJavaを触った人間がEclipseでJavaFXのGUIアプリを起動するまで
JavaFX: GUI作成ツールScene Builderは使いやすい!起動も速い!
JavaFX Scene Builder ツール配布元
実験:
実習室PCにインストールされた、Scene Builder を起動して、操作を試す。
起動: スタート → JavaFX Scene Builder → JavaFX Scene Builder 2.0
Containers から
BorderPane を配置
Paneを配置(BorderPane の TOP に配置)
Controls から
Button を Pane に配置。赤いガイドに合わせて 左右・中央 に3個配置
ImageView を BorderPane の Center に配置
ImageView の Properties (右サイドメニュー)の Specific の Image から画像ファイルを選択する。
結果の例)
メニュー → Preview → Show Preview in Window で、ウインドウ状態で動作を確認できる。
このGUIのデザインを利用してJavaFXアプリケーションを作成するには、File から Save で FXML ファイルを保存する。
JavaFXアプリケーションから、FXMLファイルをGUIレイアウト情報として読み込み、ButtonのプロパティのOnAction の設定などイベント処理のコードを作成していく。
※興味がある人は、JavaFXアプリケーションの作成について調べて eclipse から作成してみましょう。
Androidアプリ開発とGUI:
アプリ開発用IDE AndroidStudio に内蔵の Layout Editor を使用してGUIを構築する。
※Swingなど従来のJavaのGUIは利用できない。
関連記事:
Androidアプリ開発におけるGUIコンポーネントの使い方
もうXMLを使わずにAndroidのUIが作れる!「Anko」って知ってる?
JavaFXと今後のJavaクライアント技術:
世代的には最新のJavaのGUI構築フレームワークである JavaFX は、Java11以降で標準機能から外れOracle社のサポートが無くなる。
現状、JavaFXの採用事例は少なく機能更新も活発ではない。今後はOpen JDKのOSSコミュニティに発展がゆだねられている。
https://www.infoq.com/jp/news/2018/03/JavaFXRemovedFromJDK#.Wx-UpW_kPJM.twitter
今回実装する項目:
キャラクターに階層的に別のキャラクターを追加してグループとして扱う Composite パターン
キャラクターの操作と操作のUndo機能 Commandパターン
複数のキャラクターの処理 Iteratorパターン
画面の状況のスナップショットを記録 Memento パターン(Save と Load 機能)
基本形
基本形となるコードを以下に示す。
動作概要:
GUIパネルにボタン2つとステージ用パネルを配置。ボタン1でサウンドの再生と、ボタン2で画像の表示。
プロジェクト名 java6 を作成する。
コードで利用する画像やサウンドなどのリソースファイルを先回のプロジェクトから今回のプロジェクトにコピーしておく。
L11.java クラスを作成。コードは以下を参照。
import static java.lang.Math.*;
import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.ImageObserver;
import java.io.IOException;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
class L11 {
static AudioClip ac1, ac2, ac3;
static Image img1, img2;
public static void main(String args[]) {
ac1 = Applet.newAudioClip(L11.class.getResource("gun02.wav")); // 音声ファイル読み込み
ac2 = Applet.newAudioClip(L11.class.getResource("nc52380.wav")); // 音声ファイル読み込み
ac3 = Applet.newAudioClip(L11.class.getResource("se_ymc01.wav")); // 音声ファイル読み込み
try {
img1 = ImageIO.read(L11.class.getResource("kohashi.jpg"));
img2 = ImageIO.read(L11.class.getResource("photo1-2.jpg")); // 画像ファイル読み込み
} catch (IOException e) {
e.printStackTrace();
} // 画像ファイル読み込み
MyFrame myframe = new MyFrame();
}
}
class MyFrame extends JFrame {
Image img;
Stage stage;
MyFrame() {
setTitle("課題11"); // ウインドウのタイトル設定
setSize(800, 600); // ウインドウのサイズ
setLocationRelativeTo(null); // パソコンの画面中央にウインドウを配置
setVisible(true); // ウインドウを表示状態にする
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // ウインドウの閉じるボタンでアプリ終了の設定
JPanel bp = new JPanel(); // コンポーネント追加用パネル(コンテナ)
bp.add(new JButton("ボタン1") {
{
addActionListener(event -> {
L11.ac1.play();
});
}
});
bp.add(new JButton("ボタン2") {
{
addActionListener(event -> {
img = L11.img1;
stage.repaint();
});
}
});
add(bp, BorderLayout.NORTH); // オブジェクトをウインドウの上部に追加
stage = new Stage();
add(stage);
setVisible(true); // ウインドウを表示状態にする
}
class Stage extends JPanel {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setColor(new Color(128, 128, 128));
g2.fillRect(0, 0, getWidth(), getHeight());
g2.drawImage(img, 0, 0, this);
g2.dispose();
}
}
}
Compositeパターン
Compositeパターンを基本形に導入し、複数のシーンをまとめて1つのシーンとして扱える様にするグループ化に対応する。
MyFrameクラスのメンバーへ追加:
Scene rootScene = new SceneGroup();
MyFrameクラスのinner class Stageの描画内容を変更: rootSceneを描くようにする。
//g2.drawImage(img, 0, 0, this);
rootScene.draw(g2, this);
MyFrameクラスにフィールドとメソッドを追加:
画像付きシーンのSpriteを3つ生成。rootScene(ルートシーン)にスプライト s1 1つとグループ sg1 1つを追加。追加したグループには2つのスプライト s2 と s3を持たせる。
グループsg1を45度回転し、300前進。
s3 を90度回転し100前進。サイズを半分にする。
Sprite s1 = new Sprite(L11.img1);
Sprite s2 = new Sprite(L11.img1);
Sprite s3 = new Sprite(L11.img1);
SceneGroup sg1 = new SceneGroup();
private void createScene() {
rootScene.add(s1);
rootScene.add(sg1);
sg1.add(s2);
sg1.add(s3);
sg1.p.rotate(45);
sg1.p.move(300);
s3.p.rotate(90);
s3.p.move(100);
s3.scale = 0.5;
}
MyFrameのコンストラクタでステージ生成コードを呼び出す。
createScene();
基本形L11にクラスを追加:
abstract class Scene
抽象クラス Scene は、Sprite と SceneGroup の親となるクラス(スーパークラス)
役割:
・Sprite も SceneGroup もどちらも Scene として扱えるようにする。そうすることで、SceneGroupに階層的にSceneGroupやSpriteを区別なく追加することが出来るようになる。
・add と draw の実装を与える。サブクラス側で メソッドをオーバライドして、実際の動作をプログラムする。オーバライドしない場合、何もしない空のブロック { } を標準動作とする。以下のコードでは Sprite に add しても何も起きない。
・シーンの描画座標と向き Position をメンバーに持つ
※この様にスーパークラスにメンバーを持たせる必要が無い場合は、abstruct class ではなく、interface として定義すればよい。
class Position
シーンの位置と向きを保持するクラス。タートルグラフィック的な移動 move と 回転 rotate による位置の変更メソッドを持つ。
class Sprite extends Scene
画像を持たせたシーン。画像の倍率も保持する。draw でシーンの座標と向きに合わせて画像を描画する。
class SceneGroup extends Scene
シーンをグループ化するシーン。自身にシーン(シーングループ または スプライト)を追加する機能、add を持つ。シーンは ArrayList クラスのオブジェクトに保持する。
以下のコードを、L11.javaの末尾に追加する。
abstract class Scene {
Position p = new Position();
void add(Scene s) {
}
void draw(Graphics2D g, ImageObserver o) {
}
}
class Position {
int x, y;
double dir = 0.0;
void move(double d) {
x += d * cos(dir * PI * 2.0 / 360.0);
y += d * sin(dir * PI * 2.0 / 360.0);
}
void rotate(double dir) {
this.dir += dir;
}
}
class Sprite extends Scene {
Image img;
AudioClip adc;
double scale = 1.0;
public Sprite(Image img) {
this.img = img;
}
void sound() {
adc.play();
}
void draw(Graphics2D g, ImageObserver o) {
g = (Graphics2D) g.create();
g.translate(p.x, p.y);
g.rotate(p.dir * PI * 2.0 / 360.0);
g.scale(scale, scale);
g.drawImage(img, 0, 0, o);
}
}
class SceneGroup extends Scene {
ArrayList<Scene> scenes = new ArrayList<Scene>();
void add(Scene s) {
scenes.add(s);
}
void draw(Graphics2D g, ImageObserver o) {
g = (Graphics2D) g.create();
g.translate(p.x, p.y);
g.rotate(p.dir * PI * 2.0 / 360.0);
for (Scene s : scenes) {
s.draw(g, o);
}
}
}
定数 PI の部分のエラー修正について:
Math.PI とかけばエラーにはならない。
ここでは、static import 機能を利用して Math を省略するコードを追加する。
import static java.lang.Math.*
ArrayList の実験:
ArrayList<Scene> について
Javaのジェネリクス(総称型)を利用している。
JSHELL を利用して実験する。jshell は jdk9 から導入された、javaのREPL(Read Eval Print Loop)環境。
電卓のように、入力したコマンドを即時解釈して実行結果を確認できる。(インタープリタ)
※以下の実行例の 特殊な型 var について 参照 【JDK 10】varを使った型推論
※JDK9 では var はまだ利用できない。 ArrayList<Integer> a = と書かなければならない。
スタート → cmd でコマンドプロンプトを開く
d:
cd java\jdk-9.0.1\bin
jshell
jshell> var a = new ArrayList<Integer>()
a ==> []
jshell> a.add(1)
$11 ==> true
jshell> a.add(3)
$12 ==> true
jshell> a
a ==> [1, 3]
jshell> var b = new ArrayList<String>()
b ==> []
jshell> b.add("abc")
$15 ==> true
jshell> b.add("abc")
$16 ==> true
jshell> b
b ==> [abc, abc]
jshell> b.add(1)
| エラー:
| 不適合な型: intをjava.lang.Stringに変換できません:
| b.add(1)
Commandパターン
Commandパターンを追加する。コマンドの実行の履歴を残し、直前の操作をキャンセル可能にする。
パネルには移動・回転・Undoボタンを追加する。
L11に以下のパッケージをインポートする。
import java.util.Stack;
import java.util.function.Supplier;
interface Command
移動・回転などのコマンドに共通の機能、実行とキャンセルのインターフェースを定義している。
class SceneMove implements Command
移動と移動のキャンセルのコマンド。移動前の座標を記録している。キャンセル時にシーンの座標を移動前の座標に書き戻す。
class SceneRotate implements Command
回転用のコマンド。回転のキャンセル機能も持つ。
class MacroCommand
コマンドの実行履歴を記録する。undoで一番最後に実行したコマンドをキャンセル操作し、履歴から取り除く。
Stack<Command>
Javaのスタックを利用して、履歴を記録している。
JSHELLで Stack の動作を確認する。(実演)
コードの末尾に以下を追加。
interface Command {
void execute();
void cancel();
}
class SceneMove implements Command {
Scene s;
double d;
int x, y;
SceneMove(Scene s, double d) {
this.s = s;
this.d = d;
x = s.p.x;
y = s.p.y;
}
public void execute() {
s.p.move(d);
}
public void cancel() {
s.p.x = x;
s.p.y = y;
}
}
class SceneRotate implements Command {
Scene s;
double dir, orig_dir;
SceneRotate(Scene s, double dir) {
this.s = s;
this.dir = dir;
orig_dir = s.p.dir;
}
public void execute() {
s.p.rotate(dir);
}
public void cancel() {
s.p.dir = orig_dir;
}
}
class MacroCommand {
Stack<Command> stack = new Stack<Command>();
void add(Command cmd) {
stack.push(cmd);
}
void undo() {
Command cmd = stack.pop();
cmd.cancel();
}
void clear() {
stack.clear();
}
}
MyFrameのメンバーにコマンド履歴用のオブジェクトを追加
MacroCommand history = new MacroCommand();
MyFrameのコンストラクタ―にボタンの配置コードを追加
bp.add(new JButton("ボタン4") {
{
addActionListener(event -> {
Command cmd = new SceneMove(s1, 10);
cmd.execute();
history.add(cmd);
stage.repaint();
});
}
});
bp.add(new JButton("ボタン5") {
{
addActionListener(event -> {
Command cmd = new SceneRotate(s1, 15);
cmd.execute();
history.add(cmd);
stage.repaint();
});
}
});
bp.add(new JButton("ボタン6") {
{
addActionListener(event -> {
history.undo();
stage.repaint();
});
}
});
コマンドボタン追加用の補助関数の準備。関数インターフェースを利用して、コマンド登録を出来るようにする。
JButton createCommandButton(String name, Supplier<Command> sc) {
return new JButton(name) {
{
addActionListener(event -> {
Command cmd = sc.get();
cmd.execute();
history.add(cmd);
stage.repaint();
});
}
};
}
↑このコマンド登録用コードにより、MyFrameのコンストラクタのボタン配置コードを以下の様にシンプルに記述できるようになる。
bp.add(createCommandButton("ボタン5a",() -> new SceneRotate(s1,-15)));
Mementoパターン
Mementoパターンを実装し、操作業況のスナップショットを記録する。スナップショットを利用して、状態を復元可能にする。
save と load ボタンを配置する。
interface Memento
記録保存と記録読み込みのインターフェース。
abstract class Scene
に記録用オブジェクトの取得用コード、 abstract Memento getMemento();
を追加。
class Position
に現在位置と向きの記録用オブジェクトの生成メソッドを追加
追加と修正用コード:
abstract class Scene {
Position p = new Position();
void add(Scene s) {
}
void draw(Graphics2D g, ImageObserver o) {
}
abstract Memento getMemento();
}
interface Memento {
void save();
void load();
}
class Position {
int x, y;
double dir = 0.0;
void move(double d) {
x += d * cos(dir * PI * 2.0 / 360.0);
y += d * sin(dir * PI * 2.0 / 360.0);
}
void rotate(double dir) {
this.dir += dir;
}
Position copy() {
Position p = new Position();
p.x = x;
p.y = y;
p.dir = dir;
return p;
}
}
Spriteクラスに、Memento用のメソッドを追加する。
Spriteの記録には、位置座標と向き、拡大率を記録する。
同様に
SceneGroupクラスに、Memento用のメソッドを追加する。
SceneGroupの記録には、シーングループ内のすべてのシーンの記録を保存する。
記録の読み込みについても全てのシーンの記録を読み込む。
※階層的にシーンが構成されていても、save()が階層を辿って実行されるので、全階層のシーンが記録保存、再読み込みの対象となる。
class Sprite extends Scene {
Image img;
AudioClip adc;
double scale = 1.0;
public Sprite(Image img) {
this.img = img;
}
void sound() {
adc.play();
}
void draw(Graphics2D g, ImageObserver o) {
g = (Graphics2D) g.create();
g.translate(p.x, p.y);
g.rotate(p.dir * PI * 2.0 / 360.0);
g.scale(scale, scale);
g.drawImage(img, 0, 0, o);
}
Memento getMemento() {
Memento m = new SpriteMemento();
m.save();
return m;
}
class SpriteMemento implements Memento {
Position mp;
double mscale;
public void save() {
mp = p.copy();
mscale = scale;
}
public void load() {
p = mp;
scale = mscale;
}
}
}
class SceneGroup extends Scene {
ArrayList<Scene> scenes = new ArrayList<Scene>();
void add(Scene s) {
scenes.add(s);
}
void draw(Graphics2D g, ImageObserver o) {
g = (Graphics2D) g.create();
g.translate(p.x, p.y);
g.rotate(p.dir * PI * 2.0 / 360.0);
for (Scene s : scenes) {
s.draw(g, o);
}
}
Memento getMemento() {
Memento m = new SceneGroupMemento();
m.save();
return m;
}
class SceneGroupMemento implements Memento {
Position mp;
ArrayList<Memento> mscenes = new ArrayList<Memento>();
public void save() {
mp = p.copy();
for(Scene s:scenes) {
mscenes.add(s.getMemento());
}
}
public void load() {
p = mp;
for(Memento m:mscenes) {
m.load();
}
}
}
}
MyFrameのメンバーに記録保存用のオブジェクトを用意。
※複数の保存が必要な場合は、ここを増やす。
Memento mem1;
save と load ボタンの配置。
MyFrameのコンストラクタ―に追加。
bp.add(new JButton("save") {
{
addActionListener(event -> {
mem1 = rootScene.getMemento();
});
}
});
bp.add(new JButton("load") {
{
addActionListener(event -> {
mem1.load();
history.clear();
stage.repaint();
});
}
});
ここまででいったん完成。動作確認をする。
応用編
操作の対象をボタンで切り替えられるように修正する。
さらに、MyFrameとSceneをリファクタリングする。
Scene target = rootScene;
ボタン作成ヘルパー
JButton createButton(String name, ActionListener al) {
return new JButton(name) {
{
addActionListener(al);
}
};
}
操作ボタンのアクションを修正する。
操作対象(target にセットされたシーン)操作するコマンドを生成して、各ボタンに登録する。
bp.add(createButton("選択1",event -> {
target = s1;
}));
bp.add(createButton("選択2",event -> {
target = s2;
}));
bp.add(createCommandButton("前進",() -> new SceneMove(target,10)));
bp.add(createCommandButton("後退",() -> new SceneMove(target,-10)));
bp.add(createCommandButton("時計回り",() -> new SceneRotate(target,15)));
bp.add(createCommandButton("反時計回り",() -> new SceneRotate(target,-15)));
bp.add(createButton("undo",event -> {
history.undo();
stage.repaint();
}));
bp.add(createButton("save",event -> {
mem1 = rootScene.getMemento();
}));
bp.add(createButton("load",event -> {
mem1.load();
history.clear();
stage.repaint();
}));
Sceneのメンバーに記憶メモリ Memento を生成するコード Supplier<Memento> の登録場所を追加。
abstract class Scene {
Position p = new Position();
Supplier<Memento> sm;
void add(Scene s) {
}
void draw(Graphics2D g, ImageObserver o) {
}
Memento getMemento() {
Memento m = sm.get();
m.save();
return m;
}
}
Sprite と SceneGroup に Mementoの生成コードを追加。
class Sprite extends Scene {
Image img;
AudioClip adc;
double scale = 1.0;
public Sprite(Image img) {
this.img = img;
sm = () -> new Memento() {
Position mp;
double mscale;
public void save() {
mp = p.copy();
mscale = scale;
}
public void load() {
p = mp;
scale = mscale;
}
};
}
void sound() {
adc.play();
}
void draw(Graphics2D g, ImageObserver o) {
g = (Graphics2D) g.create();
g.translate(p.x, p.y);
g.rotate(p.dir * PI * 2.0 / 360.0);
g.scale(scale, scale);
g.drawImage(img, 0, 0, o);
}
}
class SceneGroup extends Scene {
ArrayList<Scene> scenes = new ArrayList<Scene>();
{
sm = () -> new Memento() {
Position mp;
ArrayList<Memento> mscenes = new ArrayList<Memento>();
public void save() {
mp = p.copy();
for(Scene s:scenes) {
mscenes.add(s.getMemento());
}
}
public void load() {
p = mp;
for(Memento m:mscenes) {
m.load();
}
}
};
}
void add(Scene s) {
scenes.add(s);
}
void draw(Graphics2D g, ImageObserver o) {
g = (Graphics2D) g.create();
g.translate(p.x, p.y);
g.rotate(p.dir * PI * 2.0 / 360.0);
for (Scene s : scenes) {
s.draw(g, o);
}
}
}