プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

JavaFXでのダイアログ作成

 だいぶJavaFXが分かってきたので、ちょいとまとめ。


 JavaFXでダイアログ表示するのって面倒くさいな〜って思って、自分で使う分には簡単に作れるようなAPIを整えていました。単にダイアログって言っても、ウィンドウ形式でポップアップするダイアログの他に、モダンブラウザで実装されているJavaScriptのalertがウィンドウ内部だけで表示されるタイプのダイアログなどがあります。その違いをなるべく気にしないでいいように実装してみました。全部のソースコードはGithubにおいています。https://github.com/nodamushi/NodamushiJFXUtilities


 言葉よりも図を示した方が良いですね↓


 左はウィンドウ形式のダイアログ、右はインナーウィンドウ形式のダイアログです。



 ウィンドウ形式はStageを、インナーウィンドウ形式は渡されたStackPaneの一番上に表示させる形で表示します。ダイアログなので、閉じるまで待機するという処理がしたいので、作るプログラムのおおよその形は以下の様になります。(importは常に省略します)

//↓次以降のコードはpackageも省略します。
package nodamushi.jfx.scene.dialog;

public final class DialogFactory{
    /**
     * ウィンドウ形式のダイアログを表示
     * @param contents 表示する内容
     * @param parentWindow ダイアログの親ウィンドウ
     * @param x ダイアログの表示位置のx座標
     * @param y ダイアログの表示位置のy座標
     * @param isWait ダイアログが閉じるまで待機するかどうか
     */
    public static void showWindowDialog(Parent contents,
            Window parentWindow,double x,double y,boolean isWait){
        //TODO 表示の為にStageを作る
        //TODO isWaitがtrueの場合はStageが閉じるまで処理を中断する
    }

  /**
     * インナーウィンドウ形式のダイアログを表示
     * @param contents 表示する内容
     * @param parentPane ダイアログを表示させるStackPane
     * @param isWait ダイアログが閉じるまで待機するかどうか
     */
    public static void showInnerDialog(Node contents,
            StackPane parentPane,boolean isWait){
        //TODO 表示の為に、全てのイベントを奪取するPaneを作成し、contentsを入れる
        //TODO isWaitがtrueの場合はPaneをparentPaneから取り除くまで処理を中断する
    }

    //ユーティリティークラスなので、インスタンスは作りません。
    private DialogFactory(){}
}




 実際にはJavaFXのスレッド以外から呼ばれても正常に動作するようにしてたり、特定のNodeの上にウィンドウ形式のダイアログを表示できるようにしたりと色々してるのですが、今回は全部省略します。

 さて、まず困るのはダイアログを閉じるという処理です。通常、ウィンドウの×ボタン以外にも、OKボタンやキャンセルボタンなどを押してもダイアログは閉じます。今回、ウィンドウ形式もしくはインナーウィンドウ形式の二つで、どちらも閉じ方が異なります。それらの違いをコントローラに任せるのは酷いというものです。なので、閉じるという処理はこちらでメソッドを実装し、そのメソッドを呼んでもらうことで違いを吸収します。

//ダイアログを閉じることを定義するインターフェース
public interface DialogCloseFunction{
    /**
     * ダイアログを閉じます。
     * このメソッドを呼び出した後はこのオブジェクトは動作をしません。
     */
    public void closeDialog();
}

//ダイアログの動作を定義するインターフェース
public interface DialogModel{
    /**
     * ダイアログを閉じる操作を実装したfuncを保持してください。
     * このメソッドはDialogFactoryからダイアログが表示されるたびに呼び出されます。
     * キャンセルボタン、OKボタン等により、ダイアログを閉じるときはこのfuncを利用してください。
     * @param func ダイアログを閉じる操作を定義したオブジェクト。
     */
    public void setDialogCloseFunction(DialogCloseFunction func);

    /**
     * ダイアログをクローズします。
     * setDialogCloseFunctionで設定されたfuncを用いてクローズしてください。
     * setDialogCloseFunctionで設定されたfuncは破棄してください。
     */
    public void closeDialog();
}




 この二つを用いて、DialogFactoryを改善します。(JavaDoc省略)

public final class DialogFactory{
    public static void showWindowDialog(Parent contents,DialogModel model,
            Window parentWindow,double x,double y,boolean isWait){
        //TODO 表示の為にStageを作る
        //TODO ダイアログを閉じることを定義し、modelに通知する
        //TODO isWaitがtrueの場合はStageが閉じるまで処理を中断する
    }

    public static void showInnerDialog(Node contents,DialogModel model,
            StackPane parentPane,boolean isWait){
        //TODO 表示の為に、全てのイベントを奪取するPaneを作成し、contentsを入れる
        //TODO ダイアログを閉じることを定義し、modelに通知する
        //TODO isWaitがtrueの場合はPaneをparentPaneから取り除くまで処理を中断する
    }
}




 後は実際に実装していくだけです。まずは簡単なウィンドウ形式の方から。

 なお、ウィンドウダイアログを作るのに必要な情報(リサイズ可能かどうか、ウィンドウのアイコン、ウィンドウのタイトルなど)を取得できるようにDialogModelの定義を拡張してあります(省略)。

 public static void showWindowDialog(Parent contents,DialogModel model,
         Window parentWindow,double x,double y,boolean isWait)
                 throws NullPointerException{
     //親ウィンドウが表示されていないときはnull扱い
     if(!parentWindow.isShowing())parentWindow=null;

     //Stageを作成、初期化
     StageBuilder<?> builder=StageBuilder.create();
     StageStyle style = model.getStageStyle();
     if(style!=null)builder.style(style);
     builder
     .scene(new Scene(contents))
     .onCloseRequest(new WindowCloseEventHandler(model));
     Collection<? extends Image> icon  = model.getIcons();
     if(icon!=null)builder.icons(icon);
     Stage s = builder.build();
     if(parentWindow!=null){
         s.initOwner(parentWindow);
     }
     if(x>=0)
         s.setX(x);
     if(y>=0)
         s.setY(y);
     
     s.resizableProperty().bind(model.dialogResizableProperty());
     s.titleProperty().bind(model.dialogTitleProperty());
     Modality m = model.getDialogModality();
     if(m==null||(m==Modality.WINDOW_MODAL&&parentWindow==null))
         m=Modality.NONE;
     s.initModality(m);

     //ダイアログを閉じることを定義 WindowDialogCloseFunctionは後述
     WindowDialogCloseFunction f = new WindowDialogCloseFunction(s);
     model.setDialogCloseFunction(f);

     //表示
     if(isWait)
         s.showAndWait();//待機する
     else
         s.show();//待機しない
 }

//ダイアログを閉じることを定義
private static class WindowDialogCloseFunction 
implements DialogCloseFunction{
    private Stage stage;

    public WindowDialogCloseFunction(Stage stage){
        this.stage = stage;
    }

    @Override
    public void closeDialog(){
        if(stage!=null){
            stage.close();
            stage = null;
        }
    }
}





 次はインナーウィンドウ形式です。Stageを使った場合は、showAndWait()を使えば処理を待機させることが出来ましたが、今回はそうはいきません。でも、やっぱり同じように待機したいです。そこで、com.sun.javafx.tk.Toolkit#enterNestedEventLoop(Object)を利用します。

 com.sun以下のパッケージを利用して良いのかという問題がありますが、使わないとどうにもならんのだから仕方がない。

private static EventHandler<Event> ALL_CONSUME=new EventHandler<Event>(){
    public void handle(Event event){event.consume();}
};
public static void showInnerDialog(Node contents,DialogModel model,
         StackPane parentPane,boolean isWait){
     //格納するPane
     FlowPane back = new FlowPane();
     back.setAlignment(Pos.CENTER);//中央に配置
     back.getStyleClass().add("innerdialog");
     back.setStyle("-fx-background-color:rgba(60,60,60,0.5);");
     back.addEventHandler(Event.ANY, ALL_CONSUME);//イベント奪取

     //TODO 本当はウィンドウっぽくしたい(面倒くさかった)
     StackPane base = new StackPane();
     base.setStyle("-fx-background-color:white;");
     base.getStyleClass().add("innerdialog-base");
     base.getChildren().add(contents);
     back.getChildren().add(base);
     parentPane.getChildren().add(back);

     //TODO フォーカスの移動を制限したい場合ってどうすれば良いんじゃ?

     InnerDialogCloseFunction f = new InnerDialogCloseFunction(parentPane, back, base, contents);
     model.setDialogCloseFunction(f);

     //待機
     if(isWait)
         f._wait();
 }

private static class InnerDialogCloseFunction 
implements DialogCloseFunction{
    private StackPane parentPane;
    private FlowPane back;
    private StackPane base;
    private Node contents;
    private Object key = new Object();
    private Object obj;
    private volatile boolean wait = false;
    private boolean closed = false;
    public InnerDialogCloseFunction(StackPane parentPane,
            FlowPane back,StackPane base, Node contents){
        this.parentPane=parentPane;
        this.back = back;
        this.base = base;
        this.contents = contents;
    }

    //待機処理を行います。(waitフラグを格納する為にここで行っています)
    void _wait(){
        wait = true;
        obj=com.sun.javafx.tk.Toolkit.getToolkit().enterNestedEventLoop(key);
    }

    @Override
    public void closeDialog(){           
        if(!closed){
            closed=true;
            parentPane.getChildren().remove(back);
            base.getChildren().remove(contents);
            if(wait)//待機している場合は復帰作業をする
                com.sun.javafx.tk.Toolkit
                .getToolkit().exitNestedEventLoop(key, obj);

            parentPane=null;
            back = null;
            base = null;
            contents=null;
            obj =key= null;
        }
    }
}





 これで、Stage#showAndWaitと同等の動作をするインナーウィンドウ形式のダイアログを作成することが出来ました。出来たのは良いんですが、まだJavaFXでのフォーカスの制御の仕方やノードが表示されたときのタイミングの取得が分からないので、ダイアログの下のノードにフォーカスが残ったりしています。何とかしないとね。

 あと、面倒くさいから放置してしまった、ウィンドウっぽいノードも作りたいね。



 今回のDialog関連の実装はhttps://github.com/nodamushi/NodamushiJFXUtilities/tree/master/src/nodamushi/jfx/scene/dialogにあります。



 次回は根気があればControlの自作と、SkinとControllerあたりの話をしようかな。本当はこの記事で全部やるつもりだったけど、根気が持たなかった。



 作ってるときに参考にした記事:"JavaFX2.2でダイアログを作る方法"