プログラムdeタマゴ

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

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

続きを読む

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で著作権とその行使は放棄しているので、好きなように改変、利用してかまいません。
 ドキュメントとかはまぁ、そのうち(気が向いたら)正月の間にやるから待ってくだしぁ。