ブログトップ 記事一覧 ログイン 無料ブログ開設

プログラムdeタマゴ

2013/10/12

JavaFXで柔軟なキーバインドを作ろうとしたけど挫折した話

動機と目的

 みなさんはキーボード何使ってますか?私は東プレのRealForceを………という話ではなくて、JISキーボードですか?USキーボードですか? US!US!なギーク(笑)には関係ない話ですが、私はJISキーボードです。前の研究室はUSキーボードだったので両方使えますが、JISのがしっくりきます。

 で、JISは世界の標準ではないので、色々問題起こります。

 「Ctrl+@」

 このキー操作を定義しようとしたときに問題が発生しました。我々JIS民は@って@を押せば出るんですよ。でも世界民はShift+2なんですよね。で、世界民はJIS民が@と呼ぶキーは[と呼ぶんですよね。

 なるほど、じゃぁ、JavaFXも@は[と呼ぶから困ると私は言いたいのか。残念ながらもっと面倒で、何故か`と判定されました。KeyCodeにATって定義されてるくせに…

 これだと、ユーザーが柔軟にキーバインドを定義するようなアプリケーション作れねーじゃん、ということで、emacsのキーバインドのように柔軟な表現で表せるようにしようと思ったわけです。柔軟な、というのは例えばJISで「C-S-@」または「C-`」は「Control+Shift+@」であり、USなら「C-S-@」は「Control+Shift+2」である等、柔軟な対応がしたいということです。



断念した実装

 キー入力「Ctrl+@」をKeyCodeという列挙型で判断しようとすると、Ctrl+バッククォートになる為、KeyCodeのみに頼ることは出来ない。しかし、KeyEventのgetText()を利用すると、このKeyCodeはバッククォートでも「@」という文字列が返ってくる。これとKeyCodeの二つ両方利用して何とかしようとした。

 これでかなり柔軟に対応するところまでは出来ました。しかし、「Ctrl+Shift+@」というショートカットを定義することが現状できないという結論に至りました。Shift+@によりgetText()の値は「`」になり、@かどうか判別することが出来ない為です。

 キーボードがJISなのか、USなのか(はたまた別の言語なのか)判断して処理を分けれればいいのですが、私にはやり方分からないし、っていうか分かっても全部やるとか面倒くさくてあり得ないし。

 というわけで、私のもくろみは散ってしまったとさ。


 でもまぁ、使い方を気をつければ、JavaFXのKeyCombinationよりは柔軟な物を作ることは出来たんじゃないかな。↓

package nodamushi.event;

import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

public class MyKeyCombination implements Cloneable{
    
    //修飾キーの値
    private static final int CONTROL=1,ALT = 1<<1,META = 1 << 2,SHIFT = 1 << 3;
    //よく分からんキーのコード
    private static final int UndefinedKeyCode = -10000;
    
    
    private static int getModifiers(boolean control,boolean alt,boolean meta,boolean shift){
        return (control?CONTROL:0) | (alt ? ALT :0) | (meta?META:0) | (shift? SHIFT:0);
    }
    
    private int modifiers;
    private int keycode=UndefinedKeyCode;
    
    
    
    public boolean match(MyKeyCombination kc){
        return modifiers==kc.modifiers&&keycode==kc.keycode;
    }
    
    @Override
    public boolean equals(Object obj){
        return (obj==this)?true:(obj instanceof MyKeyCombination)? match((MyKeyCombination)obj):false;
    }
    
    //for clone
    private MyKeyCombination(int modifiers ,int keycode){
        this.modifiers=modifiers;
        this.keycode=keycode;
    }
    
    public MyKeyCombination(boolean control,boolean alt,boolean meta,boolean shift,String key){
        if(key.length()==1){
            int ch = key.charAt(0);
            keycode = ch;
            if('A'<=ch && ch<='Z'){
                shift = true;
                keycode = ch+'a'-'A';
            }
            modifiers = getModifiers(control, alt, meta, shift);
        }else{
            modifiers = getModifiers(control, alt, meta, shift);
            switch(key.toLowerCase()){
                case "enter":
                    keycode = 13;break;
                case "tab":
                    keycode = 9;break;
                case "space":
                case "sp":
                    keycode = 32;break;
                case "pageup":
                case "pgup":
                case "pup":
                    keycode = -33; break;
                case "pagedown":
                case "pgdown":
                case "pdown":
                    keycode = -34; break;
                case "end":
                    keycode = -35; break;
                case "home":
                    keycode = -36;break;
                case "left":
                    keycode = -37;break;
                case "up":
                    keycode = -38;break;
                case "right":
                    keycode = -39;break;
                case "down":
                    keycode = -40; break;
                case "f1":
                    keycode=-112;break;
                case "f2":
                    keycode=-113;break;
                case "f3":
                    keycode=-114;break;
                case "f4":
                    keycode=-115;break;
                case "f5":
                    keycode=-116;break;
                case "f6":
                    keycode=-117;break;
                case "f7":
                    keycode=-118; break;
                case "f8":
                    keycode=-119;break;
                case "f9":
                    keycode=-120;break;
                case "f10":
                    keycode=-121;break;
                case "f11":
                    keycode=-122;break;
                case "f12":
                    keycode=-123;break;
                case "f13":
                    keycode=-124;break;
                case "f14":
                    keycode=-125;break;
                case "f15":
                    keycode=-126;break;
                case "f16":
                    keycode=-127;break;
                case "f17":
                    keycode=-128; break;
                case "f18":
                    keycode=-129;break;
                case "f19":
                    keycode=-130;break;
                case "f20":
                    keycode=-131;break;
                case "f21":
                    keycode=-132;break;
                case "f22":
                    keycode=-133;break;
                case "f23":
                    keycode=-134;break;
                case "f24":
                    keycode=-135;break;
                default:
                    keycode = UndefinedKeyCode;
                    modifiers=0;
                    break;
            }
        }
    }
    
    public boolean match(KeyEvent event){
        KeyCode code = event.getCode();
        String text = event.getText();
        boolean isC=event.isControlDown();
        boolean isA=event.isAltDown();
        boolean isS=event.isShiftDown();
        boolean isM=event.isMetaDown();
        int modifiers,keycode=UndefinedKeyCode;

        if(code.isModifierKey() || code == KeyCode.UNDEFINED){//shiftなど
            return false;//常にfalse
        }
        
        if(code.isLetterKey()){//a-z
            modifiers = getModifiers(isC, isA, isM, isS);
            keycode = code.ordinal()-KeyCode.A.ordinal()+'a';
        }else if(code.isFunctionKey()){//F1,F2,F3....,F24
            keycode = -112 - (code.ordinal()-KeyCode.F1.ordinal());
            modifiers = getModifiers(isC, isA, isM, isS);
        }else if(code.isNavigationKey()){//homeや矢印など
            if(code.isArrowKey() && code.isKeypadKey()){//キーパッドの矢印
                keycode = -33 - (code.ordinal()-KeyCode.KP_UP.ordinal());
            }else{//それ以外
                keycode = -33 - (code.ordinal()-KeyCode.PAGE_UP.ordinal());
            }
            modifiers = getModifiers(isC, isA, isM, isS);
        }else if(code.isWhitespaceKey()){//enter,tab,space
            switch(code){
                case ENTER:
                    keycode = 13;break;
                case TAB:
                    keycode = 9;break;
                case SPACE:
                    keycode = 32;break;
            }
            modifiers = getModifiers(isC, isA, isM, isS);
        }else if(code.isMediaKey()){//音量上げ下げのボタンとからしい
            //よく分からんから現状放棄
            return false;
        }else if(code.isDigitKey()){//数値
            if(code.isKeypadKey()){
                keycode = '0'+(code.ordinal()-KeyCode.NUMPAD0.ordinal());
            }else{
                keycode = '0'+code.ordinal()-KeyCode.DIGIT0.ordinal();
            }
            modifiers = getModifiers(isC, isA, isM, isS);
            if(keycode==this.keycode && modifiers==this.modifiers)
                return true;
            
            //↓例外処理。例えばShift+1はjisでは「!」など数字ではない。
            if(text.length()==1 && keycode!=text.charAt(0)){
                isS=false;
                keycode = text.charAt(0);
            }
            modifiers = getModifiers(isC, isA, isM, isS);
            return keycode==this.keycode && (modifiers|SHIFT)==(this.modifiers|SHIFT);
        }else{//わからん
            
            //TODO ここどーしよう。
            //現状の問題
            //たとえば、jis配列でCtrl+Shift+@を判定したい場合
            //KeyCodeはback_quoteが返ってくるし、textは`になってしまっている
            //要するに、Shift+@は取得できない
            
            
            
            //(↑が実装できた上での話)
            //↓マッチしなかった場合、Shiftにより文字が変わっていることを考慮する
            int tlength = text.length();
            if(tlength == 1){
                char ch = text.charAt(0);
                keycode = (int)ch;
                modifiers = getModifiers(isC, isA, isM, isS);
                return keycode==this.keycode && (modifiers|SHIFT)==(this.modifiers|SHIFT);
            }else{
                //それでも判別できない物は扱わない。
                return false;
            }
        }
        
        
        return keycode==this.keycode && modifiers==this.modifiers;
        
        
    }
    
    public MyKeyCombination clone(){
        return new MyKeyCombination(modifiers, keycode);
    }
}

2013/09/11

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を使ってみた結果です。

f:id:nodamushi:20130912052643j:image

2013/09/10

JavaFXでのダイアログ作成

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

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

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

f:id:nodamushi:20130910103418j:image:w360

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



 ウィンドウ形式は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でダイアログを作る方法”

2013/07/29

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



実行結果

f:id:nodamushi:20130729175051p:image

2013/02/19

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でちゃちゃっと作って、後はコントローラーに書くことにします。

f:id:nodamushi:20130219181825p:image:w320

 この青い円を、左右の四角形の中に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<MouseDragEvent>(){………}と書くのが面倒くさかったので、一つの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);
  }
}



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

f:id:nodamushi:20130219181826p:image:w320



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

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

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

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