JavaFX でプレゼンツール その 2

前回の続きです。

前回は、プレゼンツールに最低限な機能として、マウスクリック or キー入力によるページが切り替わることといいました。そして、イメージを切り替えるプレゼンツールを作ったところまで行いました。

しかし、これだけだと、ページを動的に変化させることができません。

そこで、今回はページを切り替えることに加え、ページ内での動きを実装していくことにします。

ところが、ここで困ったことが起きます。というのも、イメージだけでは動きを持たせることがなかなか難しいのです。

1 枚のページを複数のイメージで表すこともできますが、そのイメージをすべてプレゼンツールで保持させるのは本末転倒なような気がします。

前回、プレゼンツールとコンテンツを分けて考えるべきといいましたが、これはコンテンツ側で表さなくてはいけないことだと思うわけです。

じゃあ、コンテンツはどうやって表しましょう。これもいろいろとやり方があると思うのですが、今回は FXML を使ってコンテンツを表すことにします。

これ以外にも DSL を使ったり、独自の XML を使うなど、いろいろやり方はあると思います。たとえば、Prezi は独自の XML を使っているようです。

FXML でコンテンツを表す

FXML は JavaFX の UI を記述するための XML なので、当然プレゼンツールのような用途にも使用することが可能です。


たとえば、イメージを貼っただけの FXML の例を以下に示します。

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>

<AnchorPane id="AnchorPane" prefHeight="600.0" prefWidth="800.0" xmlns:fx="http://javafx.com/fxml">
  <children>
    <ImageView fitHeight="600.0" fitWidth="800.0" preserveRatio="true">
      <image>
        <Image url="@page1.jpg" preserveRatio="false" smooth="false" />
      </image>
    </ImageView>
  </children>
</AnchorPane>

イメージを貼るには ImageView クラスを使用します。Scene Builder で FXML を作成する場合、デフォルトのコンテナが AnchorPane クラスになるので、そこに ImageView クラスを貼っています。

で、これを読み込ませるためには、前回説明した goForward メソッドを改造するだけです。

    // ページを進める
    private void goForward() throws IOException {
        URL url = getClass().getResource(pages[pageIndex]);

        // 次のページのFXMLをロードする
        Node next = FXMLLoader.load(url);

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

前回は Image クラスと ImageView クラスで直接イメージをロードしていましたが、FXML の場合は FXMLLoader クラスの load メソッドを使用します。

これで前回のプレゼンツールと同様のことができます。

コントローラを使って、動きをだす

FXML を使ったとしても、ロードの部分が変わるだけです。しかし、これでは前回と同じ事しかできません。

じゃあ、どうするか。

答えはコントローラです。

FXML は基本的には GUI の構造だけを表すだけで、そこでの動きやモデルとのつなぎはコントローラに Java で記述するわけです。

で、ここでもページの動きをコントローラに記述することにしましょう。

マウスクリックはメインとなる SimplePresenter クラスでイベント処理するので、ページの動きは SimplePresenter クラスがコントローラに対してキックするという流れにしたいと思います。

ここではコントローラクラスを PageController インタフェース、ページを動きを行わせるメソッドを doAction メソッドとします。

そして、もしページに新たな動きがない場合は doAction メソッドの戻り値を false にするということにします。

これをシーケンス図で表したのが下の図です。

f:id:skrb:20120616013754p:image

このへんはいろいろと、設計が考えられると思うのですが、今回はこれで行きましょう。

コントローラクラスは特にインタフェースを実装しているわけでもないので (実際は Initializable インタフェースを実装している場合が多いのですが、必須ではないです)。ここでは SimplePresenter クラスからコールできるように PageController インタフェースを使用します。

package net.javainthebox.jfx.simplepresenter;

public interface PageController {
    public boolean doAction();
}

では、ここでは例としてデフォルトでイメージが表示されているページに後から、文字を浮きだたせるということをやってみます。

まず FXML の page1.fxml です。

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>

<AnchorPane id="AnchorPane" prefHeight="600.0" prefWidth="800.0" 
            xmlns:fx="http://javafx.com/fxml" 
            fx:controller="net.javainthebox.jfx.simplepresenter.page1">
  <children>
    <ImageView fx:id="p0" fitHeight="600.0" fitWidth="800.0" preserveRatio="true">
      <image>
        <Image url="@page1.jpg" preserveRatio="false" smooth="false" />
      </image>
    </ImageView>
    <Text id="p1" fx:id="p1" fill="WHITE" layoutX="564.0" layoutY="525.0"
                  stroke="WHITE" text="海へ...">
      <font>
        <Font size="64.0" />
      </font>
    </Text>
  </children>
</AnchorPane>

コントローラクラスは fx:controller 属性で指定します。

コントローラとやりとりを行う要素は fx:id 属性で名前をつけておきます。ここでは ImageView と、後から表示する Text に名前を付加しました。イメージが p0、テキストを p1 としています。

では、コントローラクラスの page1.java です。

package net.javainthebox.jfx.simplepresenter;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.animation.FadeTransition;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.image.ImageView;
import javafx.scene.text.Text;
import javafx.util.Duration;

public class page1 implements PageController, Initializable {
    @FXML
    ImageView p0;
    
    @FXML
    Text p1;
    
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        p1.setOpacity(0.0);
    }    

    @Override
    public boolean doAction() {
        if (p1.getOpacity() == 0.0) {
            FadeTransition fadein = new FadeTransition(new Duration(1000L));
            fadein.setNode(p1);
            fadein.setToValue(1.0);
            fadein.play();

            return true;
        } else {
            return false;
        }
    }
}

FXML でシーングラフに貼られると、FXML のすべての要素がシーングラフに追加され、表示されます。

ここでは、後から表示を行うため、デフォルトでは p1 を透明にしておきます。このために起動時にコールされる、initialize メソッドで p1.setOpacity(0.0); をしています。

でも、描画要素がいっぱいあったら、いちいちこうやって書くのは結構めんどう。描画要素がコレクションで管理できれば、こういうベタなことを書かずループで回せるんですけどねぇ。

マウスクリックがあったら doAction メソッドがコールされます。

onAction メソッドでは、FadeTransition クラスを使って、透明な状態から非透明に文字列を表示させます。

そして、SimplePresenter クラスは、今までマウスがクリックされた時、goForward メソッドでページを遷移させていました。これに対し、変更点はマウスクリックされたら、まずは PageController クラスの doAction メソッドをコールする、戻り値が false だったらページを切り替えすることです。

では、SimplePresenter クラスにも doAction メソッドを作りましょう。マウスクリックされたら、この doAction メソッドがコールされます。

    private void doAction() throws IOException {
        // コントローラ側でページ内の動きをつける
        // これ以上アクションがなければ、falseが戻るので
        // ページを進める
        if (!presentController.doAction()) {
            goForward();
        }
    }

コントローラの presentController 変数は FXML ロード時に取得するようにします。

    // 現在表示しているページのコントローラ
    private PageController presentController;

    // ページを進める
    private void goForward() throws IOException {
        // 次のページをロードして、表示する
        URL url = getClass().getResource(pages[pageIndex]);
        FXMLLoader loader  = new FXMLLoader(url);
        Node next = (Node)loader.load();
        root.getChildren().add(next);

        // ページのコントローラを取得
        presentController = loader.getController();
        
        // 前のページが存在していれば、presentに代入
        Node present = null;
        if (root.getChildren().size() > 1) {
            present = root.getChildren().get(0);
        }
        
        // ページ遷移のアニメーションを行う
        translatePage(next, present);
        
        // ページインデックスを進める
        // 最後までいったら最初に戻す
        pageIndex++;
        if (pageIndex >= pages.length) {
            pageIndex = 0;
        }
    }

コントローラは FXMLLoader オブジェクトから取得できるのですが、そのためには static メソッドの FXMLLoader#load メソッドでは取得できません。

そのために、まず FXMLLoader オブジェクトを生成してから、load メソッドをコールするようにしています。

さて、これで実行してみましょう。実行結果を次に示します。

ここまでくるとかなり普通のプレゼンツールに近くなりますね。

ちなみに 3 ページ目に、ちょっと違う動きを入れてみました。プレゼンの最後でツールが消えていくとアニメーションです。

page3.fxml は次のようになります。

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>

<AnchorPane id="AnchorPane" fx:id="pane" prefHeight="600.0" prefWidth="800.0" xmlns:fx="http://javafx.com/fxml" fx:controller="net.javainthebox.jfx.simplepresenter.page3">
  <children>
    <ImageView fx:id="p0" fitHeight="600.0" fitWidth="800.0" preserveRatio="true">
      <image>
        <Image url="@page3.jpg" preserveRatio="false" smooth="false" />
      </image>
    </ImageView>
    <Text fx:id="p1" fill="WHITE" layoutX="551.0" layoutY="321.0" stroke="WHITE" text="日は西に">
      <font>
        <Font size="64.0" fx:id="x1" />
      </font>
    </Text>
    <Text fx:id="p2" fill="WHITE" font="$x1" layoutX="405.0" layoutY="395.0" stroke="WHITE" text="そして日は沈む" />
  </children>
</AnchorPane>

そして、コントローラは...

public class page3 implements PageController, Initializable {

    @FXML
    AnchorPane pane;

    @FXML
    ImageView p0;
    
    @FXML
    Text p1;
    
    @FXML
    Text p2;
    
    private int index = 0;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        p1.setOpacity(0.0);
        p2.setOpacity(0.0);
    }

    @Override
    public boolean doAction() {
        switch (index) {
            case 0:
                FadeTransition fadein 
                    = new FadeTransition(new Duration(1000L));
                fadein.setNode(p1);
                fadein.setToValue(1.0);
                fadein.play();
                index++;

                return true;
            case 1:
                FadeTransition fadein2 
                    = new FadeTransition(new Duration(1000L));
                fadein2.setNode(p2);
                fadein2.setToValue(1.0);
                fadein2.play();
                
                index++;

                return true;
            case 2:
                Rectangle rect = new Rectangle(0.0, 0.0, 
                                               SimplePresenter.WIDTH,
                                               SimplePresenter.HEIGHT);
                rect.setFill(null);
                pane.getChildren().add(rect);
                
                FillTransition fill
                        = new FillTransition(new Duration(5000L), rect,
                                             Color.rgb(0, 0, 0, 0.0),
                                             Color.web("#000033"));
                
                FadeTransition fadeout 
                    = new FadeTransition(new Duration(1000L));
                fadeout.setNode(pane);
                fadeout.setToValue(0.0);
                
                SequentialTransition sequential 
                    = new SequentialTransition(fill, fadeout);
                sequential.play();
                
                index++;
                
                return true;
            default:
                return false;
        }
    }
}

1 回目と 2 回目のマウスクリックでは、文字列をフェードインさせるアニメーションを使っています。
3 回目のマウスクリックでは全体を覆うような黒い四角形をフェードインさせています。そして、その後、フェードアウトですべて見えなくなるというものです。

このために、最後に何もなくなってしまっているわけです。

ここでは四角形は FXML に記述せずに、コントローラで生成してコンテナに貼っています。FXML にすべてを書かなくてもコントローラに書いてもいいわけですから、ここらへんは柔軟にすればいいと思います。

あっ、でも表示が全部フェードアウトしても、プロセスは残っているので、キルしてあげなくちゃいけませんねww

さて、今回は FXML でやりましたけど、私が実際にプレゼンで使うツールでは SVG を使用しています。

というのも、やっぱり Scene Builder はお絵かきツールにはならないんですよね。せめて、Flash Professional ぐらいの機能があればいいのですが.... 今は Illustrator で書いて、SVG に変換し、それを読み込むということをしています。

ここまでのプレゼンテーションツールは GitHub で公開しました。

https://github.com/skrb/SimplePresenter