HitInfoを少しだけ…

JavaFX

このエントリーは、JavaFX Advent Calendar 2016 の10日目です。

昨日は @nodamushi さんの「JavaFX9が良い感じになってきた件」でした。

明日は @skrb さんの「何か書きます」です。

私は英語がよく解らないので2015年にこんなものを作ろうとしました。(^_^;

英文サイトを読み込んでテキスト化し、英単語の上にマウスをあてるとツールチップで日本語訳を表示するという安易な発想のプログラムです。

1

https://www.youtube.com/watch?v=JfifsvUVeKE

作ってる途中でいくつかの問題に遭遇しました。

その中で JavaFX では Swing の

javax.​swing.​text.​JTextComponent public int viewToModel(Point pt)

javax.​swing.​text.​JTextComponent public Rectangle modelToView(int pos) throws BadLocationException

これに相当するものはあるのだろうか?という素朴な疑問です。

調べてみたところ com.​sun.​javafx.​scene.​text public class HitInfo extends Object を使えばなんとかなりそうです。

実は、Rectangle modelToView(int pos) は面倒くさそうだったのでそれを使わずに手抜きプログラミングで妥協していました。

一年以上この問題を放置したまま(忘れていたとも言う・・・)だったので JavaFX Advent Calendar 2016 のネタとして調べてみました。

小ネタですが 参考資料の少ない JavaFX なのでメモとして残しておきます。

さて、ここから先は何も考えずに適当にプログラムを組んでいった私が次々と問題にぶち当たって泣いた記録です。

BreakIterator を使った簡易的な形態素解析の説明は省略させていただきます。

テキストエリアに適当な英文を表示して英単語上にマウスカーソル(キャレット)をもっていくとツールチップで HitInfo オブジェクトから取得したデータなどを表示させるというシンプルなプログラムを作ってみました。

このプログラムを実行して Minimal という英単語の M の中央より左の位置にマウスカーソルをもっていくと次のように表示されます。

2

中央より右の位置にマウスカーソルをもっていくと次のように表示されます。

3

それではこれらがどういったデータなのかプログラムをみていきます。

X, Y は TextArea 内のマウスカーソルの座標データです。

これらの座標データは HitInfo オブジェクトを生成するために使います。

HitInfo クラスはテキストノードのヒット情報を取得するために使われます。

TextArea の HitInfo オブジェクトを取得するためには

com.​sun.​javafx.​scene.​control.​skin public class TextAreaSkin extends TextInputControlSkin<TextArea,TextAreaBehavior>

を取得する必要があります。

javafx.​scene.​control.​Control public final Skin<?> getSkin() メソッドで TextArea のレンダリングコントロール用の Skin オブジェクトを取得します。

そして、com.​sun.​javafx.​scene.​control.​skin.​TextAreaSkin public HitInfo getIndex(double x, double y) メソッドにより (引数は TextArea 内のマウスカーソルの座標データです)

引数の座標データに基づいてヒットテストを実行し、コンテンツのインデックスにマッピングして HitInfo オブジェクトを生成します。

ここまでのコードを確認してみます。

TextArea 内でマウスカーソルの移動が検出されたときに実行されるようにしてます。

これで HitInfo クラスを使う準備ができました。

では HitInfo クラスではどういったことができるのか確認します。

HitInfo クラスには下記のメソッドがあります。

public int getCharIndex()

public boolean isLeading()

public int getInsertionIndex()

public String toString()

これら4個のメソッドのうち public String toString() メソッド以外の3個のメソッドを調べてみます。

public int getCharIndex()

これは HitInfo オブジェクトが参照している文字のインデックスを取得します。

public int getInsertionIndex()

挿入位置のインデックスを取得します。

public boolean isLeading()

API ドキュメントには下記のように記述されています。

Indicates whether the hit is on the leading edge of the character. If it is false, it represents the trailing edge.

実際に動作を確認したところマウスカーソルが文字上の左側か右側にヒットしているか判定しているようです。

左側だったら true、右側だったら false を返します。

このメソッドを利用して public int getInsertionIndex() メソッドは挿入位置インデックスを返しています。

プログラムでは Tooltip にこれらのメソッドにより取得したデータを表示させるようにしています。

最後の行にある word はマウスカーソル上の単語を HitInfo オブジェクトを利用して取得したものです。

さて、Tooltip はマウスカーソルが文字上にある場合だけ表示させたいので単純に次のような条件式を実装しました。

ところがこんな単純に期待通りの結果は得ることができませんでした。

4

マウスカーソルが文字上にないところでも Tooltip が表示されてしまいます。(×_×)

とりあえずの対策として下記のように修正しました。

public int getInsertionIndex() を利用して最後の文字の挿入インデックスに(最後の文字の次のインデックス)Morpheme オブジェクトが存在するかの判定を追加しました。

当然このコードでは文字の最後の右半分上にマウスカーソルがヒットしていても Tooltip は表示されません。

この不具合もすぐに解決しなければいけないのですが他にも問題があるのでとりあえず後回しとします。

次に解決しなければいけない問題は下図のようなものです。

マウスカーソルが右の余白部分、上の余白部分にあっても Tooltip が表示されてしまいます。

5 6

これら余白部分で Tooltip を表示させないためには TextArea のデフォルトの余白の値を取得することが必要となります。

これは仕様だとあきらめようとしたけど・・・ どうもこれでは眠れなくなりそうなので妖しい TextAreaSkin クラスのソースを覗いてみました。

たぶんこれだと思うので使ってみることにします。

private メソッドなのでリフレクションを利用してデータを取得します。

private double getTextTranslateY() メソッドのデータの取得は同様にしますのでコードは省略させていただきました。

これでデフォルトの余白データは取得できるので Tooltip の表示を制御することができました。

スクロールさせてしまえば上部の余白は隠れてしまうのですが文字が中途半端に見切れているのに Tooltip を表示させる必要はないので常に上下左右の余白分を表示させないようにしました。

一応これでも動くのですがもっとスマートな方法があります。

javafx.​scene.​Parent public Node lookup(String selector) メソッドにて引数で指定した CSS セレクタに基づいてノードを検索します。

そして返されたノードのレイアウト情報を取得すればいいだけです。

こちらのほうが簡単ですね!

さて、デフォルトの余白の対処はこれでいいのですが、

textArea.setPadding(new Insets(50, 50, 50, 50)); //(top/right/bottom/left)

のようにプログラム上で設定すればどうなるでしょうか。

さっそく試してみましょう。

7

なんじゃ、こりゃ!

テキストがパディングによりレイアウト変更されているので座標データとコンテンツとのマッピングが狂ってしまってます。

そこで HitInfo オブジェクトの生成コード、Tooltip の表示制御をパディングによってずれてしまう分の補正を考慮し次のように変更しました。

これで OK !

8

こんなシンプルなことをさせようとしているだけなのに一筋縄ではいかないですね。

ここでさらに疑問が浮上してきました。

テキストを中央表示させたらどうなるの?

下記のような CSS ファイルを追加してみました。

いやな予感的中です。

9

左の余白部分で Tooltip が表示されています。

テキストが中央表示にレイアウト変更されているのにそれが反映された結果となっていません。

この問題を解決するには JavaFX で javax.​swing.​text.​JTextComponent public Rectangle modelToView(int pos) throws BadLocationException に相当する機能が必須となります。

文字上にマウスカーソルが有るか無いかの判定がどうしても必要となるからです。

これが可能となればこれまで誤魔化していた全ての問題が解決できます。

「どうしたもんじゃろのう」とNHK連続テレビ小説「とと姉ちゃん」のように考え込みましたが答えは簡単に見つかりました。

TextArea にはハイライト表示の機能があるから絶対 Rectangle modelToView(int pos) メソッドと同じような機能が備わっているはずだ。

TextArea のレンダリング関係と言えば、TextAreaSkin クラスですよね。

ありました!(^_^)

このメソッドは指定されたインデックスにある文字の境界を返します。

これで全てクリアです。

文字の最後のインデックスはそのままだと一つ多くなってしまうので -1 オフセットしてます。

10

最終的には TextArea の背景を透明にしてその下に Canvas を置き選択された文字の Rectangle2D データを使って 文字を囲むように Rectangle を表示させています。

hit600

これで全ての問題は解決! めでたし! めでたし!

最終的なプログラムのコードは次のようになります。

jp.yucchi.Dictionary4MorphologicalAnalysis パッケージはそのまま変更はありません。

HitInfo について少しだけ・・・のはずがだらだら長くなってしまいました。

今回はこのような行き当たりばったりのプログラミングで泣きました。

試してないのであれなんですが、

TextArea クラスの public ObservableList<CharSequence> getParagraphs() メソッドを使って文字リストを取得して

TextAreaSkin クラスの public Rectangle2D getCharacterBounds(int index) メソッドに渡して各文字の領域データを取得してから

TextArea 内のカーソルの位置が文字領域内にあるときだけ HitInfo オブジェクトを生成するようにしたほうが良いのかもしれません。

誰か興味と時間のある人はお試しを!

TextArea クラスを使って HitInfo クラスを試してみましたが TextField クラスでも HitInfo クラスは使えます。

TextFieldSkin クラスにも public HitInfo getIndex(double x, double y) メソッドが用意されています。

今回試してみた TextArea クラス同様におもしろそうなことができるかもしれません。

しかし、それよりも気になるのが JavaFX 9 で javafx.graphics モジュールの javafx.scene.text パッケージにある Text クラスに

HitInfo を返す public final HitInfo hitTest(Point2D point) メソッドが用意されたことです。

あと同パッケージにある TextFlow クラスにも HitInfo を返す public final HitInfo hitTest(Point2D point) メソッドがあります。

TextFlow クラスのほうは TextArea クラスと同じようなものだと想像できます。

しかし、Text クラスのほうはちょっと気になります。

さらに JavaFX 9 ではキャレットを指定された位置に移動させるためのメソッドが TextAreaSkin クラスと TextFieldSkin クラスに用意されました。

public void positionCaret(HitInfo hit, boolean select)

これはちょっと試したくなりますよね!

そこで Text ノードの単語を選択して TextArea の虫食い文にドラッグアンドドロップするプログラムを作ってみました。

starwas_600[1]

Text ノードから単語を選択するのは先ほどのプログラムと仕組みはほぼ同じです。

JavaFX 9 で追加された新しい機能を使うにはどうすればいいのでしょうか。

まず、JDK9 Early Access Releases をダウンロードしてインストールします。

https://jdk9.java.net/download/

あとはお気に入りのエディタか IDE でプログラムを組んでいきます。

JDK9 では Project Jigsaw の影響で com.sun から始まるパッケージの名前が変更になっている場合があります。

今回は次の二つのパッケージが変更されていました。

com.sun.javafx.scene.control.skin.TextAreaSkin; // JavaFX8
com.sun.javafx.scene.text.HitInfo; // JavaFX 8
javafx.scene.control.skin.TextAreaSkin; // JavaFX 9
javafx.scene.text.HitInfo; // JavaFX 9

さて、単語を選択される側の Text ノードから hitInfo オブジェクトを生成するために public final HitInfo hitTest(Point2D point) メソッドを使います。

引数の Point2D point はコンテナの TextFlow におけるText ノードの座標です。(Text ノード上にあるマウスポインタの位置)

感のいい人なら気づいてるかもしれませんが、これ何気にうれしいですね!

そう、コンテナの TextFlow じゃなくて Text ノードで hitTest(Point2D point) メソッドを実行して HitInfo オブジェクトを生成しています。

Text ノード上でないと HitInfo オブジェクトは生成されないんですね。

もう余白のことは考えなくていいようです。

しかし、Text ノードを移動させた場合マッピングが狂ってしまうのでその補正は必要です。

上記のコードは X, Y 座標の移動を考慮して getTranslateX(),  text.getTranslateY() メソッドを利用しています。(このプログラムでは getTranslateX() は必要ないです。)

次に JavaFX 9 の新機能を使えるところは選択された Text ノードの単語をドラッグアンドドロップする時ですね。

Text ノードの単語をドラッグで TextArea 内の文字列の任意の場所を選択してキャレットを移動させるための処理です。

これは JavaFX 8 の場合はコメントアウトしてあるコードでいけます。

JavaFX 9 ならもっとスマートに処理コードが書けてしまいます。

TextAreaSkin クラスの public void positionCaret(HitInfo hit, boolean select) メソッドが優秀です。

このメソッドの第一引数は HitInfo オブジェクトです。第二引数が何か気になりますね。

API ドキュメントによると whether to extend selection to the new position. とあります。

オレオレ翻訳をすると「選択を新しい位置に拡張するべきかどうか。」ですかね?

こういうときは試して動作確認してみましょう。

第二引数の値を true に設定してプログラムの動作確認を行います。

ちょっと見づらいですけど TextArea 内のキャレットが一番左端の上部の隅にあります。

11

Text ノードから単語を選んでドラッグしています。キャレットがマウスポインタのある位置まで移動しています。

はじめにキャレットがあった場所から新たに移動した場所までが選択されている状態となりました。

12

今度はキャレットの位置を All という単語の左隣まで移動させておきました。

13

今度はそこからドラッグ操作により新たなキャレットの位置まで選択表示されています。

14

第二引数が true の時の動作はキャレットの移動先まで選択するようです。

今回のプログラムでこのような機能は必要としないので false と設定しました。

痒いところに手が届くような地味なアップデートですね。

あまり、派手な API ではないですけどこういうことができるようですね。

最後にこのプログラムのコードを載せておきます。

フォントは STARWARS.TTF フォントを使ってます。(何処で入手してか忘れました。)

長くダラダラとしたエントリーを最後まで読んでくださってありがとうございます。

間違いがありましたらコメントいただけるとありがたいです。

15

Hatena タグ: ,