JavaFX でプレゼンツール

なぜか急に TwitterJavaFX のプレゼンツールが話題になっていたのでした。で、参考になればということで、自分のプレゼンツールについて書いてみたいと思います。

まずはじめに重要なのが、プレゼンツールとコンテンツは切り離して考えるべきということ。

コンテンツ側で表現しなくてはいけないことと、プレゼンツールで実装しなくてはいけないことをはっきりさせないといけません。

たとえば、文字を整列して表示することなんかはコンテンツ側でやればいいことです。たとえば、@kyon_mm さんは DSL でコンテンツを表現しようとしていますけど、文字列を整列させるのは DSL でやればいいんです。

DSL 書けなければ、最悪イメージでもいいわけですよね。お絵かきツールで文字並べればいいんですから。

@kyon_mm さんがプレゼンツールの要件 を書いてますけど。そこに書かれているほとんどの項目はコンテンツ側の要件であって、プレゼンツールの要件ではないと思うのです。

じゃあ、何がプレゼンツールに必要なのか。

最低限の要件は、「マウスクリックやキーボード入力でページを切り替える」ということだと思います。

後は、プレゼンツールで DSL を使うのであれば、その DSL をパースして表示するという処理を追加すれば OK です。

ということで、まず最低限のプレゼンツールを作ってみましょう。

イメージを切り替えるプレゼンツール

イメージを切り替えるにはどうすればいいのかを考えて見ましょう。

JavaFX はすべての描画要素はシーングラフにで表されます。シーングラフは起動時にすべて生成しておくのでもかまわないのですが、動的に描画要素を追加・削除することもできます。

ということは、マウスクリックされたら、イメージをシーングラフに追加して、その前のイメージを削除すればいいはずです。

たとえば、イメージを pages という配列に、今表示しているページの番号を pageIndex に保持させるとさせましょう。

ここでは page1.jpg、page2.jpg、page3.jpg の 3 つのイメージを切り替えます。

public class SimpleImagePresenter extends Application {
    // 表示するページ
    private String[] pages = {
        "page1.jpg",
        "page2.jpg",
        "page3.jpg"
    };

    // 現在表示しているページ番号
    private int pageIndex;
    
    // シーングラフのルート要素
    private Group root;

後は、ページの切り替えの部分だけです。マウスクリックされたら、goForward メソッドがコールされるとしましょう。

    // ページを進める
    private void goForward() throws IOException {
        // 次のページをロードして、表示する
        String imageURL 
            = getClass().getResource(pages[pageIndex]).toString();
        Image image = new Image(imageURL);
        ImageView page = new ImageView(image);
        root.getChildren().add(page);
        
        // 前のページが存在している場合は、削除する
        if (root.getChildren().size() > 1) {
            root.getChildren().remove(0);
        }
        
        // ページ番号を進める
        // 最後までいったら最初に戻す
        pageIndex++;
        if (pageIndex >= pages.length) {
            pageIndex = 0;
        }
    }

クリックされたら、イメージをロードして、シーングラフに追加します。そして、現在表示しているページがあれば、シーングラフから削除します。if 文で表示しているページがあるかないかを調べているのは、一番始めは表示しているページがないからです。

そして、最後にページ番号を進めます。

これで、基本的な部分はできました。後は Scene オブジェクトなどを生成したりする部分だけです。クラス全体を次に示しておきます。

package net.javainthebox.jfx.simplepresenter;

import java.io.IOException;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class SimpleImagePresenter extends Application {
    // 表示するページ群
    private String[] pages = {
        "page1.jpg",
        "page2.jpg",
        "page3.jpg"
    };
    
    // 現在表示しているページ番号
    private int pageIndex;
    
    // シーングラフのルート要素
    private Group root;
    
    @Override
    public void start(Stage stage) throws Exception {
        // ステージを透明にする
        stage.initStyle(StageStyle.TRANSPARENT);
        
        root = new Group();
        root.setOnMouseClicked(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                try {
                    // マウスクリックされたら、次のページへ
                    goForward();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        });
        
        Scene scene = new Scene(root, 800, 600);
        scene.setFill(null);
        stage.setScene(scene);
        stage.show();
        
        // 最初のページを表示する
        goForward();
    }
    
    // ページを進める
    private void goForward() throws IOException {
        // 次のページをロードして、表示する
        String imageURL 
            = getClass().getResource(pages[pageIndex]).toString();
        Image image = new Image(imageURL);
        ImageView page = new ImageView(image);
        root.getChildren().add(page);

        // 前のページが存在している場合は、削除する
        if (root.getChildren().size() > 1) {
            root.getChildren().remove(0);
        }
        
        // ページインデックスを進める
        // 最後までいったら最初に戻す
        pageIndex++;
        if (pageIndex >= pages.length) {
            pageIndex = 0;
        }
    }
    
    public void main(String... args) {
        launch(args);
    }
}

たかだか、80 行でプレゼンツールができてしまいました。

もちろん、例外処理はいい加減ですし、ページを戻ったりすることもできませんが、これでも十分使えます。

ページ遷移に動きをつける

単にページを切り替えるだけというのはちょっと寂しいので、ページ切り替えの時にアニメーションを付け加えてみましょう。

ここでは、新しいページが左からスライドしてきて、今表示しているページは右にスライドするようにしてみます。

新しいページを next、今のページを present として、アニメーションを行うメソッドを作成しました。

    // ページ遷移アニメーション
    private void translatePage(Node next, final Node present) {
        // 新しいページを右からスライドさせるアニメーション
        TranslateTransition slidein 
                = new TranslateTransition(new Duration(1000));
        slidein.setNode(next);
        slidein.setFromX(WIDTH);
        slidein.setToX(0);
        slidein.play();
        
        if (present != null) {
            // 現在表示しているページがあれば、
            // 左にスライドさせる
            TranslateTransition slideout 
                    = new TranslateTransition(new Duration(1000));
            slideout.setNode(present);
            slideout.setToX(-WIDTH);
            slideout.setOnFinished(new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent event) {
                    // アニメーションが終了したら、
                    // シーングラフから削除する
                    root.getChildren().remove(present);
                }
            });
            slideout.play();
        }
    }

ページを移動させるのは TranslateTransition クラスを使っています。1 秒で、ページの幅分の移動を行います。

意外と思われるかもしれませんが、アニメーションを同時に行うには ParallelTransition クラスを使わなくても、個々のアニメーションを play() すれば大丈夫です。

もちろん、ちゃんと同期させたい場合は ParallelTransition クラスを使うべきですが、ここではそこまで厳密なアニメーションではないので、これで十分です。

現在表示しているページは、左側にアニメーションした後、シーングラフから削除しています。これを行うために、アニメーションが終了した時にコールされるコールバックメソッドを setOnFinished メソッドで設定します。

present が final なのは、この無名クラスの内部でアクセスするためです。

では、goForward メソッドで translatePage メソッドを呼ぶようにしましょう。

    // ページを進める
    private void goForward() throws IOException {
        // 次のページをロードして、シーングラフに追加する
        String imageURL 
            = getClass().getResource(pages[pageIndex]).toString();
        Image image = new Image(imageURL);
        ImageView next = new ImageView(image);
        root.getChildren().add(next);
        
        // 前のページが存在していれば、presentに代入
        Node present = null;
        if (root.getChildren().size() > 1) {
            present = root.getChildren().get(0);
        }
        
        // ページ遷移のアニメーションを行う
        translatePage(next, present);
        
        // ページインデックスを進める
        // 最後までいったら最初に戻す
        pageIndex++;
        if (pageIndex >= pages.length) {
            pageIndex = 0;
        }
    }

実行すると下のようになります。

どうですか? 結構いけていると思いませんか。

ただ、これでは静的なページしか扱うことができません。つまり、ページの中で動きがあるようなことはできないのです。

たとえば、箇条書きをマウスクリックで順々に表示するということもできません。

これについては、次回やることにしましょう。