プログラムdeタマゴ

世界の端っこ

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

続きを読む

JavaFXのLineChartがわりと高性能になった件

 ども、一ヶ月ぐらい前に作った自作のLineChartをちまちま更新していたら、何か結構高性能になったんじゃね?ってことで、動画にとって見ましたのでご紹介。
 機能としては、「データを拡大して、スライドさせることが出来る」「マウスポイントに追従してデータの位置を取得できる」「複数のLineChartの端を一致させたまま配置できる」「複数のLineChartでスクロールバーやマウスポイントを同期させられる」「大量のデータを入れてもとりあえず固まらない」あたりです。
NodamushiChart(Github)


 とりあえず、動画とか初めてキャプチャして、それをそのままYoutubeにぶち込んだだけだから画質とか酷いけど、気にしないで。てか、相当昔に取ったnodamushiのアカウントに付けてた名前が田中太郎って宇宙人かよ。(ネタ分かる人居るのかな)


JavaFXで対数グラフを表示する

 と、いうわけで、前回の自作LineChartに対数グラフの機能を実装しますた。
 NodamushiChart
 
 拘ったのはラベルのとこ。102みたいな感じに表示されていますが、これ、JavaFXの標準NumberAxisじゃまず無理ですよね。うぇへへへ。

 LogarithmicAxisの実行サンプルはtest.Test2で見ることが出来ます。

 

JavaFXでLineChartをスクロールしたり高性能にする

やろーども。滅入り苦しみます。

 と、いうわけで、全国約80人のJavaFXユーザーにnodamushiサンタがナイスノンケなプレゼントをもってやってきたぜぃ。


 さて、妙なノリもここまでにして、JavaFXのLineChartって使いにくい、というのが私の結論です。グラフ描画領域内のマウスイベントは取れない、double[]配列でデータあるのをいちいちDataに変換しないといけない、Axisをスクロールさせて描画領域を変化させることも出来ない。複数のグラフを並べて表示させることが出来ない。その上妙なアニメーションやらまじうぜぇ。



 で、これらを解決すべく色々模索した結果………


 LineChart自作してまいました( ゚д゚)メリークリスマス



ソースコード:NodamushiChart


 あ〜、車輪の再発明糞面倒くさいぜー

 まだバグバグしてるし、欲しい機能(マウスイベント、表示範囲の変更など)全てを実装し終わったわけじゃないけど、こんな感じで動きます。(クリスマス前に記事を投稿したかったんじゃー(>_<) )

 図はsinc関数を表示した物です。sinc(0)=1ですが、テストでsinc(0)=∞にしています。このグラフは∞やNaNを自動的に判断して、無限だったら図のような垂直な線を描画、NaNだったら線を切断して表示してくれます。
 また、JavaFXのXYChartの様にグラフの表示エリアをオブジェクトの持つGroupインスタンスの中で構築し、外に出さない様な仕様と違い、グラフ表示領域は一つのクラスとしてつくってあるので、複数のグラフを整頓して配置することも容易に出来るように作ってあります。

 数値解析のような大量のデータをグラフに描画して、観察したい、という用途なら既に結構十分動きます。ソースコードは上記Githubにおいておくのでご自由に使って遊んでみてください。とりあえず、test.Testを実行すれば上の図が出てきます。ライセンスはCC0で著作権とその行使は放棄しているので、好きなように改変、利用してかまいません。
 ドキュメントとかはまぁ、そのうち(気が向いたら)正月の間にやるから待ってくだしぁ。

relocateはsetLayoutX,setLayoutYの代わりではない

 朝から叫んでおりました内容が解決したので、記事にしておきます。
 まずは、以下のようなクラスを用意します。一辺100の正方形の中心に直径60の円と長さ100の横棒を重ねたような図を表示します。

import javafx.scene.Group;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
public class ShapeGroup extends Group{
  private Circle circle;
  private Line line;
  
  @Override
  protected void layoutChildren(){
    if(circle == null){
      circle = new Circle(50,50,30);
      line = new Line(0,50,100,50);
      getChildren().addAll(circle,line);
    }
  }
  @Override
  public double prefHeight(double width){
    return 100d;
  }
  @Override
  public double prefWidth(double height){
    return 100d;
  }
}



 ちなみに、Groupを継承する場合はcomputePrefWidthとかじゃなくて、prefWidthをオーバーライドします。今日初めて知りました。
 で、下図のようにこいつの棒の端っこに文字をくっつけたいと思います。

import javafx.geometry.Insets;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;


public class ClassA extends Region{
  private String str;
  private Text text;
  private ShapeGroup shape;
  
  public ClassA(String str){
    this.str=str;
  }
  
  private void initInstance(){
    if(text==null){
      text = new Text(str);
      shape = new ShapeGroup();
      getChildren().addAll(text,shape);
    }
  }
  
  @Override
  protected void layoutChildren(){
    initInstance();
    Insets inset = getInsets();
    double il = inset.getLeft();
    double it = inset.getTop();
    double textWidth = text.prefWidth(-1);
    double textHeight = text.prefHeight(textWidth);

    double shapeHeight = shape.prefHeight(-1);
    
    //ラベルのy座標はちょうど
    //shapeの中央に来るようにする
    text.resize(textWidth, textHeight);
    text.relocate(il, it + shapeHeight*0.5-textHeight*0.5);
    
    shape.relocate(textWidth+il, it);
  }
  
  
  @Override
  protected double computePrefHeight(double width){
    initInstance();
    Insets inset = getInsets();
    double shapeHeight = shape.prefHeight(-1);
    return shapeHeight+inset.getTop()+inset.getBottom();
  }
  
  @Override
  protected double computePrefWidth(double height){
    initInstance();
    Insets inset = getInsets();
    double textWidth = text.prefWidth(-1);
    double shapeWidth = shape.prefWidth(-1);
    return textWidth+shapeWidth
        +inset.getLeft()+inset.getRight();
  }
}

実行結果



 あるぇええええええ?




 これの原因が全然分からなくって、Textの高さが指定したものと異なる、もしくは、描画位置が異なるのかとか勝手に思って、勝手にドツボにハマっていました。

 原因はTextではなく、こいつです。

shape.relocate(textWidth+il, it);
ShapeGroup extends Group



 javafx.scene.Node のrelocateには

Sets the node's layoutX and layoutY translation properties in order to relocate this node to the x,y location in the parent.

と書いてある。というわけで、私はsetLayoutXやsetLayoutYではなく、もっぱらrelocateを配置する際に使っていた。しかし、このrelocateは単にsetLayoutXやsetLayoutYの代用メソッドではなくて、割といらないことしてくれる。

//javafx.scene.Nodeのrelocate
    public void relocate(double x, double y) {
        setLayoutX(x - getLayoutBounds().getMinX());
        setLayoutY(y - getLayoutBounds().getMinY());

        PlatformLogger logger = Logging.getLayoutLogger();
        if (logger.isLoggable(PlatformLogger.FINER)) {
            logger.finer(this.toString()+" moved to ("+x+","+y+")");
        }
    }

 というように、単にsetLayoutY(y)とするのではなく、getLayoutBounds().getMinY()という値を引いてしまっている。
 そして、今回、ShapeGroupは高さ100の正方形の中心に直径60の円を描くという前提でプログラムを書いた。すなわち、ShapeGroupの上下にはそれぞれ長さ20の空白がある。getLayoutBounds().getMinY()はこの20を返す。従って、高さ100の正方形をy座標が0の位置に表示したつもりでも、高さ100の正方形をy座標が-20の位置に表示することになる。これが表示しているテキストの位置と横棒の位置がずれた原因である。
 relocateをsetLayoutX,setLayoutYに置き換える、もしくはGroupを継承するのではなく、RegionやPaneなどを継承するように変更すると、私が意図したような正しい表示がされる。




 というわけで、Groupを使うときは要注意と言うことと、relocate(またはresizeRelocate)はsetLayoutXで設定するのと結果が異なる場合があると知っていないといけない、というのが今日の教訓でした。

JavaFXのInvalidationListenerやChangeListenerやObservableListやBindingについて

 いい加減、何となく動作してるからいいやじゃなくって、中で何やってるのか理解しておくかと、ソースコード読んできたのでまとめておくよ。


InvalidationListenerとChangeListener

 ついさっきまで挙動の差とか、全く理解していなかったほど、違いがよく分からんこの二つ。ソースコードを読んでも差が分からなかったので櫻庭さんに解説していただいた。





 というわけで、どうやらInvalidationListenerとChangeListenerの明確な違いは以下の所にある模様。

//DoublePropertyBaseのmakrInvalidとgetメソッド
    private void markInvalid() {
        if (valid) {//←ここと
            valid = false;//←ここと
            invalidated();
            fireValueChangedEvent();
        }
    }

    @Override
    public double get() {
        valid = true;//←これ
        return observable == null ? value : observable.get();
    }




 つまり、getを呼び出さない限り、2回目以降のfireValueChangedEventは呼び出されない


 で、私がInvalidationListenerとChangeListenerの違いが分からなかった理由は次のコード。


//ExpressionHelper.GenericのfireValueChangedEventメソッドの一部抜粋

//InvalidationListenerの呼び出し
for (int i = 0; i < curInvalidationSize; i++) {
  curInvalidationList[i].invalidated(observable);
}

//ChangeListenerの呼び出し
if (curChangeSize > 0) {
  final T oldValue = currentValue;//currentValueはフィールド変数

  //***↓ここでvalidがTrueになる**
  currentValue = observable.getValue();

  final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue);
  if (changed) {//値が変更されたときだけ呼び出す
    for (int i = 0; i < curChangeSize; i++) {
      curChangeList[i].changed(observable, oldValue, currentValue);
    }
  }
}

 櫻庭さんの言うChangeListenerを登録しているからInvalidationListenerも呼び出されるというのは、ChangeListenerを呼び出す前にgetを呼び出すからということみたい。

 違いが細けぇっ( ゚д゚)

 ということは、InvalidationListenerのみの場合でも、毎回getをしてしまえば二つの呼び出しに差がなくなる。テストするときなんかにSystem.out.println(get)なんてやっていると私のように何の違いがあるのかさっぱり分からん!となる。「変化した」というフラグだけ残しておいて、実際に処理するのは描画直前とか、そういう使い方をしたい場合はInvalidListenerの方が効率的という事の模様。
 ちなみに、InvalidationListenerもしくはChangeListenerのどちらか一つしかないような場合は、GenericじゃなくてSingle〜っていう名前のクラスで特別に場合分けされている。一つのリスナしか登録しないなんて場面は多々あるから、合理的だね。


ObservableListとInvalidationListenerとListChangeListener

 JavaFXで使う機会の多いのがFXCollections.observable(Array)Listかな。observableListでは、渡されたListをラップするOvservableListを返す。従って、渡したリストの中身を変更すると、ObservableListの中身も変わる。作られるObservableListの種類は、Listがランダムアクセスに対応しているかどうかで場合分けされている。
 さて、ObservableListの変更がリスナに通知されるのはやっぱり、fireValueChangedEventメソッドだ。ただし、今回はListListenerHelperというクラスのfireValueChangedEventメソッドだが。あと、追加削除等の変更から、実際に通知を行うところまでソースコードを読もうとすると結構長い道のりを辿る。割と面倒くさかったけど、ざっくりというと、配列のいったいどこからどこまでが変更されたのかを記録するクラス(ListChangeBuilder)があって、そいつにここ変更したよとか、終わったよと通知することで、今までの変更を勝手に処理してくれてChangeを作成し、fireValueChangedEventを呼び出してくれる。で、そのfireValueChangedEventの中身を以下に示す。

for (int i = 0; i < curInvalidationSize; i++) {
   curInvalidationList[i].invalidated(change.getList());
}
for (int i = 0; i < curChangeSize; i++) {
    change.reset();
    curChangeList[i].onChanged(change);
}

 

 結論としてはListChangeListener使っとけって話ですね。change.reset()は何もしないか、単に内部のカウンタを-1にセットするだけなので重たい処理ではありません。なので、色々情報取得できるListChangeListenerを使っておけば何の問題もないと言うことですね。普通のObservableとどっちも同じリスナで監視したいときなんかはInvalidationListenerの方が良いのかな?
 あと、注意点としてはfireValueChangedEventはaddやaddAllを呼び出すたびに実行される。中身のArrayListは十分な容量取ってるから一個一個追加していってもメモリ的に無駄がないはずと思っていても、結構無駄が多い。基本的には、他のArrayListや配列にいったん保存しておいてから、addAllで一発で全部追加するのが効率が良さそうだ。(個人的には、ObjectListenerにbeginChangeとendChageを追加してくれれば良いと思ってる。)



Binding

 Bindingって何となく使ってるけど、いったい何で変化を検出してるのか、いつ計算するのか、GCの動きとかどうなってるのかよく理解してないよね。
 まず、イベントの検出の仕方はInvalidationListenerを使っている。つまり、値が変化したかどうかは見ていない。さて、気になるメモリ管理だが、このInvalidationListenerではWeakReferenceを用いてインスタンスの保持をしている。つまり、Bindしている状態であっても、GCの動作を阻害しない

//BindingHelperObserverの一部抜粋
private final WeakReference<Binding<?>> ref;
public void invalidated(Observable observable) {
  final Binding<?> binding = ref.get();
  if (binding == null) {
     observable.removeListener(this);
  } else {
     binding.invalidate();
  }
}




 refの中にバインドされている対象が入っている。(=refの内容がobservableの値によって変化する。) バインドされている対象がGCで消されてしまったら、自動的にリスナの登録を解除してくれる。つまり、Bindしたものに関しては基本的に自分でunbindしなくても大丈夫と言うことだね。

 で、observableの内容が変化した(可能性がある)場合には、上のソースコードで見て分かるように、invalidateメソッドを呼び出す。これが何をするかというと、変更されたというフラグを立てるだけで、値の再計算は実行されない。実際に値が再計算されるのはgetメソッドを呼び出したときになっている。

//DoubleBindingのgetメソッド
    @Override
    public final double get() {
        if (!valid) {//変化フラグが立っているかどうか
            value = computeValue();//値の再計算及び保持
            valid = true;//フラグを消す
        }
        return value;
    }

 実際にgetメソッドを呼び出さない限りはcomputeValueは呼び出されないので、無駄な計算コストを支払うことはない。つまり、いつgetを呼び出すのかということが重要になってくる。変更されるたびに必要もないのにとりあえずgetしていると、無駄な計算コストを支払うことになる。描画直前とか、本当に必要なときだけ呼び出すようにすると良さそう。


 JavaFXの何かよく分からんな〜と放置していたブラックボックスがだいぶん見えてきました。やっぱり、中の挙動が分かってる方が、プログラム書いてるときも安心できるよね。