JavaFX でスライドショーツールを作る

# 概要
2016年11月、「JavaFX のセッションをやるなら JavaFX でスライドショーツールを作ろう」と思って、ついやってしまったので、記事に残しておこうと思い立った次第です。

スライドショー(slideshow)というのは slide と show を組み合わせた造語で、1単語らしいです。[Wikipedia 日本語版の記事](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%89%E3%82%B7%E3%83%A7%E3%83%BC)に書いてありました。

## 先人たちの業績
Java Champion の櫻庭さんをはじめ、JavaFX を使ったスライドショー(プレゼンテーション)ツールの実装事例は数多く存在しています。

- [JavaFX でプレゼンツール](http://skrb.hatenablog.com/entry/2012/06/13/233944)
- [Markdownでスライドを作るためのツールをJavaFXで](http://qiita.com/opengl-8080/items/cf26e29371ce55fc4356)
- [twasyl/SlideshowFX](https://github.com/twasyl/SlideshowFX)
- [発表ネタ JJUG CCC 2017 Spring向け](http://www.torutk.com/projects/swe/wiki/%E7%99%BA%E8%A1%A8%E3%83%8D%E3%82%BF_JJUG_CCC_2017_Spring%E5%90%91%E3%81%91#%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%89)

## なぜ既にあるものを使わずに自分で作ってしまったのか？
単純に自分の使いやすい方式で実装してみたかったというだけです。

----
# スライドショーツールに求める機能

- 全画面で文字や画像を表示できる……Label を貼った Pane を Scene に入れて全画面表示？
- 複数のスライドを切り替え表示できる
- スライドの内容を何らかのマークアップで記述できる……Markdown？
- スライドを PDF 化できる……スクリーンショットを1つのファイルにする？

## どうやって実装するか？
いくつかやり方はあるかと思います。

- [reveal.js](http://lab.hakim.se/reveal-js/) や [Remark.js](https://remarkjs.com/#1) でコンテンツを作成し WebView で表示……これで作ると見栄えがよく、アレンジや PDF 化も楽です
- PDF 化したコンテンツを表示
- コンテンツを JavaFX の Pane に変換して表示

1はすでにやったことがあるので、今回は全部 JavaFX で作ることにしました。

----
# 実行環境

| Name    | version
|:---|:---
| Java SE | 1.8.0_121
| OS      | Windows 10
| Eclipse | 4.5
| Gradle  | 3.0

----
# 実装
ソースコード全体は下記 GitHub リポジトリに置きました。

- [slide_show(GitHub)](https://github.com/toastkidjp/slide_show/tree/qiita)

以下、ポイントを紹介します。

## コンテンツの記述
開発者がQiitaやGitHubで使い慣れているMarkdownでコンテンツを記述できるようにしました。自由度は低いですが、下記のように簡単にコンテンツを用意できます。

```markdown
# Title
toastkidjp

## このスライドについて1234567890123456789012345678901234567890
先日、勢いで java.io.File を使っていたコードを Path と Files で置き換えたので、既存のコードを書き換える、という観点で紹介します。すでに NIO2 をバリバリ使いこなしている方は、この記事を読んでも何の気付きもないと思います。

ちなみに JDK7 からの新しいファイル関連 API は NIO2(New I/O 2)と呼ぶそうで、無印は1.4の時代に追加された Channel や Charset のことを指すらしいです。

## コードも載せられる

` ``java
(1..100)
  .collect{it % 15 == 0 
           ? "FizzBuzz" 
           :  it % 3 ==0 
               ? "Fizz" 
               : it % 5 == 0
                   ? "Buzz" 
                   : it
                   }
  .forEach{print it + ", "}
` ``

# 以上
ありがとうございます。

```

なお、私は JavaCC を使えないので、Parser は Markdown ファイルのテキストを1行ずつ読んでいく幼稚な実装になっています……

## スライドの実装
コンテンツの高さがディスプレイの高さを超えた際にスクロールバーを表示させるため、ScrollPane を継承して、その Content に文字列や画像を表示する Node を集めた Pane を入れる実装にしました。

[すべてのスライドは List に入れ](https://github.com/toastkidjp/slide_show/blob/qiita/src/main/java/jp/toastkid/slideshow/Main.java#L470-L471)、 [1つの StackPane に重ねて描画](https://github.com/toastkidjp/slide_show/blob/qiita/src/main/java/jp/toastkid/slideshow/Main.java#L394-L399)し、[current のものだけを表示し、それ以外は非表示にしています](https://github.com/toastkidjp/slide_show/blob/qiita/src/main/java/jp/toastkid/slideshow/Main.java#L441)。

## Label の文字が切り捨てられる問題
デフォルトの Label オブジェクトを使うと、表示領域を超える長さの文字列は表示しきれずに切り捨てられてしまいます。

![ss6.png](https://qiita-image-store.s3.amazonaws.com/0/102004/84bf56d3-2a46-eaef-89bf-6cf54033abcd.png)

下記の設定を Label オブジェクトに実施しました。

```java:文字列が切り捨てられないようにする設定
label.setWrapText(true);
label.setMinHeight(Region.USE_PREF_SIZE);
```


![ss6_.png](https://qiita-image-store.s3.amazonaws.com/0/102004/9b101055-07a1-0fd7-432f-e1519a839cce.png)


## コンテンツの縦がディスプレイの高さを超える問題
Qiita スライドのやり方を参考にし、スライドが表示高を超えた場合は縦スクロールができるようにしました。具体的には Slide クラスで ScrollPane を継承し、コンストラクタ内で下記の設定をします。これにより、横スクロールを無効化し、必要な時だけ縦スクロールバーを出すことができます。

```java:横スクロールを無効化し、必要な時だけ縦スクロールバーを出す
this.setHbarPolicy(ScrollBarPolicy.NEVER);
this.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
```

![ss1.png](https://qiita-image-store.s3.amazonaws.com/0/102004/d9aaf005-50c4-e45b-0af5-30200f077898.png)
![ss3.png](https://qiita-image-store.s3.amazonaws.com/0/102004/8dc56855-5ed2-0d6b-fa93-16f32f076147.png)


なお、何枚も Slide オブジェクトを重ねているせいだと思いますが、マウスではスクロールバーを動かせなかったので、キーボードショートカットで下記のメソッドを呼び出すことで動かすようにしています。

```java:上方向にスクロール
setVvalue(getVvalue() - 0.20d);
```

```java:下方向にスクロール
setVvalue(getVvalue() + 0.20d);
```

## サブメニューと Main クラスの繋ぎ込み
操作のしやすさを考え、下記のようなサブメニューを用意しました。

![ss5.png](https://qiita-image-store.s3.amazonaws.com/0/102004/602e8dbe-f925-d56e-b618-132dbdfdae2b.png)

サブメニューの Controller と、具体的な機能を実装している Main クラスは別々のクラスです。サブメニューで受け付けたイベントを Main クラスに渡して処理させる必要があります。いくつかやり方はありまして、今回は [Reactor](https://github.com/reactor/reactor-core) というライブラリを使います。 

### メッセージクラスの用意
Main クラスに処理させたい内容を伝えるための各種メッセージクラスを用意します。各クラスを Message インターフェイスの実装として作ります。

### ユーザからのUIイベントを受け付け、Main にメッセージを送る
@FXML アノテーションをつけたメソッドでイベントオブジェクトを生成し、それを TopicProcessor を使って Main クラスに渡します。TopicProcessor はサブメニュークラスを初期化した時点でインスタンスを生成し、getter 経由で Main クラスにオブジェクトを渡して、Main クラス内で subscribe し、subscribe 内でイベントの実装型に応じた処理に分岐させます。

この実装は、 Android アプリ開発で使われる [EventBus](https://github.com/greenrobot/EventBus) という仕組みを参考にしました。

### 注意点
TopicProcessor から呼び出されるメソッドの実行スレッドが JavaFX のアプリケーションスレッドではないので、描画に関する処理を実行する場合は Platform.runLater を使う必要があります。

この仕組みを採用すると Main クラスがひたすら肥大化するのが難点です。今回はこれで妥協しています。

## スライドショーのPDFファイル化
大抵の場合、スライドショーのPDFを資料として配布する必要があるかと思われます。そういうときに備えてPDF化の機能も作っています。

```java:PDFBoxを使って、すべてのスライドをPDF化
try (final PDDocument doc = new PDDocument()) {
    final int width  = (int) stage.getWidth();
    final int height = (int) stage.getHeight();
    Interval.oneTo(slides.size()).each(i ->{
        moveTo(i);
        final long istart = System.currentTimeMillis();
        final PDPage page = new PDPage(new PDRectangle(width, height));
        try (final PDPageContentStream content = new PDPageContentStream(doc, page)) {
            content.drawImage(LosslessFactory.createFromImage(
                    doc, slides.get(current.get() - 1).generateImage(width, height)), 0, 0);
        } catch(final IOException ie) {
            LOGGER.error("Occurred Error!", ie);
        }
        doc.addPage(page);
        LOGGER.info("Ended page {}. {}[ms]", i, System.currentTimeMillis() - istart);
    });
    doc.save(new File(DEFAULT_PDF_FILE_NAME));
} catch(final IOException ie) {
    LOGGER.error("Occurred Error!", ie);
}
```

1枚ずつスライドをめくって、各スライドのキャプチャ画像を取得し、それらの画像を [PDFBox](https://pdfbox.apache.org/) というライブラリを使って1つの PDF ファイルにまとめています。

試しに動かしたところ、4枚のスライドを PDF 化するのに1秒かかっていました。

```
2017-02-16 23:32:30  INFO Main Ended page 1. 334[ms]
2017-02-16 23:32:31  INFO Main Ended page 2. 269[ms]
2017-02-16 23:32:31  INFO Main Ended page 3. 181[ms]
2017-02-16 23:32:31  INFO Main Ended page 4. 169[ms]
2017-02-16 23:32:31  INFO Main Ended generating PDF. 1032[ms]
```

ちなみに [Interval](http://www.eclipse.org/collections/javadoc/8.0.0/org/eclipse/collections/impl/list/Interval.html) というのは [Eclipse Collections](https://www.eclipse.org/collections/ja/) のクラスです。

実際の発表に備えて用意した PDF ファイルは Speaker Deck にアップしてあります。
https://speakerdeck.com/toastkidjp/jjug-ccc-2016-fall-number-ccc-l5

## サンプルコード表示
開発者のプレゼンテーションではソースコードを見せることが多いかと思われます。コードをそのまま Label で見せても見辛いので、ハイライト表示が可能な TextArea の実装である [RichTextFX](https://github.com/TomasMikula/RichTextFX) というライブラリ(の CodeArea)を使いました。

例として、 Groovy の FizzBuzz は下記のようにシンタックスハイライトを適用して表示できます。

![ss4.png](https://qiita-image-store.s3.amazonaws.com/0/102004/6531220b-ce54-546e-c8e4-bfbd3f1ff1f5.png)

# 課題

## フルスクリーン状態から直接アプリケーションを終了すると JVM がクラッシュする
原因がよくわかっておらず……ちなみに ```gradle run``` の場合は、似たようなエラーメッセージは出ますが正常終了します。

```:コンソールのメッセージ
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00000000737b584a, pid=15116, tid=0x000000000000246c
#
# JRE version: Java(TM) SE Runtime Environment (8.0_102-b14) (build 1.8.0_102-b14)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.102-b14 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# C  [glass.dll+0x2584a]
#
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# /path/to/slide_show/hs_err_pid15116.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.java.com/bugreport/crash.jsp
#
```

### 全文
https://gist.github.com/toastkidjp/1d216a9ad98a65dd0bc42552dfe99791


## ディスプレイサイズが変わると表示崩れを起こす
表示領域を可変にしているせいだとは思いますが……この辺を上手くやってくれる既存のツールは素晴らしいですね。解決にはJJUG CCC 2016 Fallで高橋さんの使ったディスプレイサイズを取得してフォントサイズを動的に変える手法を適用できないかなと考えていますが、まだ試せていません。

以前このツールを使った際は30分の休憩時間があったので、そこで表示崩れがないかを入念にチェックしていました。

----

# まとめ
以上、JavaFXを使ってスライドショーを作るやり方の1つを紹介致しました。まあ、普通に PowerPoint、あるいは LibreOffice を使った方が楽ですし安全ですし高機能です。Qiita スライドであれば記事としてそのまま公開できますし共有がしやすいです。今の時点では、機能が不足しすぎていて、使う理由のかなりの部分を自己満足が占めています。

ただ、JavaFX は Java だけでなく JVM で動く言語であれば利用できるらしいので、この記事が「少し変わったプレゼンテーションをやりたい」と考えている方々のお役に立てれば幸いです。

## リンク

### 使用したライブラリ
- [Reactor](https://github.com/reactor/reactor-core)
- [PDFBox](https://pdfbox.apache.org/)
- [RichTextFX](https://github.com/TomasMikula/RichTextFX)
- [Eclipse Collections](https://www.eclipse.org/collections/ja/)

### 参考にしたライブラリ
- [EventBus](https://github.com/greenrobot/EventBus)

## リポジトリ
- [slide_show(GitHub)](https://github.com/toastkidjp/slide_show/tree/qiita)