epublibで電子書籍を作成する

イントロダクション

電子書籍をJavaで作れないか調べたところ、ありました。
このサイトにepublibの使い方などが書いてあった。

引用すると下のようなものです。

Epublib は、epub ファイルを管理するための Java ライブラリです。プログラムおよびコマンドライン ツールから epub ファイルの読み取りと書き込みが可能です。

Epublib は、コマンドライン ツールも付属するライブラリです。これは、より大きな Java アプリケーションの一部として、またコマンドライン ツールとして使用することを目的としています。

epublibを使う

下のようにMavenを使ってやりました。
POMファイルに下のように記述して、再ビルドすれば準備オッケーでした。
そして、JavaDocはここにありました。

リポジトリの追加

  <repositories>
    <repository>
      <id>psiegman-repo</id>
      <url>https://github.com/psiegman/mvn-repo/raw/master/releases</url>
    </repository>
  </repositories>

ライブラリの追加

  <dependencies>
    <dependency>
      <groupId>nl.siegmann.epublib</groupId>
      <artifactId>epublib-core</artifactId>
      <version>3.1</version>
    </dependency>
  </dependencies>

リソースの準備

そして、プログラムともとになるHTMLファイルを用意

プログラムの作成と実行

出力したものはこちらのYOUTUBEで見れます。

Githubにソースがあったのでそれを参考にしました。

【作成したコード】
元々あったコード(サンプルコード)を改造しています。改造した部分はリソースの取得部分「getResource(引数は一つ)」です。
ここでgetResourceAsStream()を使うとパスの指定が違ってしまったので、Pathsで取得してInputStreamを生成、返却するように修正しました。

    private static InputStream getResource(String path ) throws IOException{
        Path p = Paths.get(path);
        InputStream im = Files.newInputStream(p);
//        InputStream im = Main.class.getResourceAsStream( path );
        if (im == null) {
            throw new IOException("ファイルが取得できませんでした。" + path);
        }
        return im;
    }

    private static Resource getResource(String path, String href ) throws IOException {
        Resource res = new Resource( getResource( path ), href );
        return res;
    }
    public static void main(String[] args) {
        try {
            // Create new Book
            Book book = new Book();
            Metadata metadata = book.getMetadata();

            // Set the title
            metadata.addTitle("EPUB4J test book 1");

            // Add an Author
            metadata.addAuthor(new Author("Joe", "Tester"));

            final String ROOT = "src/main/resources";
            // Set cover image
            book.setCoverImage(
                    getResource(ROOT + "/book1/test_cover.png", "cover.png"));

            // Add Chapter 1
            book.addSection("Introduction",
                    getResource(ROOT + "/book1/chapter1.html", "chapter1.html"));

            // Add css file
            book.getResources().add(
                    getResource(ROOT + "/book1/book1.css", "book1.css"));

            // Add Chapter 2
            TOCReference chapter2 = book.addSection( "Second Chapter",
                    getResource(ROOT + "/book1/chapter2.html", "chapter2.html"));

            // Add image used by Chapter 2
            book.getResources().add(
                    getResource(ROOT + "/book1/flowers_320x240.png", "img/gardening.png"));

            // Add Chapter2, Section 1
            book.addSection(chapter2, "Chapter 2, section 1",
                    getResource(ROOT + "/book1/chapter2_1.html", "chapter2_1.html"));

            // Add Chapter 3
            book.addSection("Conclusion",
                    getResource(ROOT + "/book1/chapter3.html", "chapter3.html"));

            // Create EpubWriter
            EpubWriter epubWriter = new EpubWriter();

            // Write the Book as Epub
            epubWriter.write(book, new FileOutputStream("test1_book1.epub"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

課題

ここまでなんとかできたけど、ページを適当なところで次ページに移動したい。ここら辺のコントロール方法が知りたい。

VSCodeでMarkDownを使用して電子書籍を作成する

イントロダクション

今まで作成してきたブログ(技術ブログ)をまとめようと思いました。しかし、自分はデザインなどの経験がなく、絵も描けない。。。

ならば、何かしら自分の畑にあるものを使いたいと考えたのが「VSCodeでMarkDownを使用して電子書籍を作成する」ということです。

ポイントとしては、VSCodeでMD(MarkDown)を書く事のメリットとしては、MDからHTMLやPDFなどの別フォーマットへ出力することができるというところです。

しかし、レイアウトを工夫したり、背景画像の設定が行いずらい部分もあるので、HTMLからEPUBやPDFに出力することを考えいます。

HTMLから返還するべきファイル形式を調べる

つまるところは、電子書籍販売時のファイル形式を調査するということです。
結論は以下の3種類が、主だったファイル形式でした。

  • HTML
  • PDF
  • EPUB

VzSCodeでは、「MarkDown PDF」というエクステンションがあります。

まずは、これを使って自分の作りたいものを作れるように頑張りたいと思う次第です。

MarkDown-PDF

Githubにて公開されているエクステンションです。これは以下の機能をサポートしているという記述がありました。

機能

  • 構文の強調表示
  • 絵文字
  • マークダウンイットチェックボックス
  • マークダウンイットコンテナ
  • マークダウンイットインクルード
  • プラントUML -> マークダウン・イット・プラントムル
  • マーメイド

単語だけ並べてもわからないので、それを調べます。。。

まずはセットアップ

VSCodeでエクステンションを追加します。がエクステンションのアイコンです。これをクリックすると下のように見えます。

筆者のVSCodeでは「HTML Preview」と「Markdown PDF」のエクステンションがインストールされています。

ワークスペースの設定をする

File -> Save Workspace As ... を選択します。

すると「XXX.code-workspace」のようなファイルが作成されます。

このファイルはJSONファイルで、単純に設定情報を定義するものになっています。

{
    "folders": [
        {
            "path": "."
        }
    ],
    "settings": {}
}

MarkDown PDFの設定を入れる

今回は、MarkDown PDFでCSSを適用したかったので下のように設定を書きました。「markdown-pdf.styles」というプロパティ(オプション)にCSSを指定しました。GithubのCSSです。ここからダウンロード(chekout)できます。そして、自分の作成する予定のCSS「test.css」も追加しています。

{
    "folders": [
        {
            "path": "."
        }
    ],
    "settings": {
        "markdown-pdf.styles": ["./github-markdown-light.css", "./test.css"]
    }
}

test.cssには下のようなCSSを書いています。

@charset "utf-8";

.sam { color:red}

MDファイルには下のように書いています。

## テスト
<p class="sam">サンプル文字</p>

この状態で、PDF出力してみます。

上のように出力されました。CSSはHTML作成するものと同じです。しかし、MDでクラスを指定する方法がわかりません。

わからないなら調べればよいので、調べます。

MDにクラス属性を指定する

こちらのサイトを参考にしました。
【Markdown】

IDの設定:# Header 1 {#header1}
CLASSの設定:## The Site {.main}

【HTML】

<h1 id="header1">Header 1</h1>
<h2 class="main" id="the-site">The Site</h2>

改ページを入れる

下のコードを追加するとそれ以下のコードは次のページに配置されます。

<div style="page-break-before:always"></div>

想定外なところ

見出しにはクラス属性を指定できたが、段落などの指定ができなかった。

なぜだろう?他のサイトを見ても同じようなやり方をしている。。。
ちょっと迷宮入りかもしれない。

複数カラムのレイアウト

  1. Ctrl + Shift + P を押下
  2. Markdown Preview Enhanced: Customize CSSをクリック
  3. CSS(style.less)でCSSを作成
  4. Ctrl + Shift + Vでプレビューを表示
  5. 右クリックしてPDFを出力する

作成したCSSは以下の通りです。

.column-left{
  float: left;
  width: 57.5%;
  text-align: left;
}

.column-right{
  float: right;
  width: 37.5%;
  text-align: left;
}

MarkDownには下のように記述

<section class="column-left">
 ....
</section>

<section class="column-right">
 ....
</section>

Emoji(絵文字)を使う

  1. VS Code 拡張機能セクションからMardown Emojiをインストールします。
  2. 次のように 2 つのコロンの間に必要な絵文字の名前を入力します。「:smile:」

絵文字のリストはこちらのページにあります。

E-Book出力

こちらのページに必要なアプリケーションが記載されていました。
筆者の場合はWindowsを使用しているので、MSI、インストーラーでインストールできました。「ebook-convert」というのは起動ファイルのことでMSIでインストールしたら、$PATHには追加されるので問題ありません。

Java 学習 ~Greenfootのインストール~

イントロダクション

GreenFootというアプリケーションがあります。Javaプログラミングを教えて学ぶツールのようです。

インストーラーがあるので一発インストールです。

リンク一覧

Greenfootを使う

インストールしたGreenfootを起動すると下のようになります。

「Create or Open a scenario using the Scenario menu」とあり、英語になっています。

まずは日本語化

上部のメニュー「Tool」→「interface」から下のような画面が開けます。ここで日本語をしていします。

セットしたら、アプリケーションを閉じてもう一度開きます。この作業のことを「再起動」といいます。

一応、日本語化されました。

チュートリアルをやる

チュートリアルを行うのに、サンプルストーリーがあるようです。「このチュートリアルでは、ここからダウンロードできる」とあるのでここから、シナリオ(gfarファイル)をダウンロードします。
ダウンロードしたらそのファイルを開きます。
[Scenario] -> [Open GFAR]でダウンロードしたファイルを指定して開きます。

開くと下のような画面が見れます。

でわ、はじめましょう

こちらにチュートリアルのページがあります。

基本的な操作としては、Actorクラスを継承して作成されている「Wombat」と「Leaf」クラスを右クリックして操作を選択、Shiftボタンを押して「new」したクラス(オブジェクト)をWorldに配置するということです。

配置ができたら、次は「Run」ボタンを押下して、各オブジェクトを動かします。

「Wombat」クラスは葉っぱ(Leaf)を食べます。そしてこれを探すために動きます。
そして「Leaf」クラスは動きません。食べられるとなくなります。

まずは動かしてみましょう。

ここから、何かしらの物語がはじまるんだろうな。。。

Java Bag fix ~ArithmeticException in BigDecimal~

ArithmeticExceptionが出ました。

これは、割り切れない処理を行ったときにです用です。
JavaDocを参照すると「1 / 3」を行うと出力されるようです。

【実行コード】

BigDecimal three = new BigDecimal(1);
BigDecimal four = new BigDecimal(3);
System.out.println(three.divide(four));

【エラーログ】

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

    at java.math.BigDecimal.divide(BigDecimal.java:1693)
    at jp.zenryoku.sample.statics.BigDecimalSample.test01(BigDecimalSample.java:14)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)

解決策

スケール指定と、まるめ算の指定を行うことで解決しました。

※注意スケールをセットしたら、そのBigDecimalクラスを返却します。

BigDecimal left = new BigDecimal(1);
BigDecimal right = new BigDecimal(3);
BigDecimal setScale = left.setScale(2);
BigDecimal res = setScale.divide(right, BigDecimal.ROUND_HALF_UP);
System.out.println(res.toString());

下のコードだとスケールを設定していない形になるのでエラーが出る。

BigDecimal left = new BigDecimal(1);
BigDecimal right = new BigDecimal(3);
left.setScale(2);
BigDecimal res = setScale.divide(right, BigDecimal.ROUND_HALF_UP);
System.out.println(res.toString());

JavaDoc 読解~java.util.Observableクラスの使い方~

java.util.Observableクラス

このクラスは、Observerインターフェースと併用するようにできています。デザインパターンの「Observerパターン」です。

処理の概要

Observableクラスに登録されたObserverクラス(Observerインターフェースをimplementsしたクラス)のupdateメソッドを
変更の通知があったときに呼び出す。

実装してみた

Observableクラスにメインメソッドを実装しました。はじめはObserverクラスに何かしらの変更があったときにupdate処理が走るのかと思ったのですが
次のメソッドが動いたときに走るようです。

Observable#setChanged()とObservable#notifyObservers()を呼び出すとObserverクラスのupdateが起動します。

Observableを継承して作る

import java.util.Observable;
import java.util.function.Function;

public class ObserverSample extends Observable {
    /** 引数String, 返却値Stringのメソッド */
    private Function<String, String> func;

    public ObserverSample(Function fun) {
        this.func = fun;
    }

    public void execute(String str) {
        System.out.println(func.apply(str));
    }

    public static void main(String[] ags) {
        // Functionに意味はないです。実装したときの残骸です。。。
        ObserverSample sample = new ObserverSample(str -> {
            return "aaa: " + str;
        });

        ChildOverver ovs = new ChildOverver("aaa");
        ChildOverver ovs1 = new ChildOverver("bbb");
        ChildOverver ovs2 = new ChildOverver("ccc");
        sample.addObserver(ovs);
        sample.addObserver(ovs1);
        sample.addObserver(ovs2);

        sample.setChanged();
        sample.notifyObservers();
        System.out.println("^^^^^^^^^^^^^^^^^^^^");
        sample.setChanged();
        sample.notifyObservers("pppppp");
    }
}

Observerクラス

import java.util.Observable;
import java.util.Observer;

public class ChildOverver implements Observer {
    private String str;
    public ChildOverver(String str) {
        this.str = str;
    }
    @Override
    public void update(Observable o, Object arg) {
        System.out.println("Param is " + str);
        System.out.println("Count: " + o.countObservers());
        System.out.println("arg: " + arg);
    }

    public void setStr(String newStr) {
        this.str = newStr;
    }
}

プログラムの実行結果

Param is ccc
Count: 3
arg: null
Param is bbb
Count: 3
arg: null
Param is aaa
Count: 3
arg: null
^^^^^^^^^^^^^^^^^^^^
Param is ccc
Count: 3
arg: pppppp
Param is bbb
Count: 3
arg: pppppp
Param is aaa
Count: 3
arg: pppppp

Scratchをやってみよう。~はじめてのプログラミング~

プログラミングってなに?

学校でタブレットが配られて、「さぁプログラミングをやてみよう!」という人が増えてきました。
学校の先生たちも一生懸命に「プログラミングの勉強」をしています。

自己紹介

Javaプログラミングを布教す活動をしている、「たくのじ」と申します。Javaプログラミングは17年ほどやっています。
最近は、テキストRPGというアプリを作成しています。

Javaでクロス!モンハンじゃないよ?
Javaプログラミングの布教をするために奮闘中

プログラミングの必要性

「プログラミング」と聞いて「?」が頭に浮かんだ方、正常でございます。ある意味「マニアック」な領域です。
文部科学省など、「プログラミング教育」を推進しているようですが、「プログラミング」を仕事にしていた自分としては「主旨はなに?」と疑問に思いました。
文部科学省のサイトにある次のPDFを見てみると「小学校プログラミング教育の手引(第三版)」に以下のように記述がありました。

コンピュータを理解し上手に活用していく力を身に付けることは、あらゆる活動においてコンピュータ等を活用することが求められるこ
れからの社会を生きていく子供たちにとって、将来どのような職業に就くとしても、極めて重要なこととなっています。

筆者の解釈

コンピューターは世間で仕事などいろんな場面で使われるので。。。

  1. PCがどんなものか、理解しましょう。
  2. 存在するアプリ・インターネット・ビッグデータなどの扱い方を理解しましょう。

上記のようなことを、子供たちに学習させようという意図が感じられました。

プログラミングを学んでみた感想

筆者も0からプログラミングを学習したので、学習してみた結果どう感じたのか?を記述したいと思います。

一言でいうならば、『プログラミングは、主に「設計→実装→テスト」をいろんなレベルで繰り返す行為』という認識を持っています。

具体的には、以下の通りです。

設計

  1. 何かしらの目的を定めて、それを実現するための方法を考える。※必要ならば調査する
  2. パソコンに命令し、その目的を実行させるために必要な知識を学ぶ
  3. 同様に、実現するための仕組みを、頭の中で組み上げる

実装

  1. 設計したものを実現するためのモノを作る
  2. 作ったモノを動かして想定通りに動いたか確認する

テスト

  1. 特定のシナリオを用意して、動かし問題がないことを確かめる
  2. 想定とはずれたような操作をしても問題が発生しないことを確かめる

つまり

上記のようなことを、実際の物を使って行うと、『お金がいくらあっても足りない』ということになってしまいますが、プログラミングであればPCがあれば事足ります。

言葉を変えるならば、計画~実行~振り返りの工程を手軽に、体験できるということです。

「何かしらのアクションを起こす」ということと一致することなので人として、社会人として学ぶ事がたくさんあります。

我々大人も、上記のような体験(仕事)を通して成長してきました。PCを使えば「会社を巻き込まなくてもある程度の規模で『プロジェクト』を立ち上げて実行すること」ができます。

プログラミングが必要なわけ

つまり、社会人としての総合力を身に着けるよい材料になるので「プログラミング教育」を義務教育化しているのであろう。というところに落ち着きました。

はじめてのプログラミング

では、どこから始めようか?と疑問が出ると思います。プログラミングにはいろんな言語がありその言語も目的によって使い分けます。
正確には、「どの言語が良いか決めます。」

今回は。「はじめて」ということと「プログラミング教育向け」という理由から『Scratch(スクラッチ)』というプログラミング言語を紹介したいと思います。

スクラッチをやってみよう

まずは、ブラウザ(Google Chrome, Edge, Safari etc ...)でこちらのURLにアクセスしてみてください。

プログラミングは身近なもの

動画でプログラミングのやり方を学ぶ事ができます。世界ではプログラミングを広めようという動きがたくさんあります。
その中の一つとしてマサチューセッツ工科大学(MIT)があります。他にもプログラミングを広めるためにスイッチ財団などが出しているラズベリーパイ, Microsftが出しているマイクロビットなどたくさんあります。

そして。非営利のプログラミングクラブ「CoderDojo」も全世界に広まっています。日本ではCoderDojo Japanがあります。※当然日本全国にCoderDojoXXXがあります。

具体的には、北海道であれば次のDojoがあります。※もしかしたら他にもあるかもしれません。

スクラッチのやり方

チュートリアルを見るのが一番ですが、ここでは簡単に(大人向けに)記述します。こちらのページにアクセスると下のような画面が見れます。

左側に「動き」「見た目」などの項目があるのでそれを選択すると、色々なコマンドが表示されます。
コマンドは「操作」ということです。
チュートリアルにあった。操作であれば、「10歩動かす」というコマンドがあるのでそれをクリックすると「ネコが10歩動きます」

まずは、やってみることが大切ですが、さわり程度に知っておくことも大切ですね。

スクラッチで作品を作る

スクラッチのページを開くと、上部に「作る」「見る」などの項目があります。

「作る」を選ぶと

下のような画面が見れて、「実際に何か作ってみよう」となります。

「見る」を選ぶと

他の人が作った、プロジェクト(作品)を見ることができます。この作品はプログラム(ブロック)を書き換えてオリジナルなものに変更することができます。
もちろん。人のやり方を真似するもよし、人のコードから学習するもよし。使い方(学び方)は自由です。

プログラミングのコツ

作るの画面で「言葉のブロック」を組み合わせてプログラムを組みます。
具体的には、下のように考えて作ります。

  1. 下の「ブロック」にある文言を組み合わせて作ります。

  2. 具体的には、次のようにブロックを組むと、猫が10歩、歩きます。

上記のように、「ネコが、XXXしたときに、○○○する」という文章を作ったのと同じように考えることができます。
これが「コツ」です。

つまり

言葉で考えて、それをプログラム(ブロック)におとし込むというところです。

Java SwingでMP3を再生しようとしたらエラーが出た・・・

java audioエラー

ソースは、下のようなものです。

    public AudioUtil(String path) throws RpgException {
        try {
            InputStream in = Files.newInputStream(Paths.get(path));
            audioStream = AudioSystem.getAudioInputStream(in);
            AudioFormat audioFormat = audioStream.getFormat();
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
            audioClip = (Clip) AudioSystem.getLine(info);
            audioClip.addLineListener(this);
        } catch (IOException | LineUnavailableException | UnsupportedAudioFileException e) {
            throw new RpgException(e.getMessage());
        }
    }

テストクラスは、以下

    public void initAudio() {
        try {
            target = new AudioUtil("game/stories/html/title.mp3");
        } catch (RpgException e) {
            e.printStackTrace();
            fail(e.getMessage());
        }
    }

スタックトレース

1つめのエラー

mark/reset not supported

jp.zenryoku.rpg.exception.RpgException: mark/reset not supported
at jp.zenryoku.rpg.utils.AudioUtil.(AudioUtil.java:24)
at jp.zenryoku.rpg.AudioTest.initAudio(AudioTest.java:18)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:578)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:27)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

java.lang.AssertionError: mark/reset not supported

at org.junit.Assert.fail(Assert.java:91)
at jp.zenryoku.rpg.AudioTest.initAudio(AudioTest.java:21)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:578)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:27)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

2つめのエラー

Stream of unsupported format

Java SoundはMP3をサポートしていないようだ。。。
JavaFXのmediaを使用します。

jp.zenryoku.rpg.exception.RpgException: Stream of unsupported format
at jp.zenryoku.rpg.utils.AudioUtil.(AudioUtil.java:25)
at jp.zenryoku.rpg.AudioTest.initAudio(AudioTest.java:18)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:578)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:27)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

java.lang.AssertionError: Stream of unsupported format

at org.junit.Assert.fail(Assert.java:91)
at jp.zenryoku.rpg.AudioTest.initAudio(AudioTest.java:21)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:578)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:27)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

JavaFX Media

MP3の再生にはJavaFXのライブラリを使用するみたいなので、そのようにします。Mavenでライブラリを追加。

        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-media</artifactId>
            <version>11</version>
            <classifier>linux</classifier>
        </dependency>

        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-graphics</artifactId>
            <version>11</version>
            <classifier>win</classifier>
        </dependency>
    </dependencies>

次のエラー

java.lang.IllegalStateException: Toolkit not initialized

スタックオーバーフローに聞いてみました。
下のように実装するということでしたが、今は別な方法があるようで。。。

public class JavaFXInitializer extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        // JavaFX should be initialized
        someGlobalVar.setInitialized(true);
    }
}
final CountDownLatch latch = new CountDownLatch(1);
SwingUtilities.invokeLater(new Runnable() {
    public void run() {
        new JFXPanel(); // initializes JavaFX environment
        latch.countDown();
    }
});
latch.await();

最終的に解決

JavaFXのJFXPanelを起動してやればOK!
次の依存関係を追加します。

        <!-- JavaFX -->
        <!-- https://mvnrepository.com/artifact/org.openjfx/javafx-swing -->
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-swing</artifactId>
            <version>11-ea+24</version>
        </dependency>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-media</artifactId>
            <version>11</version>
            <classifier>mac</classifier>
        </dependency>

        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-media</artifactId>
            <version>11</version>
            <classifier>linux</classifier>
        </dependency>

        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-graphics</artifactId>
            <version>11</version>
            <classifier>win</classifier>
        </dependency>

こんな感じでJFXPanelを起動

public class JavaFXInitializer {
    public static void launch() {
        Platform.startup(() ->{
            new JFXPanel();
        });
    }
}

オーディオを再生

public class AudioUtil {
    private MediaPlayer player;
    public AudioUtil(String path) throws RpgException {
        try {
            JavaFXInitializer.launch();
            URL url = Paths.get(path).toFile().toURI().toURL();
            Media media = new Media(url.toExternalForm());
            player = new MediaPlayer(media);
        } catch (MalformedURLException e) {
            throw new RpgException(e.getMessage());
        }

    }

    public void play() throws RpgException {
        player.play();
    }
    public void stop() throws RpgException {
        player.stop();
    }

}

Java アプリケーションビルド github actions

イントロダクション

ノンプログラマが、文章でRPGを作成できるアプリケーションテキストRPGを作成しています。
現段階では一通り動くようになったので、いろんなストーリーを作成してプログラムが問題なく動くことを確認するためのテストを行う工程に入りました。

そのため次のことを行おうと考えております。

Githubから実行可能ファイルをダウンロードできるようにする。

Github Actions

作成したテキストRPGはMavenでビルドする形をとっています。なのでPOMファイルを使用してビルドする形ですが、これをGithubActionsで起動できるように設定したいところです。

操作手順

GithubActionsを使用してMavenビルドするワークフローを設定して、GithubPackagesに公開します。

  1. GithubActionsは下の部分をクリックするとアクセスできます。

  2. ワークフローの作成

  3. 推奨されているものを選択しました。

  4. ビルドを行う設定をYMLファイルで行い、コミットします。※デフォルト設定でよい

YMLファイルについて

参考にしたのは、さくらインターネットさんのサイトです。
そして、詳細を記述しているのはGithubのドキュメントです。

各項目の意味

今回作成したYMLファイル。「#」でコメントを記述します。
英語の部分は自動生成されたもので、日本語のものは自分が記述したものです。「name」「on」「jobs」が主要な項目のようです。

# This workflow will build a package using Maven and then publish it to GitHub packages when a release is created
# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path
# テストの実施は設定していない。今後必要になったら実施するように編集する必要がある。
# 【注意するポイント】
# PUSHしたときにビルドが走る。改修などは別ブランチを切ってやるようにする。
name: TextRPG Ver0.8

on:
  release:
    types: [created]

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
        server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
        settings-path: ${{ github.workspace }} # location for the settings.xml file

    - name: Build with Maven
      run: mvn -B package --file pom.xml

    - name: Publish to GitHub Packages Apache Maven
      run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml
      env:
        GITHUB_TOKEN: ${{ github.token }}

GitHub アクションのワークフロー構文

Githubのドキュメントを参考にしています。

  • 「YAML を Y 分で学ぶ」も参考にするとよいらしいです。
  • ワークフロー ファイルは、.github/workflowsリポジトリのディレクトリに保存する必要があります。

name


ワークフローの名前。GitHub は、ワークフローの名前をリポジトリの [アクション] タブに表示します。を省略した場合name、GitHub はそれをリポジトリのルートからの相対ワークフロー ファイル パスに設定します。

run-name


ワークフローから生成されたワークフロー実行の名前。GitHub は、リポジトリの [アクション] タブのワークフロー実行のリストにワークフロー実行名を表示します。が省略されているか空白のみの場合run-name、実行名はワークフロー実行のイベント固有の情報に設定されます。たとえば、pushまたはpull_requestイベントによってトリガーされるワークフローの場合、コミット メッセージとして設定されます。

記述例

run-name: Deploy to ${{ inputs.deploy_target }} by @${{ github.actor }}

githubこの値には式を含めることができ、およびコンテキストを参照できますinputs。

on


ワークフローを自動的にトリガーするには、 を使用してonワークフローを実行できるイベントを定義します。使用可能なイベントのリストについては、「ワークフローをトリガーするイベント」を参照してください。

ワークフローをトリガーできる単一または複数のイベントを定義したり、タイム スケジュールを設定したりできます。特定のファイル、タグ、またはブランチの変更に対してのみワークフローの実行を制限することもできます。これらのオプションについては、次のセクションで説明します。

たとえば、次のワークフローは、pull_requestプル リクエスト ターゲティングのイベントが発生するたびに実行されます。

main( refs/heads/main)という名前のブランチ
mona/octocat( refs/heads/mona/octocat)という名前のブランチ
( )releases/のように名前が で始まるブランチreleases/10refs/heads/releases/10

指定のブランチを除くこともできるようです。

on:
  pull_request:
    # Sequence of patterns matched against refs/heads
    branches:    
      - main
      - 'mona/octocat'
      - 'releases/**'

MavenでJAR出力

POMファイルに以下の部分を追加する

    <build>
        <plugins>
            <!-- 実行可能jarファイル用のプラグイン -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.4.2</version>
                <configuration>
                    <finalName>test</finalName>
                    <descriptorRefs>
                        <!-- 依存するリソースをすべてjarに同梱する -->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <!-- 起動するメインメソッドを指定する -->
                            <mainClass>jp.zenryoku.rpg.TextRpgMain</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <!-- idタグは任意の文字列であれば何でもよい -->
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

これで、以下の手順を実行するとJARファイルが出力される。

  1. IntelliJ IDEA で Maven プロジェクトを開きます (ファイル → 開く)。

  2. View → Tool Windows → Mavenに移動して、Maven ツール ウィンドウが表示されていることを確認します。

  3. ツリーでプロジェクトを展開し、ライフサイクルを展開して、パッケージをダブルクリックします。

  4. IntelliJ が Mavenpackageフェーズを実行し、下の別のウィンドウに出力が表示されます。

  5. JAR ファイルはtarget/your-app-1.0.jar!で入手できます。

出力されJARファイルは2種類ありますが、「jar-with-dependencies」が付いているほうが実行可能jarファイルです。

Java Swing ゲーム作りや学習に画面をつくる

イントロダクション

Javaを学習して、基本文法がわかると今度は、Java API、つまりはListインターフェースなど既存のクラスを使用する事を学習すると思います。
ここら辺から、学習がうまく進まなくなる。自分がそうでした。。。

今思い返すと、何かしらのアプリケーションを作ってみるのが一番良いのだけど、「何かを作るほどの理解がない」「イマイチ自身が。。。」などと感じる人のもいるかもしれません。

しかし、まずはプログラミングを楽しみましょうという気持ちでSwingしてみませんか?
ちなみに、Jazzの世界ではカッコイイ演奏をすることを「Swingしてるねぇ!」といいます。関連は全くありません(笑)

ちなに、インスタンス=コンポーネントです。つまりラベルが表示されているというのはラベルがインスタンス化されているということです。
目に見えないインスタンスが目に見えマス。※目に見えないものもありますので注意が必要です。

Java Swing

Oracleのページでは、下のように書いていました。
Swingはグラフィカル・ユーザー・インタフェース(GUI)を構築し、豊富なグラフィック機能および双方向性をJavaアプリケーションに追加するコンポーネントのセットを実装しています。

Swingコンポーネントは、全体がJavaプログラミング言語により実装されています。プラガブルなルック・アンド・フィールにより、プラットフォーム間で同じ外観になるGUI、または現在のOSプラットフォーム(Microsoft Windows、Solaris(tm)、Linuxなど)のルック・アンド・フィールを想定したGUIが作成できます。

そして、チュートリアルもあるようです。

まずは、ここのチュートリアルを行い自分で学習していく土台を作っていくのも一つだと思います。

Swingの仕組み

Swingフレームワークでは、コンテナー(Container)にコンポーネント(Component)をセットして、それを表示するという仕組みを持っています。
単純に、画面上に「Hello World」と表示するプログラムを書くと下のようになります。

<プログラムコード1>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.getContentPane().add(new JLabel("Hello World."));
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}

処理内容

JFrame frame = new JFrame();の部分は、トップレベルコンテナーのJFrameクラスをインスタンス化しています。
そして、トップレベルコンテナーは以下の3つがあります。

JFrameについて

このうちJFrameクラスを使用するサンプルコードが良く見受けられます。上記のサンプルもこのJFrameを使用しています。
ちょっと長いですが、オラクルのドキュメントの説明が良いと思います。


Frame は、タイトルと境界線を持つトップレベル ウィンドウです。フレームのサイズには、ボーダー用に指定されたすべての領域が含まれます。この方法を使用して境界領域の寸法を得ることができるgetInsets。境界領域はフレーム全体のサイズに含まれているため、境界はフレームの一部を効果的に覆い隠し、サブコンポーネントのレンダリングおよび/または表示に使用できる領域を、左上隅の位置が 、 、および の長方形に制限し(insets.leftますinsets.top)。width - (insets.left + insets.right)のサイズは ですheight - (insets.top + insets.bottom)。
クラスのインスタンスとして実装されるフレームは JFrame、境界線やタイトルなどの装飾を持ち、ウィンドウを閉じるかアイコン化するボタン コンポーネントをサポートするウィンドウです。通常、GUI を使用するアプリケーションには、少なくとも 1 つのフレームが含まれます。アプレットもフレームを使用することがあります。
別のウィンドウに依存するウィンドウを作成するには (たとえば、別のウィンドウがアイコン化されると非表示になります)、dialogの代わりにa を使用しますframe.。別のウィンドウ内に表示されるウィンドウを作成するには、内部フレームを使用します。

まぁなんとなくの理解でもよいと思います。

コンポーネントの追加

frame.getContentPane().add(new JLabel("Hello World."));はラベル、テキストフィールド、ボタンなどのコンポーネントを追加する
時のコードです。つまり、このコードの引数にラベル以外のものもセットできるというわけです。

ちなみに、色付きのコードと下のコードは同じことを行っています。

Container cont = frame.getContentPane();
cont.add(new Label("Hello World."));

一行で書くか2行で書くかの違いです。細かいところでは、Containerのインスタンスを変数「cont」にセットしています。
複数回同じ処理をするのであれば、変数に取り出したインスタンスを使用するのがエコなコードになります。

処理の内容としては、以下の通りです。

  1. JFrameからコンテナーを取得する
  2. 取得したコンテナーにコンポーネント(ラベル)を追加する

閉じるときの処理設定

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);画面を閉じるときの処理です。
のコードを書くことで画面を閉じるとアプリケーションが終わります。

単純に追加

単純にコンポーネントを追加すると下のようになります。
<プログラムコード2>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame();

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."));
        cont.add(new JLabel("*************"));
        cont.add(new JLabel("**        **"));
        cont.add(new JLabel("**        **"));
        cont.add(new JLabel("**        **"));
        cont.add(new JLabel("*************"));

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}

<実行結果>

一番最後に追加したコンポーネントのみが表示されています。おそらく、コンテナーに一つのコンポーネントしか登録できないのでしょう。
なので、JPanelクラスを使用してコンポーネントを複数追加します。

JPanelを使う

上のコードを下の様に書き換えます。
<プログラムコード3>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame();

        Container cont = frame.getContentPane();
        JPanel panel = new JPanel();
        cont.add(panel);

        panel.add(new JLabel("Hello World."));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}

<実行結果>

コードのように四角っぽいのを表示したかったのですが、横並びですね。。。

これは、デフォルトで設定されているレイアウトマネージャ(XXXLayoutクラス)がFlowLayoutになっているので、横並びになります。

BorderLayoutについて

デフォルトで設定されているレイアウトクラスBorderLayoutクラスです。下の図のようにレイアウトを組むことができます。
<レイアウト図1>

上のコードでは、レイアウトの指定(NORTH, CENTERなど)を使用していないので、余計変な形になっています。

レイアウトを使う

<プログラムコード4>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);

        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }
}

BoxLayoutというクラスも出てきましたが、このクラスに関しては、さておきにして下のように表示されました。
<実行結果>

ポイントは、次のような部分です。

  1. JFrameのコンストラクターの引数に「Window Test」と追加したのでウィンドウのタイトル部分に表示されている。
  2. JPanelにBoxレイアウトを設定して、縦に追加する形を作ったので壮丁通りの表示になった。
  3. setSize()メソッドを使用して、JFrameのサイズを指定している。

レイアウトを色々試す

上記の表示コンポーネントに、いろいろと装飾を加えてみることにします。

コンポーネントにボーダーを入れてみる。

コンポーネント(表示している部品)に、ボーダーを入れるには次のメソッドを使用します。
JComponent#setBorder()を使用してボーダーを追加します。しかし表示しているJPanelのメソッドではありません。
それでも問題はありません。JavaDocを見ればわかるように、JPanelはJComponentのサブクラス(子供)になっています。
つまり、JComponentのメソッドを使用することができます。※privateのメソッドは使えません。

パネルにボーダーを入れる

プログラムコード4を修正して次のプログラムコード5を作成します。

<プログラムコード5>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);

        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }
}

実行結果は下の通りです。

ボーダーが真ん中らへんに1本入っただけです。。。これは、レイアウト図1のNORTHとCENTERにそれぞれ、ラベルとパネルを設定しているためNORTHとCENTERの境目にボーダーが票う辞された程度になっています。もっとわかりやすくほかのものも追加してみます。

<プログラムコード6>

public class SwingMain {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);

        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);

        JLabel west = new JLabel("東");
        west.setBorder(BorderFactory.createLineBorder(Color.BLUE));
        cont.add(west, BorderLayout.WEST);

        JLabel east = new JLabel("西");
        east.setBorder(BorderFactory.createLineBorder(Color.RED));
        cont.add(east, BorderLayout.EAST);

        JLabel south = new JLabel("南");
        south.setBorder(BorderFactory.createLineBorder(Color.YELLOW));
        cont.add(south, BorderLayout.SOUTH);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }
}

上記のように、東西南北と中心の5つの領域にそれぞれのコンポーネントをセットしてやる形の表示ができました。

簡単なリファクタリング

今度はBoxLayoutを使用してみます。ここで、プログラムのコード量が増えてきたので、コードを分割してみようと思います。
コードを分割する手段として使えるのは「メソッドを使う」というところです。
具体的には次のようにやります。

1.メソッドの処理を言葉で表現する。

筆者の表現したものなので、「これが正解!」ってことではないので注意してください。それぞれの人にそれぞれの言葉があるように表現の仕方は無限大にあります

  1. JFrameをインスタンス化
  2. BorderLayoutをセット
  3. 北(NORTH)にJLabel「Hello World」をセット
  4. 中央にJPanelをセット
  5. 東。。。
  6. 西。。。
  7. 南。。。

※性格が出るんですね。。。

このように、東西南北、中央の5つに各描画処理を行っているので、そのように処理を分割しようと思います。
メソッド名は、わかりやすいとおもうので「中央を描く」「北を描く」。。。のようにつけることにします。

2.表現したように分割

東西南北、中央の5つに分割することにしたので、次のようなメソッドを作成しました。まだからの状態です。引数および返り値もありません。
そのため、ビルドエラーが出ます。説明の段階を踏むためなので、ご了承ください。
<プログラムコード7>

public class SwingMain {
    ...
    /** 北を描く */
    private void darwNorth() {
    }
    /** 中央を描く */
    private void darwNCenter() {
    }
    /** 西を描く */
    private void darwWest() {
    }
    /** 東を描く */
    private void darwEast() {
    }
    /** 南を描く */
    private void drawSouth() {
    }
}

そして、単純に既存のコードを移植します。
<プログラムコード8>

public class SwingMain {
    public static void main(String[] args) {
        SwingMain main = new SwingMain();
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        main.darwNorth();
        main.darwNCenter();
        main.darwWest();
        main.darwEast();
        main.drawSouth();

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }

    /** 北を描く */
    private void darwNorth() {
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);
    }
    /** 中央を描く */
    private void darwNCenter() {
        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);
    }
    /** 西を描く */
    private void darwWest() {
        JLabel west = new JLabel("西");
        west.setBorder(BorderFactory.createLineBorder(Color.BLUE));
        cont.add(west, BorderLayout.WEST);
    }
    /** 東を描く */
    private void darwEast() {
        JLabel east = new JLabel("東");
        east.setBorder(BorderFactory.createLineBorder(Color.RED));
        cont.add(east, BorderLayout.EAST);

    }
    /** 南を描く */
    private void drawSouth() {
        JLabel south = new JLabel("南");
        south.setBorder(BorderFactory.createLineBorder(Color.YELLOW));
        cont.add(south, BorderLayout.SOUTH);

    }
}

この状態でもエラーが出ます。それは、Containerが宣言されているのは、メインメソッドだからです。

ならば、引数にContainerをわたしてやればよいのでわ?とおもうのでそのようにします。
<プログラムコード9>

package jp.zenryoku.swing;

import javax.swing.*;
import java.awt.*;

public class SwingMain {
    public static void main(String[] args) {
        SwingMain main = new SwingMain();
        JFrame frame = new JFrame("Window Test");
        frame.setLayout(new BorderLayout());

        Container cont = frame.getContentPane();
        main.darwNorth(cont);
        main.darwNCenter(cont);
        main.darwWest(cont);
        main.darwEast(cont);
        main.drawSouth(cont);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(500, 200));
        frame.setVisible(true);
    }

    /** 北を描く */
    private void darwNorth(Container cont) {
        cont.add(new JLabel("Hello World."), BorderLayout.NORTH);
    }
    /** 中央を描く */
    private void darwNCenter(Container cont) {
        JPanel panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.black));
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.add(new JLabel("*************"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("**        **"));
        panel.add(new JLabel("*************"));
        cont.add(panel, BorderLayout.CENTER);
    }
    /** 西を描く */
    private void darwWest(Container cont) {
        JLabel west = new JLabel("西");
        west.setBorder(BorderFactory.createLineBorder(Color.BLUE));
        cont.add(west, BorderLayout.WEST);
    }
    /** 東を描く */
    private void darwEast(Container cont) {
        JLabel east = new JLabel("東");
        east.setBorder(BorderFactory.createLineBorder(Color.RED));
        cont.add(east, BorderLayout.EAST);

    }
    /** 南を描く */
    private void drawSouth(Container cont) {
        JLabel south = new JLabel("南");
        south.setBorder(BorderFactory.createLineBorder(Color.YELLOW));
        cont.add(south, BorderLayout.SOUTH);

    }
}

実行結果は、下のようになります。

はじめに表示したものと同じです。そうならなくてはなりません。リファクタリングした後は、処理結果が全く同じになる事が大切です。
なので、単体テストクラスがないとリファクタリングするのがとても大変になリます。
逆に単体テストクラスがない場合はリファクタリングができません。

HTMLを読み込む

単純にHTMLファイルを作成して、それをSwingで表示するということができます。

<プログラムコード10>

    public static void main(String[] args) {
        SwingHtml main = new SwingHtml();
//        JEditorPane editorPane = new JEditorPane("text/html", HTML);
        JEditorPane editorPane = null;
        try  {
            URL url = new File("PracticeJava1/resources/012.html").toPath().toUri().toURL();
            editorPane = new JEditorPane(url);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
        editorPane.setEditable(false);
        editorPane.setPreferredSize(new Dimension(200, 150));
        JScrollPane scrollPane = new JScrollPane(editorPane);

        JPanel panel = new JPanel();

        panel.setLayout(new BorderLayout());
        panel.add(scrollPane, BorderLayout.CENTER);
        main.getContentPane().add(panel);
        main.setSize(new Dimension(400, 300));
        main.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        main.setVisible(true);
    }

JAXB関連のエラー対応

JAXB関連のエラー

エラーその1

Java Swingでの開発中に表題の様なエラーが出ました。

javax.swing.SingleSelectionModelはインタフェースです。JAXBはインタフェースを処理できません。

実装内容としては、次のようになっています。

  1. XMLファイルを読み込み
  2. 対応するデータをクラスで保持する⇔XMLファイルに出力

このエラーは、

実装部分としては下のようなコードです。
<処理部分>

public class InputSelector extends JPopupMenu {
    ・・・
    private void openCommandMenu(SelectMenu item) {
        List<Command> cmdList = play.getJob().getCommandList();
        for (Command cmd : cmdList) {
            cmd.addActionListener(this);
            add(cmd);
            addSeparator();
        }
    }
    ・・・
} 

add()しているCommandクラスをJMenuItemで拡張しています。※拡張したらエラーになりました。
JMenuItemを継承したのでエラーになりました。
なので、このクラスを保持するクラスを作成して、エラーを直しました。

@Data
public class Command extends JMenuItem implements ConfigIF {
    private String id;
    private String name;
    private Formula formula;

    public Command() {
    }

    public Command(String id, String name, Formula formula) {
        this.id = id;
        this.name = name;
        this.formula = formula;
    }
}

新たに作成したクラス
<CommandMenu>

import javax.swing.*;

public class CommandMenu extends JMenuItem {

    private Command command;

    public CommandMenu() {
    }

    public CommandMenu(Command cmd) {
        super(cmd.getName());
        this.command = cmd;
    }

    public Command getCommand() { return command; }
    public void setCommand(Command cmd) { this.command = cmd; }
}

余談1

JAXBでXMLからクラスを生成するとき、対象のクラスには、デフォルトコンストラクタが定義されていないとエラーになる。
<XMLの一部抜粋>

<config>
    <views>HP</views>
    <views>MP</views>
    <views>LV</views>
    <money>
        <key>NIG</key>
        <name>ニギ</name>
        <value>0</value>
    </money>
    <money>
        <key>GLD</key>
        <name>ゴールド</name>
        <value>1</value>
    </money>
    <element>
        <id>FIR</id>
        <name>火</name>
    </element>
    <element>
        <id>WIN</id>
        <name>風</name>
    </element>
    <element>
        <id>WAT</id>
        <name>水</name>
    </element>
    <element>
        <id>EAT</id>
        <name>土</name>
    </element>
</config>

<クラス>

package jp.zenryoku.rpg.data.config;

import lombok.Data;
/**
 * このゲームで使用する要素・エレメントを定義する。
 * 
 * @author (Takunoji)
 * @version (1.0)
 */
@Data
public class Element
{
    /** ID */
    private String id;
    /** 要素名 */
    private String name;

    /** デフォルトコンストラクタ */
    public Element() {
    }

    public Element(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

エラーその2

下の様なエラーが出ました。

javax.xml.bind.DataBindingException: com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
プロパティmoneyが存在しますが、@XmlType.propOrderに指定されていません

XML出力の順番を指定する「\@XmlType(propOrder=XXX)」の指定の中にフィールド変数の「money」が登録されていなかった。
下のコードは追加した後です。

対象のクラス

@XmlRootElement( name="player")
@XmlType(propOrder={"level", "name", "sex", "status", "items", "job", "state", "wepon", "armor", "money"})
@Data
public class Player implements Cloneable
{
    private static final boolean isDebug = true;
    /** 名前 */
    protected String name;
    /** レベル */
    protected int level;
    /** 性別 */
    protected SEX sex;
    /** ステータス */
    protected Map<String, Params> status;
    /** 所持アイテム */
    protected List<Item> items;
    /** 職業 */
    protected Job job;
    /** 状態 */
    protected State state;
    /** 武器 */
    protected Wepon wepon;
    /** 防具 */
    protected Armor armor;
    /** お金 */
    protected int money;
    ...
}

エラーその3

javax.xml.bind.DataBindingException: com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
プロパティmoneyが@XmlType.propOrderにありますが、そのようなプロパティは存在しません。noの誤りである可能性があります。

対象のクラス

もともとholdmneyの部分がmoneyになっていた。
propOrderの中身を正しく書き換えたら治った。

@Data
@XmlRootElement( name="monster")
@XmlType(propOrder={"no", "talk", "message", "type", "holdmoney", "exp"})
public class Monster extends Player implements Cloneable, Serializable
{
    /** 番号 */
    private int no;
    /** 話す */
    private boolean talk;
    /** 話すときのメッセージ */ 
    private String message;
    /** モンスタータイプ */
    private MonsterType type;
    /** お金 */
    private int holdmoney;
    /** 経験値 */
    private int exp;
    ...
}