カスタムブラウザを作る

6/16 に JavaFX 2.0 勉強会を行いますが、そこでミニハンズオンをしようと思っています。以下、ハンズオンで使用する資料です。

JavaFX ユーザグループ 第 5 回勉強会

今回はカスタムブラウザを作っていきます。NetBeans 7 + JavaFX Plugin を使用していますが、Eclipse + JavaFX SDK でも、JavaFX SDK 単体でもかまいません。

完成したソースは GitHub に置いてあります。

https://github.com/skrb/CustomBrowser

プロジェクトを作成

本節は NetBeans 用の説明なので、NetBeans を使わないのであれば飛ばしてください。

まず、NetBeans でプロジェクトを作成します。

左側のプロジェクトの部分で左クリックし、[新規プロジェクト] を選びます。


f:id:skrb:20110615131010j:image


プロジェクトのカテゴリは [Java]、プロジェクトの種類は [Java FX Application] を選択して、[次へ]。

f:id:skrb:20110615131153j:image


次にプロジェクト名、プロジェクトの場所などを設定します。ここでは CustomBrowser というプロジェクト名にしてみました。

f:id:skrb:20110615131515j:image

[完了] をクリックするとプロジェクトが作成されます。

メインクラスを作成にしたので、次に示すプログラムが生成されます。

package custombrowser;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class CustomBrowser extends Application {

    public static void main(String[] args) {
        Application.launch(CustomBrowser.class, args);
    }
    
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Hello World");
        Group root = new Group();
        Scene scene = new Scene(root, 300, 250, Color.LIGHTGREEN);
        Button btn = new Button();
        btn.setLayoutX(100);
        btn.setLayoutY(80);
        btn.setText("Hello World");
        btn.setOnAction(new EventHandler<ActionEvent>() {

            public void handle(ActionEvent event) {
                System.out.println("Hello World");
            }
        });
        root.getChildren().add(btn);        
        primaryStage.setScene(scene);
        primaryStage.setVisible(true);
    }
}

これで、まず実行してみましょう。主プロジェクトであれば F6 で起動します。主プログラムでなければ、プロジェクトを右クリックし、[実行] を選択します。

すると、緑の背景にボタンが表示されます。

f:id:skrb:20110530165958j:image

ブラウザの表示

では、このソースに手を入れていきます。

まずは、いらない部分をバサッとカットします。下のグレーの部分を削除します。

public class CustomBrowser extends Application {

    public static void main(String[] args) {
        Application.launch(CustomBrowser.class, args);
    }
    
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Hello World");
        Group root = new Group();
        Scene scene = new Scene(root, 300, 250, Color.LIGHTGREEN);
        Button btn = new Button();
        btn.setLayoutX(100);
        btn.setLayoutY(80);
        btn.setText("Hello World");
        btn.setOnAction(new EventHandler() {

            public void handle(ActionEvent event) {
                System.out.println("Hello World");
            }
        });
        root.getChildren().add(btn);        
        primaryStage.setScene(scene);
        primaryStage.setVisible(true);
    }
}

削除した結果はこうなります。
Stage のタイトルと、Scene のサイズも変更してあります。

public class CustomBrowser extends Application {

    public static void main(String[] args) {
        Application.launch(CustomBrowser.class, args);
    }
    
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Custom Browser");
        Group root = new Group();
        Scene scene = new Scene(root, 800, 600);

        primaryStage.setScene(scene);
        primaryStage.setVisible(true);
    }
}

これで単にウィンドウだけが表示されるソースになりました。

では、ここにブラウザのコントロールである WebView オブジェクトを追加します。

    public void start(Stage primaryStage) {
        primaryStage.setTitle("Custom Browser");
        Group root = new Group();
        Scene scene = new Scene(root, 800, 600);
        
        WebEngine engine = new WebEngine("http://google.co.jp/");
        WebView view = new WebView(engine);
        
        root.getChildren().add(view);

        primaryStage.setScene(scene);
        primaryStage.setVisible(true);
    }

HTML のパースを行うのが WebEngine クラス、描画が WebView クラスという役割分担になっています。

では、実行してみましょう。

f:id:skrb:20110615142505j:image

URL を入力する (イベント処理)

これだけで、ブラウザを操作することはできますが、Java から任意の URL を描画させることはできません。そこで、テキストボックスとボタンを追加して、URL の入力とロードをできるようにします。

まず、その前にノードを配置するために、レイアウトを考えます。

ここでは縦方向にノードを配置する VBox クラス、そして、水平方向にノードを配置する HBox クラスを使用します。

VBox には HBox と WebView を配置し、HBox に TextBox と Button を配置します。

f:id:skrb:20110615152148j:image

    public void start(Stage primaryStage) {
        primaryStage.setTitle("Custom Browser");
        
        Group root = new Group();
        Scene scene = new Scene(root, 800, 600);

        // 垂直方向にレイアウトするコンテナ
        VBox vbox = new VBox(10);
        vbox.setLayoutY(10);

        // 水平方向にレイアウトするコンテナ
        HBox hbox = new HBox(10);
        hbox.setAlignment(Pos.CENTER);
        
        // テキスト入力
        TextBox box = new TextBox(40);
        hbox.getChildren().add(box);
        
        // ボタン
        Button button = new Button("Load");
        hbox.getChildren().add(button);
        
        // HBoxをVBoxに貼る
        vbox.getChildren().add(hbox);
        
        // ブラウザ
        WebEngine engine = new WebEngine("http://google.co.jp/");
        WebView view = new WebView(engine);
        
        // WebViewをVBoxに貼る
        vbox.getChildren().add(view);
        
        // VBox をルートに貼る
        root.getChildren().add(vbox);

        primaryStage.setScene(scene);
        primaryStage.setVisible(true);
    }

これだけだと入力できるだけなので、イベント処理を加えます。

JavaFX のイベントはリスナではなく、EventHandler を使用します。Swing/AWT ではイベントの種類に対応したリスナがいろいろありますが、JavaFX はすべて EventHandler です。

その代わり、ジェネリクスでイベントを指定します。

EventHandler の登録を行うメソッドはイベント種類によって異なります。たとえば、ボタンをクリックした時には setOnAction メソッドで EventHandler を登録します。

試しに、ボタンをクリックしたら標準出力に Click! と出力するようなコードを記述してみましょう。

        // ボタン
        Button button = new Button("Load");
        button.setOnAction(new EventHandler<ActionEvent>() {
            public void handle(ActionEvent event) {
                // イベント処理
                System.out.println("Click!");
            }
        });

では、テキストボックスに入力した URL を WebView で表示できるようにしてみましょう。

なお、無名クラスで扱いやすくするために、box 変数と engine 変数をフィールドで定義するようにしておきました。

        Button button = new Button("Load");
        button.setOnAction(new EventHandler<ActionEvent>() {
            public void handle(ActionEvent event) {
                // テキストボックスから取得した文字列を
                // WebEngine でロードする
                String url = box.getText();
                engine.load(url);
            }
        });

実行して、適当な URL を入力してボタンをクリックしてみてください。

f:id:skrb:20110615154317j:image

では、この段階のソースを示しておきます。

package custombrowser;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextBox;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class CustomBrowser extends Application {
    private TextBox box;
    private WebEngine engine;
    
    public static void main(String[] args) {
        Application.launch(CustomBrowser.class, args);
    }
    
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Custom Browser");
        
        Group root = new Group();
        Scene scene = new Scene(root, 800, 600);

        // 垂直方向にレイアウトするコンテナ
        VBox vbox = new VBox(10);
        vbox.setLayoutY(10);

        // 水平方向にレイアウトするコンテナ
        HBox hbox = new HBox(10);
        hbox.setAlignment(Pos.CENTER);
        
        // テキスト入力
        box = new TextBox(40);
        hbox.getChildren().add(box);
        
        // ボタン
        Button button = new Button("Load");
        button.setOnAction(new EventHandler<ActionEvent>() {
            public void handle(ActionEvent event) {
                // テキストボックスから取得した文字列を
                // WebEngine でロードする
                String url = box.getText();
                engine.load(url);
            }
        });
        
        hbox.getChildren().add(button);
        
        // HBoxをVBoxに貼る
        vbox.getChildren().add(hbox);
        
        // ブラウザ
        engine = new WebEngine();
        WebView view = new WebView(engine);
        
        // WebViewをVBoxに貼る
        vbox.getChildren().add(view);
        
        // VBox をルートに貼る
        root.getChildren().add(vbox);

        primaryStage.setScene(scene);
        primaryStage.setVisible(true);
    }
}

エフェクト

JavaFX ではぼかしやエンボスなど、さまざまなエフェクトを使用することができます。

エフェクトは Effect クラスのサブクラスと実装されており、ノードに設定する場合には setEffect メソッドで指定します。

ここでは反射のエフェクトを使ってみましょう。反射のエフェクトは Reflection クラスで行います。

        WebView view = new WebView(engine);
        // サイズの指定
        view.setPrefSize(800, 400);
        // 反射のエフェクト
        view.setEffect(new Reflection());

WebViewオブジェクトはサイズを指定しないと、余っている領域をすべて使用してしまいます (レイアウトのコンテナによります)。そこで、setPrefSize メソッドでサイズを指定しておきます。

ちなみに、setPrefSize メソッドは、Swing でいうところの setPreferredSize メソッドに対応します。

そして、setEffect メソッドで Reflection オブジェクトを指定します。


f:id:skrb:20110615160035j:image


簡単ですね。

ページのロードに応じた処理 (非同期処理)

Web ページ内で、他のページに遷移すると、今のままではテキストボックスが更新されません。そこで、ページを読み込み終ったら、テキストボックスを更新させるようにしてみましょう。

Web ページのロードは Task クラスを使用して非同期で行われます。ただし、Task クラスは今後他のクラスに置き換わることが決まっていますので、ご了承ください。

ロートのタスクは WebEngine クラスの getLoadTask メソッドで取得できます。

        // Webページのロードのタスク
        Task task = engine.getLoadTask();

NetBeans では Task に線が引かれてしまいますが、これは Duprecated だということを表しています。

Task クラスは非同期に行う処理の経過によってイベントを投げることができます。非同期処理が終了した時のイベント処理は setOnDone メソッドで登録することができます。

        // Webページのロード完了時のイベント処理を登録
        task.setOnDone(new EventHandler<TaskEvent>() {
            public void handle(TaskEvent event) {
                // WebEngineからURLを取得し
                //テキストボックスに反映させる
                String url = engine.getLocation();
                if (url != null && !url.isEmpty()) {
                    box.setText(url);
                }
            }
        });

これで、ページのロードが完了したら、テキストボックスの値が更新されます。

たとえば、Google で JavaFX を検索すると、次のようになります。

f:id:skrb:20110615165057j:image

テキストボックスに URL のパラメータが表示されていることが分かります。

アニメーション

最後にアニメーションを付加してみましょう。

アニメーションは時間軸を表す Timeline クラスと、ある時間におけるオブジェクトの状態を示す KeyFrame クラスで表します。

Timeline クラスを使えば、さまざまなアニメーションが記述できるのですが、如何せん記述がめんどうになりがちです。

そこで、よく使うアニメーションは簡単に使えるようになっています。たとえば、移動は TranslateTransition クラスで行うことができます。

ここでは、TranslateTransition クラスを使用して、読み終わったページを左に移動させ、ロードしたページを右から移動させてみます。

アニメーションは前節のロードタスクを使用して、ページのロードが開始したら左にはけさせ、ページのロードが完了したら右から入らせるようにしました。

まずは、読み終わったページを左に移動させる処理です。

        // Webページのロード開始時のイベント処理を登録
        task.setOnStarted(new EventHandler<TaskEvent>() {
            public void handle(TaskEvent event) {
                // 現在のページを左方向に800移動させる
                TranslateTransition transition 
                        = new TranslateTransition(new Duration(500));
                        
                transition.setNode(view);
                transition.setFromX(0);
                transition.setToX(-800);
                transition.play();
            }
        });

TranslateTransition オブジェクトは、時間を指定して生成します。時間には Duration クラスを使用します。ここで指定した時間がアニメーションの時間になります。

setNode メソッドでアニメーションを行う対象のノードを指定します。

setFromX メソッドでアニメーションの開始点、setToX メソッドで終了点を指定します。0 は現在位置を示しているので、現在位置から左方向 (x軸の負の方向) に 800 移動させます。

同じように setFromY/setToY メソッドもありますが、今回は水平方向に移動させるので使用していません。

最後に play メソッドをコールすれば移動します。

同じようにロードが完了した時のアニメーションです。

        // WebViewの初期位置をアニメーションの開始位置に
        // あわせておく
        view.setTranslateX(800);

        // Webページのロード完了時のイベント処理を登録
        task.setOnDone(new EventHandler<TaskEvent>() {
            public void handle(TaskEvent event) {
                // 読み込んだページを右から800移動させる
                TranslateTransition transition 
                        = new TranslateTransition(new Duration(500));
                transition.setNode(view);
                transition.setFromX(800);
                transition.setToX(0);
                transition.setInterpolator(Interpolator.EASE_BOTH);
                transition.play();
                
                // WebEngineからURLを取得し
                //テキストボックスに反映させる
                String url = engine.getLocation();
                if (url != null && !url.isEmpty()) {
                    box.setText(url);
                }
            }
        });

このアニメーションは開始点が 800 なのですが、アニメーションが開始するまでは 0 の地点にいます。したがって、一瞬 0 の地点に表示され、その後 800 の地点に移動するという変な動きになってしまいます。

そこで、WebView オブジェクトの初期位置も 800 ずらしておきます。

これで完成です。ページを遷移させると、アニメーションが行われます。

f:id:skrb:20110615174125j:image

最後に完成したソースを示しておきます。

package custombrowser;

import javafx.animation.Interpolator;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.async.Task;
import javafx.async.TaskEvent;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextBox;
import javafx.scene.effect.Reflection;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.util.Duration;

public class CustomBrowser extends Application {
    private TextBox box;
    private WebEngine engine;
    private WebView view;
    
    public static void main(String[] args) {
        Application.launch(CustomBrowser.class, args);
    }
    
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Custom Browser");
        
        Group root = new Group();
        Scene scene = new Scene(root, 800, 600);

        // 垂直方向にレイアウトするコンテナ
        VBox vbox = new VBox(10);
        vbox.setLayoutY(10);

        // 水平方向にレイアウトするコンテナ
        HBox hbox = new HBox(10);
        hbox.setAlignment(Pos.CENTER);
        
        // テキスト入力
        box = new TextBox(40);
        hbox.getChildren().add(box);
        
        // ボタン
        Button button = new Button("Load");
        button.setOnAction(new EventHandler<ActionEvent>() {
            public void handle(ActionEvent event) {
                // テキストボックスから取得した文字列を
                // WebEngine でロードする
                String url = box.getText();
                engine.load(url);
            }
        });
        
        hbox.getChildren().add(button);
        
        // HBoxをVBoxに貼る
        vbox.getChildren().add(hbox);
        
        // ブラウザ
        engine = new WebEngine();
        view = new WebView(engine);
        // サイズの指定
        view.setPrefSize(800, 400);
        // 反射のエフェクト
        view.setEffect(new Reflection());
        
        // WebViewをVBoxに貼る
        vbox.getChildren().add(view);
        
        // VBox をルートに貼る
        root.getChildren().add(vbox);

        primaryStage.setScene(scene);
        primaryStage.setVisible(true);
        
        initLoadTask();
    }
    
    private void initLoadTask() {
        // Webページのロードのタスク
        Task task = engine.getLoadTask();

        view.setTranslateX(800);
        
        // Webページのロード開始時のイベント処理を登録
        task.setOnStarted(new EventHandler<TaskEvent>() {
            public void handle(TaskEvent event) {
                // 現在のページを左方向に800移動させる
                TranslateTransition transition 
                        = new TranslateTransition(new Duration(500));
                transition.setNode(view);
                transition.setFromX(0);
                transition.setToX(-800);
                transition.setInterpolator(Interpolator.EASE_BOTH);
                transition.play();
            }
        });

        // Webページのロード完了時のイベント処理を登録
        task.setOnDone(new EventHandler<TaskEvent>() {
            public void handle(TaskEvent event) {
                // 読み込んだページを右から800移動させる
                TranslateTransition transition 
                        = new TranslateTransition(new Duration(500));
                transition.setNode(view);
                transition.setFromX(800);
                transition.setToX(0);
                transition.setInterpolator(Interpolator.EASE_BOTH);
                transition.play();
                
                // WebEngineからURLを取得し
                //テキストボックスに反映させる
                String url = engine.getLocation();
                if (url != null && !url.isEmpty()) {
                    box.setText(url);
                }
            }
        });
    }
}