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への埋め込み

関連記事:

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は利用できない。

関連記事:

Layout Editor による UI の作成

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.*

参照 static importの基本的な使い方

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);

}

}

}