プログラムdeタマゴ

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

JavaFX9が良い感じになってきた件

 この記事はJavaFX Advent Calender 2016の9日目の@arachan@githubさんの記事になるそうです。
 前日は@skht777さんのJavaFXで動くプロ生ちゃんデスクトップマスコットを作る - Qiitaでした。
 明日は@Yucchi_jpさんのHitInfoを少しだけ…です。


 はい、テンプレってこんなもんで良いんですかね。
 何か今年やたらと参加者が少なくて盛り上がってる風がないですよね。寂しいですな。
 普段はこういうのは眺めてるだけで、関連記事とか書いてても参加しない奴なんですが、人が少なくて寂しいので、珍しく参加してみようかと。




今、JavaFX9が静かに熱い

 
 世にJavaFXが出てからというもの、派手に目立ったことはないように思います。

 私はJavaFX2からさわり初めて、本格的にJavaFX8から利用をしていますが、これまでの印象はおおよそこんな感じ。

  • JavaFX Script : そんな子はいなかった
  • JavaFX2 : え?WebView?あぁ、Qtですか?
  • JavaFX8 : そんな子がいるような気がする


 なんというか、なんだかんだちょっと凝ったことをしようとするとすぐに制限に引っかかったり、実装を追ってハックするような真似をしないといけなくて、正直に言えば、Swingの方が使いやすいと思っていました。

 しかし、そんなこれまでのバージョン達であっても、登場前や直後は話題にする人は結構話題に挙げていたのですが、JavaFX9の話題は全く盛り上がっていないようです。「JavaFX9」でググってみても、2016年12月現在、id:aoe-tkさんの記事ぐらいしかトップに出てきません。なお、JavaFX8ではいろんな記事が出てきます。

 たしかに、新しいコントローラなどが一切追加されない、非常に地味なアップデートしかありません。派手な話題は描きにくいかもしれません。


 だが、私は言いたい。


 JavaFX9はこれまでJavaFXにあった不満を多く解決してきたと。

 これまであってもなくてもぶっちゃけ変わらない、実に微妙な立ち位置でしかなかったあのJavaFXが。
 あのJavaFXがついにJavaFX9で、僅かに痒いところに手が届かないところまで来たと!



 そんなJavaFX9の良いところをちょっとだけ、紹介いたします。


Skinが公開された!

 皆さんもご存じ、ControlのSkinがついに、ついに、ついにjavafx.scene.control.skinパッケージとして公開されます。
 ほんと、ついにですよ。なんで最初から公開していないんですかね。


 本当にジミーな内容ですね。これが目玉の変更点なんだから、まー盛り上がりませんわな。(´・д・`)


 でも、今まではちょっと見た目を追加したいとか、ちょっと機能を追加したい等々、「ちょっとレールから外れたい」ということをやろうと思えば、com.sunパッケージをいやーな気分で操作するか、多大なコストを払って実装するか(それでも結局com.sumパッケージに行き着くんだけど)しかなかったんです。
 いや、もう、ホント、これで大手を振って色々実装できますよ。


 でも、今回公開されるのはSkinだけで、Behaviorは公開されません。BehaviorはControlの機能、動作を決定する重要なクラスです。com.sunパッケージ時代のSkinだと、コンストラクタの引数にBehaviorが要求されていたんですが、これらがなくなってしまいました。


 じゃぁ、Behaviorそのものがパージされてしまったのかというと、そうでもなくて、結局com.sunパッケージとして残っています。で、コンストラクタの引数ではなくて、コンストラクタの中で生成するようになってしまいました。うわぁ~ぃ(泣
 一応、privateなメソッドでgetBehaviorとかがあるので、そのうち公開する気なのかも知れませんね。気長に待っています。



 あ、そういえば、(少なくとも私が読んだ)JavaFX8までのBehaviorの動作って、次のようになっていたと思うんです。

  1. マウスイベントなどを受け取る
  2. マウスイベントなどからアクションに対応する「String」に変換する
  3. BehaviorにStringを投げる
  4. Stringに対応する処理を実行する


 全部が全部って訳ではないと思いますが、少なくともTextAreaのBehaviorは次のような感じに変わったようです。

  1. Behaviorがマウスイベントなどを受け取る
  2. イベントをキーとして、登録しているラムダを取得する
  3. ラムダを実行する


 見えないところも整理が進んでいるようですね。



Tooltipが良くなった!

 去年、私はこんな記事を書きました。
nodamushi.hatenablog.com

 要約すると、ざっと以下の二つが不満点。

  1. 表示の遅延、表示時間が変更できなくて不満だ
  2. フォーカス持っていないウィンドウでもツールチップが表示される上に、画面の最上にウィンドウが表示されてうざい。


 しかし、JavaFX9からは次の3つのPropertyが追加されます。

  • showDelay : 表示するまでの遅延時間。デフォルトは1秒
  • showDuration : 表示時間。デフォルトは5秒
  • hideDelay : マウスがNodeから離れてからTooltipが消えるまでの時間差。デフォルトは0.2秒
Label label = new Label("マウスを置いているとポップアップするよ。");
Tooltip tooltip = new Tooltip("すぐに現れて消えないTooltip!");
tooltip.setShowDuration(Duration.INDEFINITE);
tooltip.setShowDelay(Duration.ZERO);
label.setTooltip(tooltip);
Scene s = new Scene(label);
stage.setScene(s);
stage.show();

f:id:nodamushi:20161208203008p:plain

 いや~、ツールチップが消えないからキャプチャするのも急がなくて良いし楽だね。


 でも、残念なことに二つ目の不満点である、フォーカスを持たないウィンドウでもポップアップする挙げ句、ウィンドウを最上位まで持ち上げてきてしまう問題点は解決されていませんでした。



 これ結構な問題だと思うんだけど、中の人達に認識されていないのかな?



 あ、あと、これの解決方法が新しく追加されていないかと色々見てたときに気がつきましたが、JavaFX9からは現在表示しているJavaFXウィンドウの一覧をWindow.getWindows()で取得できるようになりました。これ、地味に嬉しい。
 私、これを自前で実装してたからね。

ObservableList<Window> windows = Window.getWindows()



子要素の順番を入れ替えなくても表示順序を変えられる!

 いままで、重なっている子要素の表示上下関係を入れ替えようと思ったら、getChildren()で子要素のリストを取得して、並びを入れ替えるしかありませんでした。
 Swingだったらzindexで変化するのに!と思っていたら、ついにJavaFX9でViewOrderプロパティが出来ました!


 ViewOrderは小さいほど手前に来ます。0が基準値です。負数も可能の様です。
 同じViewOrderの場合は子要素のリストに挿入された順番が反映される。

  Circle c1 = new Circle(100, 100, 100, Color.AQUA);
  Circle c2 = new Circle(100, 150, 100, Color.RED);
  Circle c3 = new Circle(300, 100, 100, Color.AQUA);
  Circle c4 = new Circle(300, 150, 100, Color.RED);
  pane.getChildren().addAll(
          c1,c2,//アクア色の円は赤より下
          c3,c4 //アクア色の円は赤より下
  );
  //c3,c4の表示順序を入れ替え、アクア色の円を赤の上に持ってくる
  c3.setViewOrder(0);//※0は初期値です
  c4.setViewOrder(1);

 実行結果の下図をご覧下さい。右側の二つの円はアクア色の円が赤色の円より上に表示され、左側の二つとは重なり方が変わっています。






念願の行番号付きテキストエディタが使える!

 JavaFX9で行番号付きのピュア(com.sunパッケージやリフレクションを利用しないという意味)なエディタが使えるようになります。
 主に私が作っていたアプリケーションの関係でどうしても欲しかったけど諦めてListViewとTextEditorを行き来することでなんとなくそれっぽい動作で諦めた、あの行番号付きエディタが!当時の私が知ったら号泣して喜んだでしょう。




 まぁ、これ、私が実装したんだけどね。ソースコードはGistにおいておきました。
JavaFX9でTextAreaに行番号がついたTextAreaを作ってみた · GitHub


 さて、先にも書いたように、ついにJavaFX9で今まで秘匿されてきたSkinの実装が公開されました。まだまだ公開して欲しいAPIはいっぱいあるのですが、重要なAPIは結構出そろってきました。その中の一つがText関連のAPIです。
 座標とTextの文字の位置を紐付けるHitInfoクラスTextInputControlSkinのgetCharacterBoundsメソッドが公開されたことにより、文字のインデックスとTextシェイプの座標内の対応が取れるようになりました。

 今回はこの二つを利用して行番号を表示すべき位置を取得し、TextAreaに行番号を付けてみました。


手順

 いい加減記事が長いので、詳細はソースコードを適当に眺めていただくとして、実装したものはザックリと説明すると以下の3手順です。

  1. まずは、TextAreaの文字列を行単位に分割する
  2. TextAreaの左上の座標と左下の座標の挿入位置を検出し、その範囲に入る行について3を処理する
  3. 各行の行頭文字のBoundsを取得し、その位置に会うように行番号を配置(空行などは上手いこと何とか処理する)


 まず、1の文字列を行単位に分割するのは別に説明は要らないでしょう。
 3の、表示位置の取得は、各行の行頭文字インデックスが分かれば、getCharacterBounds(index)で簡単に取得可能です。で、問題は2でした。


HitInfoの落とし穴

 さて、TextInputControlSkinから挿入位置を取得するには、getIndex(x,y)でHitInfoを取得するか、getInsertionPoint(x,y)で取得します。これらはどっちも結果は同じです。
 さて、画面左上の座標の挿入位置と、左下の座標の挿入位置を検出したいのだから、単純に考えるとこうなるはず。

int topInsert = getInsertionPoint(0,0);
int bottomInsert = getInsertionPoint(0,height);


 私は最初これで実装して、行番号が表示されなくて悩みました。

 これで取得されるtopInsertとbottomInsertは、topInsertは0で固定、bottomInsertは-1もしくは、画面の大きさ依存の値で飽和するかになります。

 と、言うのもここでの(0,0)や(0,height)の座標は、TextAreaの座標ではなく、あくまでText内での座標を指しています。従って、Textの原点位置での挿入場所は当然最初の0になってしまうし、(0,height)と指定しても、スクロールバーとか考慮されていないので、ある値で飽和してしまう。-1が返ってくるのは、(0,height)の範囲にTextシェイプが存在していなかったからかも知れないが、これに関しては詳しくはまだ追っていません。

 従って、目的のことがしたい場合は次のようにオフセットを考慮してあげる必要があります。

double offsetY = getCharacterBounds(0).getMinY();
int topInsert = getInsertionPoint(0,-offsetY);
int bottomInsert = getInsertionPoint(0,height-offsetY);
if(bottomInsert < 0)bottomInsert = textArea.getLength();

 最初の文字のY座標をオフセットとして引いてやると、上手くいく。
 bottomInsertは-1になる可能性があるから、-1になっていたら、ひとまずTextAreaの文字列長に設定してやれば良いと思われる。



 まぁ、これはHitInfoの落とし穴というよりは、getIndex(x,y)の仕様ミスだと思うけど。
 実際に公開するまでに変わったりするかもね?変えた方が良いと思うよ?


みんなJavaFX9使おうぜ!

 はい、というわけで、どうでしたでしょうか?
 これまでは、「はぁ~、これもできねぇのかよ」とか、「あ~、動きがもっさり」とか、「JavaでもQtでよくね?Swingでよくね?」「男なら黙ってCUI」などなど、散々(主に私に)捻られてきたJavaFXですが、かなり良い子になってきたと思います。

 Swingとは使い勝手がだいぶ違うので、今までSwingをやってたって人だと、それなりに学習コストを払わなくてはなりません。しかし、それを払うだけの価値はJavaFX9でそれなりに出てきたと思います。


 まだ画面がなーんかちらつくとか、3DShapeとかいらねーからOpenGL使わせろとか、シェーダー書かせろとか、KyeEventが使いにくいとか、Behaivor公開しろとか、言いたいことは山ほどありますが。まぁ、要求なんて何年たっても0になることはないので、焦ってもしょうがないですね。



 ぜひ、この機会にJavaFXを本格的に使いはじめてみませんか?ということで、今回の記事はおしまいです。

 最後まで読まれた方がいましたら、長々とおつきあいただきありがとうございました。





 実は、この記事は5日前に最後の行番号付きTextAreaの話だけで公開しようとしてたんです。でも、ついでにAdventカレンダーに登録してやろっかなーと思ったところ、明日のYucchi_jpさんと内容被りそうだったので、誤魔化すために色々追加したらこんなに長くなりましたとさ。も~、今年のブログ更新はこれで最後でいいな。良いお年を。