プログラムdeタマゴ

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

JavaFXのControlとSkinとControllerと

 前回のダイアログの話の続きです。前回はダイアログを表示するための機構を作りました。今回はそのダイアログに表示する中身を作っていこうと思います。
 単純なダイアログって、「ダイアログのタイトル」「メッセージの詳細」「Cancel,No,Yesボタン」から構成されます。気が向いたら「画像」があるぐらいかな。(!とか▲みたいな画像が多いですよね)


 それを表示するのに、毎回JavaFX Scene Builderを起動してFXMLを作ってって、さすがに面倒くさい。一つのモジュールというか、クラスというか、とにかく簡単に扱いたい。

 そこで、今回は「ダイアログのタイトル」「メッセージの詳細」「Cancel,No,Yesボタン」「画像」これらの要素を持つControlを作ってみました。

MVCモデル

 まずはControlを作る前に、ModelとViewとController(頭文字を取ってMVC)のことについてお話しします。でもその前に、正直ControlだとかControllerだとか似た名前で何が何だか………とか私は思いました。JavaDocによると、Controlはユーザーが操作できるシーングラフ中のノードと定義されています。MVCのControllerとControlは関係ないので混合しないようにしましょう。

 Controllerはという単語は、JavaFX Scene BuilderでFXMLを作成したとき、なんか知らないけどチュートリアルで〜Controllerクラスを作れって言われたからMyControllerクラスを作って、んで@FXMLが〜onActionが〜、とかやった時に聞いたことあると思います。そのControllerです。
 MVCモデルはGUIのプログラム構成を、動作やデータを定義するModel、実際に画面に出力するView、Viewからのアクション(マウスクリックなど)を受け取り処理をするControllerからなります。最も重要なのはModelで、こいつがいろいろ計算したり、データ保持したり書き換えたりと言った重要な仕事をします。次に重要なのはViewで、Modelから表示するべき内容等のGUI処理の基本を担います。Controllerは、ほんっとどーでもいいやつで、何も仕事しません。Controllerの役割はユーザーからの入力の処理に相当するModelのメソッド、もしくはViewのメソッドを呼び出すために、入力を適切な形に変換することにあります。よくあるFXMLのControllerのチュートリアルって、やたらControllerが機能持ってて、処理はControllerですれば良いんだーって感じだけど、あれおかしいと思います。何かするのはModelです。


 じゃぁ、そのModelはJavaFXでは何になるのかというと、Nodeになります。特に、ユーザーが何らかの操作ができて、複雑なデータを保持するようなものはControlが行います。


 で、最後にViewはどこにあるのかっていうと、たぶんJavaFXには我々がいじれる範囲ではほとんど見えてないんだと思います。Canvasを使って自前でレンダリングするというならViewになるかも。CSSやSkinがViewだっていう説明をどっかで読んだのですが(出典不明)、それっておかしくない?だって、CSSはViewの動作を間接的に定義しているからまだしも、Skinが返すのはNodeだから、SkinがViewならNodeはGUIになっちゃうよ。Nodeが保持するデータやNodeそれ自体は、ViewがGUIを構築するためのデータと考えるのが良いのかも。私はSwingの癖でNode=GUIと考えていたので、最初どうもJavaFXが受け入れられなかったです。


Controlを作る

 MVCモデルを説明したところで、ようやくControlを作っていきましょう。実装しなくてはいけないことは以下の二つ。

Model:ダイアログのタイトル、メッセージの詳細、画像などのデータの保持、キャンセル、yes、noの動作、Nodeの構築

Controller:Cancel,Yes,Noボタンのアクションを受け取り、Modelのメソッドをよぶ。


 まずはModelから見ていきましょう。


 ModelはNDialogというクラスに実装することにしました。

package nodamushi.jfx.scene.dialog;
public class NDialog extends Control{
    public NDialog(){
       getStyleClass().add("ndialog");//CSSのクラスはndialogにしました。
       //TODO データの初期化
       //TODO 子要素のNodeの作成(LabelやらButtonやら)
    }

    public void cancel(){
      //TODO cancelの実装
    }
    public void yes(){
      //TODO yesの実装
    }
    public void no(){
      //TODO noの実装
    }

    //--------------data field-------------------

    //TODO データの作成

    //--------------end data---------------------
}




データや振る舞いの実装

 保持するデータをPropertyで保持しておくと、Ovserbalパターンを使ったバインディングの仕組みにより、Modelの変更をViewやそのほかのNodeに通知する必要がなくなり便利です。一方、毎回以下の様に

private IntegerProperty numberprop = new SimpleIntegerProperty(this,"number");
public int getNumber(){return numberprop.get();}
public void setNumber(int value){prop.set(value);}
public IntegerProperty numberProperty(){return numberprop;}

 長ったらしいコードを書かないといけないので滅茶苦茶面倒くさいです。読み込みだけで、他クラスからはsetできないようなReadOnly〜Propertyはさらに記述が面倒くさいです。
 私は愛用しているEclipseたんにProperty作成の便利機能が揃っていないので、PowerShellで「fxprop int writable number」とコマンドを実行すると、上記の内容を作成し、クリップボードにコピーするようにしてプロパティを作っています。

 ReadOnlyPropertyに関してはインナークラス作るの面倒くさいので、私の場合はnodamushi.jfx.beanにReadOnlyPropertyが簡単に作れるようなファクトリを作成しています。わざわざ自作しなくてもReadOnly〜PropertyWrapperクラスを使っても問題ありません。

private ReadOnlyStringPropertyFactory namepropfactory = new ReadOnlryStringPropertyFactory(this,"name");
private void setName(String value){namepropfactory.setValue(value);}
public String getName(){return namepropfactory.getValue();}
public ObjectProperty<String> nameProperty(){return namepropfactory.get();}

 もち、「fxprop String readonly name」だけで上記の内容がクリップボードにコピーされるようにしています。


 なお、わざわざ私がファクトリを作った理由は、マルチスレッドに対応させることと、わざわざPropertyをベースにしてWrapするのって無駄と思ったからです。本当はSimple〜Propertyのマルチスレッド版も作ろうかと思ったけど、面倒くさかったなりぃ。


 とにかく、上記のようなコードを何個も書いてデータを作成していきます。データは、表示する文字や結果、ボタンが有効か無効かのプロパティ等々。無駄に長いのでコードは省略。(NDialog.javaに全て書いてあります。)

 つぎに、cancelやらのメソッドを実装をします。cancelやyesなどのメソッドが呼ばれたら、結果を格納する他にも、前回話したようにダイアログを閉じる必要があります。前回、その作業はDialogModelというインターフェースにDialogCloseFunctionを渡すことで実装するとしたので、NDialogにDialogModelを実装します。

public class NDialog extends Control implements DialogModel{
    private DialogCloseFunction func;
    @Override
    public void setDialogCloseFunction(DialogCloseFunction func){
        if(this.func!=null)throw new RuntimeException(
                "setDialogCloseFunction:closeDialogが呼ばれる前に再設定は出来ません");
        this.func = func;
    }

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

    public void cancel(){
        //※結果を格納する為にResultという列挙型を定義しています
        setResult(Result.CANCELLED);
        closeDialog();
    }
    
    public void yes(){
        setResult(Result.YES);
        closeDialog();
    }
    
    public void no(){
        setResult(Result.NO);
        closeDialog();
    }




Skinの実装

 後は子要素の作成をすればModelは完成です。頑張りましょう。

 子要素の作成はここまでのModelのデータや振る舞いの実装と違って、見た目を構築していくかなりView寄りの話です。なのでこのままNDialogの中にその実装までしてしまうと、かなりちぐはぐな印象のクラスができあがってしまいます。そこで、Controlには子要素の構築に関してはSkinを実装したクラスに委任する仕組みがあります。Skinとして実装するメリットはすっきりするという以外にも、Skinを変えるだけでModelの変更はしなくても見た目を変えることが出来るなどがあります。ここではNDialogSkinクラスで実装することにします。

    private static final String SKINCSS = String.format("-fx-skin:'%s';", NDialogSkin.class.getName());
    //↓コンストラクタ
    public NDialog(){
        getStyleClass().add("ndialog");
        //SkinのFQCNをCSSで登録しておきます。
        setStyle(SKINCSS);
    }



 Skinの設定にはCSSを使います。ただ、JavaFX8からはcreateDefaultSkin()というメソッドが定義されているので、そちらを使った方が良いと思います。なお、setSkinClassNameというメソッドもありますが、これは使わない方が良いです。Skinを作るにはどうしてもSkinに自分を渡す必要があります(setSkinClassNameで指定したSkinはコンストラクタがSkinnableを受け取れる必要がある)。ということは、コンストラクタ内でSkinを作ろうとするとどうしても、コンストラクタが完了していない未完成なオブジェクトをSkinに渡すことになってしまいます(setSkinClassNameは呼び出したときにSkinインスタンスを作ります)。コンストラクタからthisを漏らすってバグの温床です。色々怪しいですね。一方CSSで指定した場合は、表示されてない段階でCSSの解析が行われないようなので、コンストラクタ中でSkinが作られることはありません。後は、クラスを継承したときに、無駄にSkinインスタンスを作られないというメリットもありますね。

 で、メインのSkinの実装ですが、今回は面倒くさぁいということで、全部JavaFX Scene Builderのダイアログテンプレートを、ちょっとIDとかclass名とか変更して作ったFXMLに任せてしまいました。FXMLを使わずに実装するとなると結構面倒だね。その面倒を解消する為か、JavaFX8からはSkinBaseってのが提供されるみたいです。

public class NDialogSkin implements Skin<NDialog>{
    private NDialog skinnable;
    private Parent contents;
    private static String CSS=NDialogSkin.class.getResource("ndialog.css").toExternalForm();

    //コンストラクタの引数はSkinnable(=Controlとか)です。
    public NDialogSkin(NDialog nd){
        skinnable = nd;
        FXMLLoader loader = new FXMLLoader(NDialogSkin.class.getResource("ndialog.fxml"));
        //Controllerの作成 FXMLで設定するとFQCNが変わったときに面倒くさいからこっちの方が良いかと
        loader.setController(new NDialogController(nd));
        try {
            contents = (Parent)loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
        nd.getStylesheets().add(CSS);
    }
    @Override
    public NDialog getSkinnable(){
        return skinnable;
    }
    @Override
    public Node getNode(){
        return contents;
    }
    @Override
    public void dispose(){
        contents = null;
    }
}




Controllerの実………そ………う?

 最後に、FXMLLoaderで作ったParentのイベントを受け取る為のControllerを作れば自作Controlは完成だ!

 JavaFX1からの流れなのか、Controllerではなく、〜Behaviorというクラス名で実装されていることが多いです。

 今回は内容が非常に単純なので、キャンセルボタンが押されたらNDialogのcancel()を、イエスボタンが押されたらyes()を、ノーボタンが押されたらno()を呼びだすということを実装するだけです。本来は。

public class NDialogController implements Initializable{
    @FXML public Label messageLabel,titleLabel;
    @FXML public Button cancelButton,noButton,yesButton;
    @FXML public ImageView imageView;    
    private NDialog ndialog;
    public NDialogController(NDialog d){ndialog=d;}
    
    @Override
    public void initialize(URL location ,ResourceBundle resources){
        
        // 本来はSkinで実装すべき内容
        messageLabel.textProperty().bind(ndialog.messageProperty());
        titleLabel.textProperty().bind(ndialog.titleProperty());
        cancelButton.textProperty().bind(ndialog.cancelTextProperty());
        //……こんな感じでbind,bind,bindしまくっていきます(省略)
        
        //   Skinの内容終わり
        
        cancelButton.setOnAction(new EventHandler<ActionEvent>(){
            public void handle(ActionEvent event){ ndialog.cancel();}
        });
        noButton.setOnAction(new EventHandler<ActionEvent>(){
            public void handle(ActionEvent event){ndialog.no();}
        });
        yesButton.setOnAction(new EventHandler<ActionEvent>(){
            public void handle(ActionEvent event){ndialog.yes();}
        });
    }
}




 うん、何故かControllerでBindの設定をしまくっていますねー。おかしいですねー。でもFXMLLoaderを使うんだったら、こうならざるを得ないと思いますねー。面倒くさいですからねー。妥協ですねー。
 あと、onActionの設定をFXMLでしていないのは、単にメソッド名を考えるのが面倒くさいからです。リファクタリングすると使えなくなるし。



CSSの拡張

 JavaFX2.2の段階では公開になっていないので使えませんが、CssMetaDataやStyleablePropertyを使えば独自にCSSの拡張が行えるみたいです。(現在はcom.sunパッケージに)




 これで、自作Controlクラスの完成です。まとめると、Controlを作るときはデータや振る舞いを定義する基本Modelクラスを作り、Modelの子要素を構築するSkinを作り、Skinに対応するControllerを作るという流れになります。こう書くと、確かにSkinがViewっぽいけど、やっぱりど〜〜も納得いかない。Nodeの木構造の構築はModelの操作なのか、Viewなのか、はっきりして欲しいぜよ。

 まぁ、そんな細かいことはどーでもいいとして、この記事で一番重要なことは、Controllerにコードいっぱい書くな!みっともない!って事です。(え?マジで?)


 それでは、ここまで読んでくださった方お疲れ様でした。今回のコードはnodamushi.jfx.scene.dialogで全て公開してあります。



↓適当なテストクラスを作ってNDialogを使ってみた結果です。

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でダイアログを作る方法"

BufferedImageからJavaFXのWritableImageへの変換

 全く別の目的でJavaFXのJavaDocを眺めていたら、SwingFXUtilsクラスのtoFXImageっていう静的関数を知りました。JavaDoc{SwingFXUtils(JavaFX2.2)}


 第二引数のWritableImageはnullの場合や、渡された画像がBufferedImageのサイズと一致していない場合は新たなWritableImageが生成される。この画像はスナップショットでBufferedImageを書き換えても、変更はされない。ま〜、データの保持の仕方自体から異なるから当然っちゃ当然か。

 これで、今まで通りBufferedImageで編集した画像を簡単にJavaFXで表示できるようになったっちゃなったんだけど、ぶっちゃけSwingのままでいいや(ウォイ





テストコード

import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class Main extends Application{
    public static void main(String[] args) {launch(args);}
	
    @Override
    public void start(Stage stage) throws Exception{
	    
        HBox p = new HBox();
        p.setStyle("-fx-background-color:yellow;");

        //以下のBufferedImageのタイプそれぞれに画像を生成する
        int[] types = {BufferedImage.TYPE_3BYTE_BGR,BufferedImage.TYPE_4BYTE_ABGR,
            BufferedImage.TYPE_INT_RGB,BufferedImage.TYPE_INT_ARGB};
        for(int t:types){
            BufferedImage bimg = new BufferedImage(100, 100, t);
            Graphics2D g = bimg.createGraphics();
            g.setColor(Color.cyan);
            g.fillOval(0, 0, 40, 40);
            g.dispose();

            WritableImage img = SwingFXUtils.toFXImage(bimg, null);
            ImageView iview = new ImageView(img);

            p.getChildren().add(iview);
        }
        Scene scene = new Scene(p);
        stage.setScene(scene);
        stage.show();
    }
}




実行結果

JavaFXの練習5:Drag&DropでNodeを移動する

 前回は欽ちゃん1号さん、コメントありがとうございました。返信が遅くなったことをお詫びします。
 さて、今回はDrag&Dropの話です。(以下D&Dと略記)


 JavaFX D&DでGoogle検索しても、なんか出てくるのはクリップボードを経由したD&Dの話ばっかり。私が今回やりたいのはそうじゃなくって、あるNodeをドラッグして、別なParentの上でドロップしたら、そのParentにNodeを移動させるってことをしたい。
 とりあえず、JavaFX2.2のMouseEventとMouseDragEventのJavaDocを読みながらモソモソやってみたらとりあえず出来ました。


 いちいちJavaでコンポーネントの配置を書くのは面倒くさいので、JavaFX Scene Builderでちゃちゃっと作って、後はコントローラーに書くことにします。



 この青い円を、左右の四角形の中にD&Dで移動させることを目的とします。
 fx:idはそれぞれ

  • 青い円(Circle):draggable
  • 左側の四角(Pane):left
  • 右側の四角(Pane):right

 としました。


 なので、このFXMLのコントローラーはこんな感じになります。

package ctr;

import java.net.*;
import java.util.*;
import javafx.event.*;
import javafx.fxml.*;
import javafx.scene.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.shape.*;

public class Test implements Initializable{
  public Pane left,right;
  public Circle draggable;

  @Override
  public void initialize(URL location, ResourceBundle resources) {
  }

}

 最終的に必要なimportは先に載せておきました。これ以降は全部initializeメソッドの実装になります。

 まずは、draggableをドラッグできるようにする必要があります。これには、draggable.startFullDrag()というメソッドをDragDetectedイベントが起こった時に呼び出せばいいようです。これを呼び出すと、press-drag-releaseのジェスチャーのソースとすることが出来るようになる模様。

draggable.setOnDragDetected(new EventHandler<MouseEvent>() {
  @Override
  public void handle(MouseEvent event) {
    System.out.println("drag detected");
    //ドラッグ開始
    draggable.startFullDrag();
    event.consume();
  }
});



 で、これで準備は完了とは行かなくて、この状態でD&Dしても、ほかのノードにはそのイベントの情報が伝わらない。そこで、マウスイベントを透過させるようにします。

draggable.setOnMousePressed(new EventHandler<MouseEvent>(){
  @Override
  public void handle(MouseEvent event) {
    System.out.println("mouse pressed");
    //dragイベントをマウスの下のノードにも伝わるようにするために
    //マウスイベントの透過性をtrueにする。
    draggable.setMouseTransparent(true);
    //consume()をしておかないと
    //下のノードにpressedイベントが伝わってしまう。
    event.consume();
  }
});

draggable.setOnMouseReleased(new EventHandler<MouseEvent>(){
  @Override
  public void handle(MouseEvent event) {
    System.out.println("mouse released");
    //処理が終わったので、元に戻しておく。
    draggable.setMouseTransparent(false);
    event.consume();
  }
});




 これで、draggableの準備は完了です。次に、leftとrightがドラッグイベントを受け付けれるようにします。今回は毎回new EventHandler(){………}と書くのが面倒くさかったので、一つのEventHandlerで対応させました。

EventHandler<MouseDragEvent> drageventhandler = 
    new EventHandler<MouseDragEvent>() {  
  @Override
  public void handle(MouseDragEvent event) {
  }
};
//MouseDragEvent全部にdrageventhandlerを適応する場合
right.addEventHandler(MouseDragEvent.ANY, drageventhandler);

//個別に適応する場合
left.setOnMouseDragExited(drageventhandler);
left.setOnMouseDragEntered(drageventhandler);
left.setOnMouseDragReleased(drageventhandler);
left.setOnMouseDragOver(drageventhandler);




 後はhandleメソッドを実装していくだけです。まずは、それぞれのイベントの状態に合わせて文字列を出力させるようにしてみます。

public void handle(MouseDragEvent event) {
  Object type = event.getEventType();
  if(type==MouseDragEvent.MOUSE_DRAG_RELEASED){
    System.out.println("drag release");
  }else if(type == MouseDragEvent.MOUSE_DRAG_ENTERED){
    System.out.println("drag entered");
  }else if(type == MouseDragEvent.MOUSE_DRAG_ENTERED_TARGET){
    System.out.println("drag entered target");
  }else if(type == MouseDragEvent.MOUSE_DRAG_EXITED){
    System.out.println("drag exited");
  }else if(type==MouseDragEvent.MOUSE_DRAG_EXITED_TARGET){
    System.out.println("drag exited target");
  }else if(type == MouseDragEvent.MOUSE_DRAG_OVER){
    System.out.println("drag over");
  }
  event.consume();
}




 enumじゃないので、分岐が面倒くさいです。なお、typeの型がObjectなのは、単純に書くのが面倒くさかっただけです。MOUSE_DRAG_ENTERED_TARGET、MOUSE_DRAG_EXITED_TARGETは何のことかよくわかりません。


 ドロップされた後の処理を書くにはMOUSE_DRAG_RELEASEDで処理を記述していけばいいです。

if(type==MouseDragEvent.MOUSE_DRAG_RELEASED){
  System.out.println("drag release");
      
  //draggableの取得
  Object srcobj = event.getGestureSource();
  //left またはrightの取得
  Object targetobj = event.getTarget();
      
  //一応型チェック
  //Parentだと、getChildren()が見えないので
  //Paneにしてある。
  if((srcobj instanceof Node) &&
     (targetobj instanceof Pane)){
        
    Pane target = (Pane)targetobj;
    Node src = (Node)srcobj;
    //どうやら、元の親からのリムーブは
    //自動で行われるっぽい。
    target.getChildren().add(src);
  }
}




 とりあえず、これで移動させることが出来ました。




 ただ、なんか動きがやたらもっさりしているような気がします。あまり正攻法じゃないのでしょうか?

 あと、ドラッグしている間、せっかくなのでマウスに円を追随させたいのですが、SwingでいうGlassPane的なものはないんですかね?よくわからんです。

 それと、ドロップが完了したことをソース元に通知する簡単な手段も見当たりませんが、どうしたらいいんですかね?(setOnDragDoneは反応しませんでした。)


 今日やったところはこんな感じです。

JavaFXの練習4:レイアウトがわからない

 さぁ、JavaFX2記事第4弾。ついに詰まりました。全然わかりません。誰か教えてください。
 今、nodamushiがわからないこと

  1. 別スレッドで処理した内容をsetTextでLabelの内容を変えようとするとスレッドがJavaFXのスレッドじゃないとエラーになる。SwingUtilities.invokeLater(Runnable)みたいなことはどうすればいいの?
  2. あるプロパティの値の変化でNodeの最適な大きさが変化したときに、どうすれば自動的に大きさを変更できるのか。(Swingでいうrevalidateとかみたいな)
  3. ぶっちゃけ、プロパティのbeanって何よ。豆って何よ。ていうか、プロパティってどれ使えばいいのよ。

 というわけで、誰か優しい人が教えてくれると期待しつつ(チラッ 今日やった内容を書き留めときます。


自作のレイアウトを作ってみる。

 簡単なレイアウトなら、既存のレイアウトパネル群を使えば、大体は出来るだろう。でも、やっぱ出来ないことだってあるじゃん。それに、今までSwing使ってきた私としては、自分でLayoutManagerを書いた方が何となくやりやすい。
 というわけで、練習として、今回は下の図の様に自分の子を円形に配置するレイアウトを作ってみることにしたよ。




 レイアウトのためのノードを作るには、javafx.scene.layout.Paneを継承したクラスを作ればいいっぽい。で、以下のメソッドをオーバーライドする。


  • layoutChildren():実際に子の配置を行うメソッド
  • computePreWidth(double):このノードの最適な幅を計算するメソッド
  • computePreHeight(double):このノードの最適な高さを計算するメソッド
  • computeMinWidth(double):このノードの最小の幅を計算するメソッド
  • computeMinHeight(double):このノードの最小の高さを計算するメソッド
  • computeMaxWidth(double):このノードの最大の幅を計算するメソッド
  • computeMaxHeight(double):このノードの最大の高さを計算するメソッド

 今回、最大最小は特に気にしないことにしたので、最初の三つをオーバーライドすることにした。なお、最小値のデフォルトはinsetの大きさ、最大はDouble.MaxValueを返すらしい。

 円形に配置したいので、高さの最適値は「円の直径+子の高さの中で最大のもの」、幅も同様に「円の直径+この幅の中で最大のもの」とした。今回はinsetは考えていない。
 と、いうわけで、これを元に実装するとこうなる。

package nodamushi.layout;

import javafx.beans.property.*;
import javafx.collections.*;
import javafx.scene.*;
import javafx.scene.layout.*;

public class CirclePane extends Pane{
  //半径
  private double radius;  
  public CirclePane(double r) {
    setRadius(r);
  }
  
  public void setRadius(double d){radiuse=d;}
  public double getRadius(){return radius;}
  
  private double getMaxChildWidth(double height){
    ObservableList<Node> nodes = getChildren();
    double maxWidth=0;
    for(Node n:nodes){
      double d = n.prefWidth(height);
      if(maxWidth < d)maxWidth = d;
    }
    return maxWidth;
  }
  private double  getMaxChildHeight(double width){
    ObservableList<Node> nodes = getChildren();
    double maxHeight=0;
    for(Node n:nodes){
      double d = n.prefHeight(width);
      if(maxHeight < d)maxHeight = d;
    }
    return maxHeight;
  }
  @Override protected double computePrefWidth(double height) {
    return getMaxChildWidth(height)+getRadius()*2;
  }
  
  @Override protected double computePrefHeight(double width) {
    return getMaxChildHeight(width)+getRadius()*2;
  }
  
  @Override protected void layoutChildren() {
    ObservableList<Node> nodes = getChildren();
    int length = nodes.size();
    if(length == 0)return;
    double step = 2*PI/length;//一つ配置する毎の回転角度
    double rad = 0;//現在の配置角度
    double width = getWidth();//このノードの大きさ
    double height = getHeight();
    double r = getRadius();//半径
    for(Node n:nodes){
      //配置するノードの中心座標を計算
      double x = Math.cos(rad)*r+width/2;
      double y = Math.sin(rad)*r+height/2;
      //配置するノードの左上の座標を計算
      double w = n.prefWidth(-1);
      double h = n.prefWidth(-1);
      x = x- w/2d;
      y = y- h/2d;
      //再配置
      n.resizeRelocate(x, y, w, h);
      //配置角度の更新
      rad+=step;
    }
  }
}

 このCirclePaneに適当にCircleを30個追加して表示した結果が先の図ってわけです。ひとまず、レイアウトをすることは出来たね。でも、せっかくなので、この半径を変更したり、配置の開始角度を変更したりしてアニメーションさせることが出来たら楽しそうだよね。

 というわけで、double radiusをDoublePropertyに変えてみよう………と思わなけりゃ良かった。
 とりあえず、radiusの値の変更があったら、自動で再レイアウトをしてもらわないといけない。何となく、それっぽい関数にrequestLayout()があったので、それを呼んでみた。


private DoubleProperty radius = new DoublePropertyBase() {
  @Override
  public void invalidated() {
    requestLayout();
  }
  
  @Override
  public String getName() {
    return "radius";
  }
  
  @Override
  public Object getBean() {
    return CirclePane.this;
  }
};
public final void setRadius(double d){radiuseProperty().setValue(d);}
public final double getRadius(){return radius.getValue();}

 とりあえず、これでアニメーションしてみた。

//importは略
public class Main  extends Application{
  public static void main(String[] args){launch(args);}
  public void start(Stage stage) throws Exception {
    CirclePane c = new CirclePane(200);
    ObservableList<Node> child = c.getChildren();
    for(int i=0;i<30;i++){
      Color color = new Color(Math.random(), Math.random(),Math.random(), 1);
      child.add(new Circle(20, color));
    }
    VBox vbox = new VBox();
    ObservableList<Node> vchild = vbox.getChildren();
    vchild.add(new Label("top"));
    vchild.add(c);
    vchild.add(new Label("bottom"));
    
    stage.setScene(new Scene(vbox));
    stage.show();
    
    new Timeline(new KeyFrame(new Duration(1500), new KeyValue(c.radiuseProperty(), 250))).play();
  }
}

 以下の内容は勘違いだと気がつきました。ちゃんと、レイアウトの大きさの変更は上のレイアウトに伝わっていました。最初っからがっつりウィンドウサイズを大きくしてから実行した例↓

 きちんとアニメーションに従ってbottomの位置が変化しました。とりあえず、requestLayoutで良さそうです。


 で、結果というと


 ふ〜む………。いやね、ウィンドウサイズまで変化するとは私も思っていなかったんですよ。でも、CirclePaneを代入したVBoxはレイアウトを変更してくれる。つまり、上の図では小さくて見にくいと思いますが、topとbottomの文字が円に被らないように再配置されると思ったんですよ。しかし、結果はtopとbottomは動かないっ!再配置されない!


 で、わからんから、OpenJFXのソースコード読んでみたんですよ。そしたら、平気で、impl_markDrityとか意味わからん関数使っとるんす。意味わからねーよ。
 というわけで、私が目的とする動作をさせるためにはどうすればいいのか、誰か教えてください………(´Д⊂ヽ

JavaFX練習3:アニメーションとか

 前回の続きで今日はアニメーションについて練習しました。あと、SceneはSwingのContentPane相当だということを理解しました。
 しかし、あれだね。JavaFXで3Dが使えるようになると言うことで、今のうちに覚えておこうとやっているんだけど、やっぱりJavaFXって好かんわ。もちろん、Swingよりずっと勝ってるところがあるのはわかるんだよ。でもね、気持ち悪いんだ。
 一番気持ち悪いのはNodeのサイズをいったい誰がどうやって計算してるのかさっぱりわからんこと。prefWidthやらmaxWidthやらminWidthやらはあっても、全部値が-1だったりして、おま、どうやって大きさ決めてんねん。意味わからんわ。だから、初心者の私には挙動がさっぱりわからん。どこをどう設定したら自分の目的とする大きさに設定できるのかがわからん。あと、レンダリングがいったい誰がいつどうやってやってんのかさっぱり見えないこと。設定、設定、で動くのは確かに便利かもしれんが、SwingでpaintComponentやGraphicsとかで直接指示出したり、画像処理で直にピクセル弄ってる人間からしたら気持ち悪くてしょうが無い。もうすでに老害というやつになってるんでしょうか、私。


アニメーション

 JavaFXではアニメーションはAnimationクラスのサブクラス達を使って行うみたいです。挙動はどうあれ、アニメーションの動きの基本は、

こんな感じのようです。このアニメーションはJavaFX Application Threadという名前のスレッドで動いているみたいです。JavaFX Application Threadはどうやら、SwingでいうところのEDTみたいなもののようです。ということは、うっかりアニメーション処理で重たい処理を挟むとJavaFXが止まると言うことなんでしょうかね。
 で、Animationクラスは大きく分けて、TimelineとTransitionの二つで、Timelineはいろんな値を扱ったアニメーションするために、Transitionは対象ノードのプロパティの値を変化させるアニメーションをするために利用するみたいです。

 TimelineはKeyFrameというクラスを使って複数の動作をするアニメーションを定義できるようです。Timelineと言う名前から、私は最初このクラスの挙動は、KeyFrame1を実行後、KeyFrame2を実行して、その後KeyFrame3を………というものだと思っていたのですが、全部いっぺんに動作し始めるみたいでした。

 この図で言うと、上の挙動をするとおもっていたけど、下の方だったということです。

 Transitionは、FadeTransition, FillTransition,PathTransition, RotateTransition, ScaleTransition, StrokeTransition, TranslateTransitionがあり、それぞれ透明度の変化だったり、移動だったりのアニメーションを行ってくれます。また、これらのアニメーションを同時に全て実行するアニメーションをつくるParallelTransition、アニメーションが終わると次のアニメーションを実行するSequentialTransitionがある。


PauseTransitionの価値がよくわかんないっす。setDelayじゃだめなんすか?


スライドイン、スライドアウトするアニメーションを作ってみる

 前回のやつをアニメーションするようにします。アニメーションは

  1. スライドインしながらフェードイン もしくは スライドアウトしながらフェードアウト
  2. 矢印の向き回転

 という動きをするようにします。

 まずは、スライドの動きを作りたいのですが、JavaFXのアニメーションをさせるには何はともあれWritablePropertyが何か必要なんですが、widthPropertyはReadOnlyだし、maxwidthとかの値はそもそもpropertyがない。

 どうすればいいのか、よくわからなかったので、正攻法かどうかわかりませんが、自分で以下のようなWritablePropertyを定義してみました。

    //AnchorPane直下のVBoxもControllerで受け取れるように変更しました。
    public VBox contents_info;
    //サイズを変えるために、間に噛ませたプロパティ
    private DoubleProperty infoboxWidth=new SimpleDoubleProperty(1){
        public void set(double d) {
            super.set(d);
            //dによってmaxWidthを変化させる
            if(d==1d){
                infobox.setMaxWidth(-1);
                return;
            }
            double contents_info_width = contents_info.getWidth();
            double boxwidth = contents_info_width*d;
            infobox.setMaxWidth(boxwidth);
        }
    };

 で、このプロパティの値を0〜1まで変化させることで、スライドのアニメーションを作ってみました。ノードのサイズの変更は、resizeRelocateを使ったら出来るかと思ったのですが、スライドさせることが出来ませんでした。
 以下はアニメーション定義の全容です。全てのソースコードはGitHubにあげておきました。
 もっとうまいやり方があるのかもしれませんが、とりあえず、今回はゴリゴリアニメーションの定義を書いてみました。一応、目的とする動作をさせることが出来ました。



    private SequentialTransition openAnimation,closeAnimation;

    public void slideAction(ActionEvent event){
        if(isOpen)closeAnimation.play();
        else openAnimation.play();
        isOpen = !isOpen;
    }

    @Override
    public void initialize(URL url, ResourceBundle resource) {
        //アニメーション設定

        //スライドインさせるアニメーション
        Timeline slidein = new Timeline(
                new KeyFrame(new Duration(0), new EventHandler<ActionEvent>(){
                    @Override
                    public void handle(ActionEvent e) {
                        if(closeAnimation.getStatus()==Status.RUNNING)
                            closeAnimation.stop();//ついでなので、クローズアニメーションをストップさせてみた
                    }

                }),
                new KeyFrame(new Duration(300),new KeyValue(infoboxWidth,1))
                );
        //フェードインさせるアニメーション
        //Timelineに追加してもいい気がしたけど、
        //練習のためにFadeTransitionを利用
        FadeTransition fadein = new FadeTransition(new Duration(300),infobox);
        fadein.setFromValue(0);
        fadein.setToValue(1);
        fadein.setDelay(new Duration(100));//ちょっと開始を遅らせる。

        //スライドとフェードを結合
        ParallelTransition open = new ParallelTransition(slidein,fadein);

        //スライドインが終わった後にボタンの向きを回転させる
        RotateTransition openrotate = new RotateTransition(new Duration(300),sbutton);
        openrotate.setToAngle(-90);
        openrotate.setFromAngle(90);
        openrotate.setDelay(new Duration(100));


        //openと回転を順次実行するアニメーション
        openAnimation = new SequentialTransition(open,openrotate);

        //スライドアウト
        Timeline slideout = new Timeline(
                new KeyFrame(new Duration(0), new EventHandler<ActionEvent>(){

                    @Override
                    public void handle(ActionEvent e) {
                        if(openAnimation.getStatus()==Status.RUNNING)
                            openAnimation.stop();
                    }

                }),
                new KeyFrame(new Duration(300),new KeyValue(infoboxWidth, 0))
                );
        //フェードアウト
        FadeTransition fadeout= new FadeTransition(new Duration(200),infobox);
        fadeout.setFromValue(1);
        fadeout.setToValue(0);

        //結合
        ParallelTransition close = new ParallelTransition(slideout,fadeout);

        //回転
        RotateTransition closerotate = new RotateTransition(new Duration(300),sbutton);
        closerotate.setToAngle(90);
        closerotate.setFromAngle(-90);
        closerotate.setDelay(new Duration(100));
        //結合
        closeAnimation = new SequentialTransition(close,closerotate);

JavaFXの練習2:Controllerとか

 というわけで、今回もJavaFXの練習した内容です。

 今回は以下のような感じのパネルを作ってみようとしております。



 画像と、ファイル名と、ファイルの説明を表示して、ボタンをクリックするとファイル名と説明部分がスライドアウトしたりスライドインしたりするようなものです。かっくいいね。このスライドイン、アウトをどうすればいいのかにえらい四苦八苦しました。。。


 今回はJava言語を利用する必要があるので、Eclipseでプロジェクトを作成します。先に空っぽのtest.cssと表示する画像のサンプルをプロジェクトフォルダに入れておきました。



 JavaFXの開発をするにはjfrt.jarが必要ですので、jre/libの下にあるjfxrt.jarをビルドパスに追加しておきます。




 ビルダーを起動して、とりあえず空っぽのまま、fxmlをEclipseのプロジェクト上に保存しておきます。





 さて、準備完了したので上の設計図の様に作れるように配置をしていきます。とりあえず、こんな感じになりました。




 AnchorPaneはスライドインしたり、スライドアウトしたりする為に利用しています。VBoxのAnchorPaneに対する制約は、上下右を全部0にしています。
 

 また、全てのノードの大きさを計算した結果の最適な大きさに指定しています。ていうか、なんでJavaFXはサイズに関する指定をCSSでできるようにしなかったのかね?





 CSSを適応させる為に、各ノードのStyleClass名をこんな感じに設定しました。

 で、先に作っておいたtest.cssを読み込ませます。



 さて、CSSを作っていきます。
 まずは背景は真っ黒で、文字列は白色なので、その設定。

.image_info_panel{
    -fx-background-color:black;
    -fx-padding:20px 0 20px 20px;
}

.image_info_panel .label{
    -fx-text-fill:white;
    -fx-font:14px "Arial",self;
}

 次に、文字列やボタンを予定通り下に配置する為にalignmentをbottom-leftにする。

.contents_info,.slidepanel{
    -fx-alignment:bottom-left;
    -fx-padding:0 5px 10px 5px;
}



 ボタンの色と、ボタンの上にマウスが乗ったときに色がほんのり変わるように設定。三角形が横を向くように-fx-rotateをCSSで90を指定しておくと、Java側でsetRotateをしても変化させることが出来なくなったので、コメントアウトしています。この辺JavaFXのCSSは使いにくい…。

.slidebutton{
    -fx-background-color:rgba(0,0,0,0);
    -fx-padding:0;
    -fx-text-fill:gray;
    /*-fx-rotate:-90;*/
}

.slidebutton:hover{
    -fx-text-fill:#608C71;
   -fx-effect:dropshadow( gaussian,#ff0 ,10px, 0, 0,0);
}



 微妙に装飾が寂しかったので、色々装飾。そのさい、-fx-font-style:italicを設定しても、全然斜体にならなかったんですけど、これなんでなんですかね。

/*画像のファイル名*/
.fname_info{
    -fx-padding:0 10px 10px 10px;

    /*font-styleで結果が変わらないんだけど、なんで?*/
    -fx-font-style:italic;
    

    /*仕方が無いので、書き直し*/
    -fx-font:italic 14px "Arial",self;
    -fx-effect:dropshadow( gaussian,#ff0 ,10px, 0, 0,0);
}

/*画像の説明*/
.exp_info{
    -fx-padding:0 10px 0 30px;
}

/*画像の内側にインナーシャドウ*/
.contents_img{
    -fx-effect:innershadow( gaussian, black, 20px , 0,10,0);
}



 はい、これでCSSの設定は終わりです。こんな感じのデザインになりました。

 お〜、当初の予定をだいぶいい感じに再現できましたね。後はJavaでこれらを操作できるようにControllerの設定をしてビルダーのお仕事は終了です。

 Eclipseに戻ってきて、JavaFXを実行できるようにします。実はSceneが何なのか私よく理解してないんですが、とりあえず、これが定石らしいです。

import java.net.*;
import java.nio.file.*;
import javafx.application.*;
import javafx.fxml.*;
import javafx.scene.*;
import javafx.stage.*;

public class Main extends Application{
  public static void main(String[] args) {
    launch(args);
  }

  @Override
  public void start(Stage stage) throws Exception {
    Path fxmlfile = Paths.get("test.fxml");
    URL url = fxmlfile.toUri().toURL();
    
    Parent parent = (Parent)FXMLLoader.load(url);
    
    Scene scene = new Scene(parent);
    stage.setScene(scene);
    stage.show();
  }
}




 で、コントローラーの作成。infoboxとかsbuttonとかの値の設定やslideActionとかの呼び出しはリフレクションが使われる為、セキュリティの設定よっては、privateやprotectedにするとエラーになるらしい。というわけで、publicにしておいた。privateなどの場合は@FXMLアノテーションを付ける必要がある。

package test;

import javafx.event.*;
import javafx.fxml.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;

public class ImgInfoController{
  //これらの値は自動的にセットされる
  public AnchorPane infobox;
  public Button sbutton;
  
  public void slideAction(ActionEvent event){
    System.out.println("クリックされた!");
  }
}

実行結果




 予定通り、閉じたり開いたり出来るようにしたい。今回はいきなりアニメーションに行くのはしんどいので単純に閉じたり開いたりするだけを実装してみました。

private boolean isOpen=true;

public void slideAction(ActionEvent event){
  System.out.println("クリックされた!");
  
  if(isOpen){
    infobox.setMaxWidth(0);
    infobox.setMinWidth(0);
    sbutton.setRotate(90);
    isOpen=false;
  }else{
    infobox.setMaxWidth(Double.MAX_VALUE);
    sbutton.setRotate(-90);
    isOpen=true;
  }  
}

 で、やってみると………

 あるぅええええ?画像の上に文字が出てるんですけどー。とりあえず、CSSのoverflow:hiddenとか設定すればいいのかと思ったら、そんな項目ないし。で、色々悩んで、とりあえずclip設定すればいいんでね?とやってみた。
 clipの設定はオブジェクトが作られたときにやって欲しかったので、このクラスにInitializableを実装する。

import java.net.URL;
import java.util.ResourceBundle;
import javafx.scene.shape.Rectangle;
public class ImgInfoController implements Initializable{
  //オーバーしたのを隠すためのクリップ
  private Rectangle clip;
  
  @Override
  public void initialize(URL url, ResourceBundle resource) {
    clip = new Rectangle();
    infobox.setClip(clip);
    //clipの大きさをinfoboxの大きさに常に合わせる
    clip.widthProperty().bind(
        infobox.widthProperty());
    clip.heightProperty().bind(
        infobox.heightProperty());
    
    sbutton.setRotate(-90);
  }

 実行結果



 おぉ、消えた消えたヽ( ・∀・)ノ

 次はアニメーションに挑戦かー。まだやってないので、次がいつのかわかりません。