読者です 読者をやめる 読者になる 読者になる

プログラムdeタマゴ

世界の端っこ

JavaScriptで作ったちょっとしたスクリプトからファイルを保存したい

 JavaScriptはちょこっと何かを作ろうと思うと、一番使いやすいと思っている。なんせ特に何もいらない。エディタとブラウザがあればGUIを持った簡単なスクリプトなんかすぐ作れる。最近は簡単な物ならブラウザ内で全部完結して、エディタすらいらない。
 Excelマクロを習ってGUIの作成を習うよりずっと簡単だと私は思っている。なお、異論は認める。

 が、このJavaScriptは簡単ではあるが、ファイルの保存が出来ない。File APIもChromeしかない。
 私ならJavaScriptで何かして保存したいときはGreasemonkey使ったり、keysnail使ったりするって手段もあるっちゃある。
 でも、あまりプログラミングに詳しくない人が簡単に作って試して保存まで勢いで出来なければ意味が無い。


 だが、そんな方法はない。と、思っていたら意外とHTML5には別の手段で保存する方法があるようだ。
 以下のsaveTextを実行するだけで、とりあえずローカルに保存できる。ただし、ブラウザ設定によっては何も聞かずにDownloadフォルダに勝手に保存しちゃうのが玉にキズだけどね。

function saveText(text, fileName) {
  var a, blob, event;
  if (fileName == null) {
    fileName = "textfile.txt";
  }
  blob = new Blob([text], {
    type: "text/plain"
  });
  if (window.navigator.msSaveBlob != null) {
    window.navigator.msSaveBlob(blob, fileName);
  } else {
    a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.target = "_blank";
    a.download = fileName;
    event = document.createEvent("MouseEvents");
    event.initEvent("click", false, true);
    a.dispatchEvent(event);
  }
};


CoffeeScriptバージョン

saveText = (text,fileName)->
  if !fileName? then fileName = "textfile.txt"
  
  blob = new Blob [text],{type:"text/plain"}
  if window.navigator.msSaveBlob? then window.navigator.msSaveBlob blob,fileName
  else
    a = document.createElement "a"
    a.href = URL.createObjectURL blob
    a.target = "_blank"
    a.download = fileName
    event = document.createEvent "MouseEvents"
    event.initEvent "click",false,true
    a.dispatchEvent event
  return


参考:
JavaScriptだけでファイルの保存機能を実装する - 新人Webエンジニアの記録。
JavaScriptのクリックイベントを発火させる方法 - ゆうなんとかさんの雑記帳的な。yuuxxxx.hatenablog.com





ちょっと根性あるならnode-webkitという選択肢も

 去年ぐらいからちょくちょく耳にすることがあるnode-webkit。要するにHTML5とJavaScriptとそれの実行環境をひとまとめにしてアプリケーションにしましょうというものですかね。
 node.jsの機能が使えるのでファイルの保存とか出来ます

 詳しくはよく知らないけどね!(え 個人的には単体で動くブラウザのプラグインだと思っています。

 liginc.co.jp




まだ根性あるなら好みの関数を持つブラウザ作っちゃおうぜ

 回りくどいことせずに、JavaScriptからダイレクトに保存関数を呼べるブラウザを自作しちゃうってのも一つの手だ(え?


 例えば、Qtとか、JavaFXにはWebEngineがあるから、それを使うと、意外に簡単にJavaScriptで保存することが出来るブラウザを作れる。
 まぁ、確かに、最初のブラウザを作るのはちょい骨折れるけど、一回作ってしまったら、node-webkitの用に毎回アプリケーションにする必要なんて無いぞ!これは楽だ!




 ………いや、やっぱそれぐらいならJavaやらC#やらExcel VBAを覚えた方が絶対早いよ




 最後に載せたJavaFXのWebEngineを使った例に次のHTMLファイルを開かせると、ファイルの保存画面が開いてテキストファイルが保存される。





 さぁ、私の匙は投げられた。好きな方法を選ぶがよい。


test用HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script>
Files.saveText("内容");
</script>
</body>
</html>


自作ブラウザ

import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.util.*;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.web.*;
import javafx.stage.*;
import javafx.stage.FileChooser.ExtensionFilter;
import netscape.javascript.JSObject;

public class Browser extends Application{

  public static void main(final String[] args){
    System.setProperty("prism.lcdtext", "false");
    launch(args);
  }
  private WebView view;
  private TextField url;

  @Override
  public void start(final Stage stage) throws Exception{
    final WebView v = view=new WebView();
    JSFiles.install(v);

    final TextField url = this.url = new TextField();
    final Button open = new Button("Open");
    url.setOnAction(this::openURL);
    open.setOnAction(this::openURL);

    final BorderPane p = new BorderPane(v);
    HBox.setHgrow(url, Priority.ALWAYS);
    p.setTop(new HBox(url,open));
    p.setPrefSize(800, 600);

    stage.setScene(new Scene(p));
    stage.show();
  }


  public void openURL(final ActionEvent e){
    final String text = url.getText().trim();
    if(text == null) {
      return;
    }

    String url;
    try{
      url = Paths.get(text).toUri().toURL().toString();
    }catch(final Exception e1){
      url = text;
    }


    final WebEngine engine = view.getEngine();
    if(url.equals(engine.getLocation())){
      engine.reload();
    }else{
      engine.load(url);
    }

  }



  public static class JSFiles{

    public static void install(final WebView v){
      final JSFiles jsf = new JSFiles(v);
      final WebEngine engine = v.getEngine();
      final JSObject window = (JSObject)engine.executeScript("window");
      window.setMember("JavaFiles", jsf);

      final JSObject eval = (JSObject)engine.executeScript(
          "var Files = {};"
          + "Files.saveText = function(str,charset){"
          + "JavaFiles.__saveText__(str,charset);"
          + "};");
      System.out.println(eval);
    }

    private WebView view;

    public JSFiles(final WebView view){
      this.view = view;
    }


    public boolean __saveText__(final String str,final String charset){
      final FileChooser chooser = new FileChooser();
      chooser.getExtensionFilters().addAll(TEXTFILTER,ALL);
      final Window w = view.getScene().getWindow();
      final File file = chooser.showSaveDialog(w);

      if(file!=null){

        Charset c =null;
        try{
          if(charset != null){
            c= Charset.forName(charset);
          }
        }catch(final Exception e){}
        if(c == null) {
          c = Charset.defaultCharset();
        }

        @SuppressWarnings("resource")
        final Scanner scan = new Scanner(str);


        final Iterator<CharSequence> i = new Iterator<CharSequence>(){
          @Override
          public CharSequence next(){
            return scan.nextLine();
          }
          @Override
          public boolean hasNext(){
            return scan.hasNextLine();
          }
        };

        try {
          Files.write(file.toPath(),()->i, c);
          return true;
        } catch (final IOException e) {
          e.printStackTrace();
          return false;
        }
      }else{
        return false;
      }
    }
    private static ExtensionFilter TEXTFILTER=new ExtensionFilter("*.txt", "*.txt");
    private static ExtensionFilter ALL=new ExtensionFilter("*", "*");
  }

}

ファイル選択のControl作った

 ウィーっす。
 さて、昨日は入力候補が出るTextFieldを作ってみました。nodamushi.hatenablog.com

 で、これを利用してファイル選択用のConrolを作ってみました。
 こんな感じに動作します。
f:id:nodamushi:20150814173654p:plain

 うん、使いやすい。私はやっぱりマウスでクリックするより、キーボードで打ち込んだ方が楽だからねー。
 昨日と同じくソースコードはこちら。NodamushiFXControls
 クラス名はnodamushi.jfx.path.PathChooserが今回のControlです。

 FileChooserとかDialogChooserとか自分で操作しなくても勝手にやってくれます。
 もともとこれを作りたいが為に、CompletionTextFieldを作りました。こう言うのって、ありそうで、検索してみても出てこなかったんだよね。
 (既にあったらすみません)






 しかし、まー、だいたい、目的の動作はするんだけど、まだ微妙にバグってたりはします。
 時々ListViewが上手くレンダリングされなかったり、ScrollBarが表示されているかどうかの判定にミスってるみたいなんだよねぇ。
 f:id:nodamushi:20150814175004p:plain
 f:id:nodamushi:20150814175006p:plain

 どっちも常に起こるわけじゃないんだよね。その発生条件もよく分かってない。
 直し方が分からんし、根性が切れたので、ここでいったん公開して、記事にしました。
 

入力候補が出るTextField作ってみた

 夏風邪ひいて熱が出たり咳が出たり鼻水出たりでしんどいですが、皆さん体調いかがでしょうか。


 さて、風邪ひいていようと暇なものは暇なので、タイトル通りの物を作っていました。
 イメージとしてはこんなのね。(Firefoxの検索窓)
f:id:nodamushi:20150813230113p:plain:w240

 ソースコードはこちら。NodamushiFXControls
 クラスは CompletionTextFieldです。
 候補や挿入方法はCandidateインターフェースで定義してあるだけなので、よく言えば自由がある、悪く言えばユーザーが用意しないといけない。

 デモ用に作ったCandidateインターフェースの実装(ForwardMatchCandidate)ではこんな感じに動作します。
f:id:nodamushi:20150813230150p:plain:w240
f:id:nodamushi:20150813230152p:plain:w240
f:id:nodamushi:20150813230154p:plain:w240





自作PopupControlの作り方

 意外とここの部分でつまった。
 最も単純な方法では、TooltipのGraphicに表示させたいNodeを突っ込んで表示するってのが楽そうだけど、やっぱそれだと微妙だよね。
 Tooltipの元となっているPopupWindowないし、PopupControlから派生させたい物です。


 今回はPopupControlにNodeを表示させる方法を調べたところ、単純にSkinを登録するだけでした。
 私が作ったコードではこんな感じ。ComboBoxSkinのコードを参考にしています。(ていうか、ほとんどここはまんまか)

    final PopupControl p = new PopupControl(){
      @Override
      public Styleable getStyleableParent(){
        return getSkinnable();//ポップアップ元のノードを返しています
      }
      {
        setSkin(new Skin<Skinnable>(){
          @Override public Skinnable getSkinnable(){return CompletionTextFieldSkin.this.getSkinnable();}
          @Override public Node getNode(){return getPopupContent();}
          @Override public void dispose(){}
          });
      }
    };





JavaFX8ではListViewの行の高さを取得できなかった

 ListViewを表示するときに、ある一定の行までは表示して、それ以上はスクロールバーで対応させたい。
 つまり、n行を表示するための高さを取得して、それをPrefHeightとすれば良いのだが、この1行の高さというのが実は取得できない。

 でも、ComboBoxとかではそれを実現しているように見える。ではどうやっているのかとソースコードを調べ、最終的に次のコードに落ち着いた。

  private static final Class<?> VIRTUALCONTAINERBASE;
  private static final Method
  GET_VIRTUAL_FLOW_PREFERRED_HEIGHT,//n行分の高さを取得するメソッド
  UPDATE_ROW_COUNT;//今何行分のアイテムがあるのかSkinに強制的に更新させる
  static{
    Class<?> c;
    Method m,m2;
    try {
      c=Class.forName("com.sun.javafx.scene.control.skin.VirtualContainerBase");
      m = c.getDeclaredMethod("getVirtualFlowPreferredHeight", int.class);
      m2 =c.getDeclaredMethod("updateRowCount");
      m.setAccessible(true);
      m2.setAccessible(true);
    } catch (final Exception e) {
      c = null;m=null;m2=null;
    }
    VIRTUALCONTAINERBASE=c;
    GET_VIRTUAL_FLOW_PREFERRED_HEIGHT=m;
    UPDATE_ROW_COUNT=m2;
  }


  private double getListViewPrefHeight() {
    double ph;
    final int maxRows = min(textField.getVisibleRowCount(),getCandidateSize());
    if (VIRTUALCONTAINERBASE!=null &&listView.getSkin()!=null &&
        VIRTUALCONTAINERBASE.isAssignableFrom(listView.getSkin().getClass())) {
        try{
          //これ↓を挟まないと次のgetVirtualFlowPrefreredHeightでちゃんと高さが出なかった。
          UPDATE_ROW_COUNT.invoke(listView.getSkin());
          ph =(double) GET_VIRTUAL_FLOW_PREFERRED_HEIGHT.invoke(listView.getSkin(), maxRows);
        }catch(final Exception e){
          final double ch = maxRows * 25;
          ph = Math.min(ch, 200);
        }
    } else {
      final double ch = maxRows * 25;
      ph = Math.min(ch, 200);
    }
    return ph;
  }


 う~ん、リフレクションかぁ。com.sunパッケージかぁ(´Д⊂ヽ

 とりあえず、これに期待するしかないっすね。。。いったいどの程度JavaFX9でPublic APIが増えるんでしょうかね。
 
JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization



 特に落ちもまとめもありませんが、以上。

JavaFXのTooltipの挙動を変えるTooltipBehavior作った話

 JavaFXでTooltipを使ったことがあるだろうか?
 うん、みなまで言わなくてもいい。

  使いにくいよね


 ざっと欠点を挙げるとするならば、

  1. 表示されるまでが遅い
  2. 表示されるまでの時間を変えられない
  3. 表示時間が短い
  4. 表示時間を変えられない
  5. フォーカス持っていないウィンドウでもポップアップする。ついでにウィンドウが一番手前に来る


 屋上へ行こうぜ…久しぶりに…キレちまったよ…な状態になってしまったのは一番最後の項目。
 この症状は複数のWindowを一つのApplicationで開いているときに起こる。(なお、他のプロセスのウィンドウより前に来ることはないっぽい。)
f:id:nodamushi:20150805231216p:plain:w250

f:id:nodamushi:20150805231241p:plain:w250

 これを解消する方法がどうしてもわからんかった。次善策として、フォーカス持っていないウィンドウではせめてポップアップしないようにしたい。


自作TooltipBehaviorを作ってみることにした

 JavaFXにおいて、Tooltipの挙動を決定しているのはTooltipBehaviorというクラスです。
 このTooltipBehaviorはTooltipのシングルトンな内部クラスで、唯一のTooltipBehaviorが色々とEventHandlerを登録したり、今どれが表示されてて、どれが表示されようとしているのかとかを管理しています。
 基本的な処理はこんなかんじ。

  • マウスがホバーしたらactivateタイマーを起動して、時間差でポップアップさせる
  • ポップアップしたら、他に表示されているTooltipは非表示にする
  • ポップアップしたらhideタイマーを起動し、時間差でポップアップを非表示にする
  • マウスが対象ノードから出た場合は、activate,hideタイマーは停止し、現在対象のTooltipが表示中の場合はleftタイマーを起動して時間差でポップアップを非表示にする。
  • マウスプレスされたら全てのタイマーを止め、Tooltipを消す


 この動作を基本として、次の機能を持つ自作TooltipBehaviorを作ることにした。

  1. Tooltip以外のPopupWindowにも使える
  2. フォーカスを持たないウィンドウからもポップアップするかどうか変更できる
  3. 「時間差」を変更することが出来る
  4. 一つのTooltipインスタンスを使い回すことが出来る
  5. 複数の自作TooltipBehaviorインスタンスをまたいで一つだけTooltipが表示できるようにする

というわけで、なんかできた

 成果がこちら。なお、JavaFX8以上が対象。
TooltipBehavior/SinglePopupBehavior.java at master · nodamushi/TooltipBehavior · GitHub
TooltipBehavior/TooltipBehaviorBase.java at master · nodamushi/TooltipBehavior · GitHub
TooltipBehavior/TooltipBehavior.java at master · nodamushi/TooltipBehavior · GitHub

 著作権は放棄してる(パブリックドメイン)のでどなたでも利用改変して問題ありません。
※なお、一部Tooltipの動作がオリジナルのTooltipBehaviorが保持するマウス座標の値に依存している部分があるので、オリジナルのTooltibBehaviorを使わずにTooltipを利用すると、おかしな挙動を示す場合があると思います。
 ただし、その場面はポップアップしているときに、テキストの内容を変更した場合だけのようなので、基本的には問題ないんじゃね、たぶん。



 これらのデモ実行のコードはこちら。
TooltipBehavior/test at master · nodamushi/TooltipBehavior · GitHub

 自作TooltipBehaviorの使い方は至って極シンプルで、最初にTooltipBehaviorのインスタンスを用意しておく以外はTooltip.installを使うのと変わらない

final TooltipBehavior behavior = new TooltipBehavior();
//マウスが乗ってから0.1秒後に表示
behavior.setOpenDuration(new Duration(100));
//ずっと表示
behavior.setHideDuration(Duration.INDEFINITE);
//マウスが放れてから0.3秒後に非表示
behavior.setLeftDuration(new Duration(300));

//………中略………

final Rectangle r = createNode(size);
//色をツールチップで表示する
final Tooltip tooltip = new Tooltip(r.getFill().toString());
//インストール Tooltip.install(r,tooltip)と同じ
behavior.install(r, tooltip);


 BehaviorTestを実行してみるとこんな画面が出てくる。Tooltip.installを用いたときと(表示スピードが違う以外)同じ動作をする。
f:id:nodamushi:20150805235025p:plain



 BehaviorTestでは色のついたRectagleを作るたびにTooltipも生成している。
 今回はたかが色コードのテキストを表示しているだけなのに、無駄だよね。できるなら一つのTooltipで表示できた方が良いはずだ。


 というわけで、それができるよ、と言うことを示すのがBehaviorTest2。

    //表示前にTooltipの内容をColorの値に更新させる
    behavior.setPopupUpdater((final Tooltip t,final Node n)->{
      if(n instanceof Rectangle){
        t.setText(((Rectangle)n).getFill().toString());
      }
    });

    for(int y=0;y<l;y++) {
      for(int x=0;x<l;x++){
        final Rectangle r = createNode(size);
        g.add(r, x, y);

        //インストール Tooltipは一つのインスタンスを使い回す
        //(TooltipBehaviorがデフォルトのTooltipを持っている)
        behavior.install(r);
      }
    }


 上のように、BehaviorTest2では、Rectangleを作るたびにTooltipを作成すると言うことはしていない。
 代わりに、setPopupUpdaterというメソッドに渡しているラムダ式の中でTooltipの文字列を更新しているっぽい。まぁ、要するにCellと同じ考え方だね。
 むろん、これもちゃんと同じように動作します。

f:id:nodamushi:20150805235834p:plain

 Tooltipを更新するフックの他に、表示するかどうかのフックも用意してあるので、結構自由度が高くて個人的には使いやすいです。




 ところで、自作TooltipBehaviorはオリジナルのTooltipBehaviorと違い、Singletonではないので、複数のBehaviorを定義できます。
 なので、複数のBehaviorを同時に利用すると、こんなことになっちゃったりします。

f:id:nodamushi:20150806000425p:plain


 こりゃ困りまんねん。
 というわけで、自作TooltipBehaviorではGroupという概念で、複数のTooltipBehaviorにまたがっても一つしかTooltipを表示させないように出来ます。

    final TooltipBehavior behavior1 = new TooltipBehavior();
    final TooltipBehavior behavior2 = new TooltipBehavior();

    //BehaviorGroupに登録すると、一つのBehaviorGroupに登録された
    //Behaviorの中では、ポップアップするTooltipが常に一つになります
    final BehaviorGroup group = new BehaviorGroup();
    group.addAll(behavior1,behavior2);

 これをしておくと、問題なく表示されるようになります。

f:id:nodamushi:20150806000712p:plain

 
 


CSSで指定できた方がよくね?

 ここまで作って、欲が出たのか、CSSで指定したくなった。だって、Behavior単位で時間管理するより、Tooltip単位で時間管理できた方が楽に決まってる。

 というわけで、表示までの時間等をCSSで指定できるようにプロパティを追加したTooltipのサブクラスを……… 

    private final class CSSBridge extends PopupControl.CSSBridge {
        private Tooltip tooltip = Tooltip.this;

        CSSBridge() {
            super();
            setAccessibleRole(AccessibleRole.TOOLTIP);
        }
    }

(Tooltip.javaより引用)

 ( ゚д゚)作れねえええええええ CSSBridgeが見えねぇええ




 リフレクションに頼るかどうか悩んだ末、Tooltipを丸コピして、必要なプロパティやBehaviorを追加することにした

TooltipBehavior/NTooltip.java at master · nodamushi/TooltipBehavior · GitHub


 うん、まぁ、ねぇ。一応たぶん、動くんだけどね。著作権とかライセンス的にどうなんだろうね、これね
 こいつのSkinに至っては、package名と名前以外完全にコピーだからね。やばいね。よって、NTooltipとNTooltipSkinの著作権は放棄してないよ。たぶん、OpenJDKのライセンスに沿うんだと思うよ。




 というわけで、最後はちょっとあれな感じだけど、結構便利なものが出来たんじゃないかと思ってるよ。

JavaFXのWebViewがマウスイベントでJVMごとクラッシュする

 いろんな訳あってブレークポイントをつけれるエディタを内蔵したいのだ。でも、JavaFXには良いテキストエディタがない。(だいたい、いつになったら全角文字がなんか変な色がつくの直るんだ?)
 
 Threadをまたぐのは正直面倒くさいけど、背に腹は代えられない。SwingNodeでつっこんでやるから、Swingでもいいや、と思ったのだが意外にもSwingも全然ねぇ。


 なら、JavaScriptでもいいよ!ということで、検索してみたら見つけました素晴らしいエディタ。その名もAce

 ブレークポイントあり、Emacsキーバインドあり(超高評価)の素晴らしいエディタです。これだ!これを突っ込もう。


 というわけで、突っ込んでみました。

f:id:nodamushi:20150714231705p:plain

import java.nio.file.Path;
import java.nio.file.Paths;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.web.WebView;
import javafx.stage.Stage;


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


  @Override
  public void start(final Stage s) throws Exception{
    final WebView v = new WebView();
    final Path p = Paths.get("ace","editor.html");
    s.setScene(new Scene(v));
    v.getEngine().load(p.toUri().toURL().toString());
    s.show();
  }

}

 一見問題なさそうに見えます。が、コピー&ペーストができません。
 そして、何よりも問題なのは、マウスクリックしていると、突然不意に落ちること。

f:id:nodamushi:20150714231938p:plain


 残されたログを見ても、やっぱりマウスイベントで落ちているようです。
f:id:nodamushi:20150714232108p:plain


 JRE8u45でも試してみましたが、結果は同じでした。


 むろん、これをFirefoxやChromeで動かしても特に問題は起こりません。
 コピペが出来ないだけだったら、JavaScriptを駆使して何とかなるかなとか思ってたんですが、JVMごとクラッシュするとなると流石にどうもならない。





 これがAceだけに限る問題なのか、WebViewそのものの問題なのか、ちょっと判断がつきませんが、WebViewもまだまだ不安定なんだなと思い知らされました。
 バグレポート?めんどくせ



 一応検索用にエラーコード貼り付け。

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x0000000000000000, pid=7052, tid=6804
#
# JRE version: Java(TM) SE Runtime Environment (8.0_40-b25) (build 1.8.0_40-b25)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.40-b25 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# C  0x0000000000000000
#
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
#

Graphicを設定したRadioButtonをSkinで使うと表示がおかしくなる

 ControlのSkinにRadioButtonを使う。RadioButtonには画像も表示させたい………。そんなに特別な場面ではないだろう。ところが、おそらくJavaFX8のバグと思われる妙な挙動をしたので、ここで報告しておく。すでにバグとして登録されているかはよく知らない。

 問題となるソースコードは次のようなもの。基本的には、TilePaneを拡張し、子要素に二つのRadioButtonを並べたSkin2というクラスを4つ表示しているだけだ。
 ただし、この二つのRadioButtonには、GraphicとしてAというラベルとBというラベルを設定している。

//import は略
public class Test extends Application{

  @Override
  public void start(final Stage stage) throws Exception{
    //-fx-skinでSkin2をskinに指定
    final Control2 c2 = new Control2();
    //createDefaultSkinでskinを指定
    final Control3 c3 = new Control3();
    //直接Skin2を表示
    final Skin2 s = new Skin2(null);
    //もう一回表示
    final Control2 c22 = new Control2();

    final VBox v = new VBox();
    v.setPadding(new Insets(50));
    v.getChildren().addAll(c2,c3,s,c22);
    stage.setScene(new Scene(v));
    stage.show();
  }

  public static class Control2 extends Control{
    public Control2(){
      setStyle("-fx-skin:'"+Skin2.class.getName()+"'");
    }
  }

  public static class Control3 extends Control{
    @Override
    protected Skin<?> createDefaultSkin(){
      return new Skin2(this);
    }
  }

  public static class Skin2 extends TilePane implements Skin<Control>{
    Control c ;
    public Skin2(final Control c){
      this.c = c;
      final RadioButton
      r1 = new RadioButton("a"),
      r2 = new RadioButton("b");
      r1.setGraphic(new Label("A"));
      r2.setGraphic(new Label("B"));
      getChildren().addAll(r1,r2);
      r1.setSelected(true);
    }

    @Override
    public Control getSkinnable(){return c;}
    @Override
    public Node getNode(){return this;}
    @Override
    public void dispose(){}
  }

  public static void main(final String[] args){launch(args);}
}

これを実行すると次のようになる。
f:id:nodamushi:20150302185844p:plain

最初と最後が明らかにおかしい。ボタンはないし、●は変なところに表示されているし、aと表示されていない。しかし、二つ目の「○B b」は正しく表示されている。

Skinで表示すると問題なのかと思ったが、createDefaultSkinを用いると、あら不思議、二列目のように正しく表示されている。ということは、リフレクション関連のバグ?

というわけで、どーいうことなんですかねぇ、これ。

ToggleButtonの集合を簡単に扱えるようにしてみた

 はてなにお布施の時期が過ぎていたので、ついでにはてなブログに移行してみました。プロの値段…高いよ………。

 さて、そんなこんなで、今日もJavaFXネタ。
 RadioButtonって複数のRadioButtonを並べて表示するって使い方が普通の場面だと思う。でも、これをするには

  1. 複数のRadioButtonを用意して
  2. ToggleGroupを用意して
  3. ToggleGroupにRadioButtonを追加して
  4. レイアウト用PaneにRadioButtonを代入して
  5. ToggleGroupのselectedToggleプロパティを監視して必要な値に変換させる

 ってな感じで面倒くさい

 というわけで、もうどっかにあるんだろうけど、この手順を簡略化するクラスを作ってみました。(長いので続きに。もしくはToggleButtons.java)

 で、それを使うと以下のように短く書けます。

//importは略
public class Test extends Application{

  public static void main(final String[] args){launch(args);}

  @Override
  public void start(final Stage primaryStage) throws Exception{
    final String[] fileList={"None Selected",
        "Lenna","Mandrill","Aerial","Earth","Girl","Parrots","Pepper","Siboat"};
    final ToggleButtons buttons = new ToggleButtons();
    //画像付きRadioButtonを追加
    for(int i=1;i<fileList.length;i++){
      final String fname = fileList[i];
      buttons.createItem(fname,Test.class.getResourceAsStream(fname+".jpg"));
    }
    //選択されているToggleButtonは数値になっている
    buttons.selectedProperty().addListener(new InvalidationListener(){
      @Override
      public void invalidated(final Observable observable){
        System.out.println(fileList[buttons.getSelected()+1]);
      }
    });

    primaryStage.setScene(new Scene(buttons.asFlowPane()));
    primaryStage.show();
  }
}

 実行するとこんな感じ
 f:id:nodamushi:20150301174913p:plain

続きを読む