Java
このエントリーは http://qiita.com/advent-calendar/2016/java_puzzlers の 21 日目です。
昨日は @zr_tex8r さんのの「ループ以外の文にラベルは付けられるか?」でした。
明日は @khasunuma さんの「誰も投稿しそうにないので、もう少し頑張ってみる」です。
連日高度な Java Puzzle が投稿されていて楽しませていただいてます。
「ジャバチョットデキル」という凄い人たちの投稿でクリティカルヒットを食らってダウンしてます。(^_^;
ここらへんで CoffeeBreak ということで息抜きパズルでエントリーさせていただきます。
古典的なありふれたネタですがお楽しみください。
ちなみにこのプログラムはパズル用なので実用性の欠片もありません。
問題
次のプログラムをコンパイル、実行すると標準出力にどんな結果を表示するでしょうか?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
|
package jp.yucchi.lookbeforeyouleap; import java.lang.management.ManagementFactory; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class LookBeforeYouLeap { public static void main(String[] args) { Runnable r = new Friends(); int procs = ManagementFactory.getOperatingSystemMXBean().getAvailableProcessors(); if (procs < 2) { procs = 1; } ExecutorService executor = Executors.newFixedThreadPool(procs); CompletableFuture<Void> friends1e = CompletableFuture.runAsync(r, executor); CompletableFuture<Void> friends2e = CompletableFuture.runAsync(r, executor); CompletableFuture<Void> friends3e = CompletableFuture.runAsync(r, executor); CompletableFuture<Void> friends4e = CompletableFuture.runAsync(r, executor); CompletableFuture<Void> friends1 = CompletableFuture.runAsync(r); CompletableFuture<Void> friends2 = CompletableFuture.runAsync(r); CompletableFuture<Void> friends3 = CompletableFuture.runAsync(r); CompletableFuture<Void> friends4 = CompletableFuture.runAsync(r); CompletableFuture<Void> allTask = CompletableFuture.allOf(friends1, friends2, friends3, friends4, friends1e, friends2e, friends3e, friends4e); allTask.join(); while (!executor.isShutdown()) { executor.shutdown(); } System.out.println("executor.shutdown()"); } private static class Friends implements Runnable { private final List<String> friends = new ArrayList<>(16); public synchronized void add(String name) { friends.add(name); } public synchronized void remove(String name) { friends.remove(name); } public void update() { friends.add("北川景子"); friends.add("剛力彩芽"); friends.add("桐谷美玲"); friends.add("堀北真希"); friends.add("新垣結衣"); friends.remove("北川景子"); friends.remove("剛力彩芽"); friends.remove("桐谷美玲"); friends.remove("堀北真希"); System.out.println(friends.hashCode() + " " + Thread.currentThread().getName()); } @Override public void run() { update(); } } } |
(1)
1767504129 pool-1-thread-1
1767504129 pool-1-thread-3
1767504129 pool-1-thread-2
1767504129 pool-1-thread-4
-243838107 ForkJoinPool.commonPool-worker-25
1829062017 ForkJoinPool.commonPool-worker-18
1664456421 ForkJoinPool.commonPool-worker-18
856650241 ForkJoinPool.commonPool-worker-25
executor.shutdown()
ExecutorService を使った方は同じハッシュ・コード値を必ず返す。
(2)
1767504129 pool-1-thread-4
1767504129 pool-1-thread-2
1767504129 pool-1-thread-1
1767504129 pool-1-thread-3
1829062017 ForkJoinPool.commonPool-worker-25
1829062017 ForkJoinPool.commonPool-worker-18
1829062017 ForkJoinPool.commonPool-worker-18
1829062017 ForkJoinPool.commonPool-worker-25
executor.shutdown()
ExecutorService、ForkJoin ともに同じハッシュ・コード値を必ず返す。
(3)
-230323071 pool-1-thread-1
1767504129 pool-1-thread-4
-2046939163 pool-1-thread-3
-230323071 pool-1-thread-2
-243838107 ForkJoinPool.commonPool-worker-25
1829062017 ForkJoinPool.commonPool-worker-18
1664456421 ForkJoinPool.commonPool-worker-18
856650241 ForkJoinPool.commonPool-worker-25
executor.shutdown()
同じハッシュ・コード値を返す場合もある
(4)
1647295788 pool-1-thread-1
-591109637 pool-1-thread-4
1767504129 pool-1-thread-2
357637727 pool-1-thread-3
-243838107 ForkJoinPool.commonPool-worker-25
1829062017 ForkJoinPool.commonPool-worker-18
1664456421 ForkJoinPool.commonPool-worker-18
856650241 ForkJoinPool.commonPool-worker-25
executor.shutdown()
おのおのユニークなハッシュ・コード値を返す。
(5)
実行時例外
(6)
コンパイルエラー
答え
正解は‥‥
(1) ではありません。
(2) でもありません。
(3) が正解といいたいところですが違います。
実はこのように出力されることもあります。
ただし、100 パーセントこのように出力される保証はなくプログラムのコードには致命的な大きな問題がひそんでいます。
よって、不正解ということでご了承くださいませ。
正解は (5) 実行時例外です。
java.util.ConcurrentModificationException が投げられます。
65 行目の System.out.println(friends.hashCode() + ” ” + Thread.currentThread().getName()); が原因です。
java.util.ConcurrentModificationException とはどんな例外なのでしょうか?
Javadoc を見てみましょう。
この例外は、オブジェクトの並行変更を検出したメソッドによって、そのような変更が許可されていない場合にスローされます。
たとえば、あるスレッドが Collection で繰り返し処理を行なっている間に、別のスレッドがその Collection を変更することは一般に許可されません。
通常、そのような環境では、繰り返し処理の結果は保証されません。
いくつかのイテレータ (Iterator) の実装 (JRE が提供するすべての一般的な目的のコレクションの実装の、イテレータの実装を含む) は、その動作が検出された場合にこの例外をスローすることを選択できます。
この例外をスローするイテレータは、フェイルファストイテレータと呼ばれます。
イテレータは、将来の予測できない時点において予測できない動作が発生する危険を回避するために、ただちにかつ手際よく例外をスローします。
この例外は、オブジェクトが別のスレッドによって並行して更新されていないことを必ずしも示しているわけではありません。
単一のスレッドが、オブジェクトの規約に違反する一連のメソッドを発行した場合、オブジェクトはこの例外をスローします。
たとえば、フェイルファストイテレータを持つコレクションの繰り返し処理を行いながら、スレッドがコレクションを直接修正する場合、イテレータはこの例外をスローします。
通常、非同期の並行変更がある場合、確かな保証を行うことは不可能なので、フェイルファストの動作を保証することはできません。
フェイルファストオペレーションは最善努力原則に基づき、ConcurrentModificationException をスローします。
したがって、正確を期すためにこの例外に依存するプログラムを書くことは誤りです。
ConcurrentModificationException は、バグを検出するためにのみ使用してください。
以上、 Javadoc より
つまり、 Java の同期化コレクションはマルチスレッドなどによる並行的な変更には完全に対応できないということです。
だから並行的な変更への対処はイテレーション開始後に Collection が変更されたのを検出すると ConcurrentModificationException 例外をただちに投げるというフェイルファストという設計になっている。
ただし、このフェイルファスト設計は実行性能に与える影響を抑えるためにそれほど優秀な設計にはなってないようです。
では、この問題の鍵はどこにあるのだろう?
どうして? Iterator で回してないのにと思われる人は純な心の持ち主でしょう。
hashCode() メソッドの実装は次のようになっています。
|
public int hashCode() { int hashCode = 1; for (E e : this) hashCode = 31*hashCode + (e==null ? 0 : e.hashCode()); return hashCode; } |
拡張 for 文を使っていますね。
これは内部でイテレータを使ってます。だから java.util.ConcurrentModificationException が投げられます。
もう少し確認するために次のような変更をプログラムに加えてみましょう。
65 行目の System.out.println(friends.hashCode() + ” ” + Thread.currentThread().getName()); を
System.out.println(“My Friends: ” + friends + ” ” + Thread.currentThread().getName()); に変更して実行してみます。
これでも java.util.ConcurrentModificationException が投げられます。
どうしてでしょう?
まず、文字列の連結操作により StringBuilder クラスの次のメソッドが呼ばれます。
|
@Override public StringBuilder append(Object obj) { return append(String.valueOf(obj)); } |
続いて String クラスの次のメソッドが呼ばれます。
|
public static String valueOf(Object obj) { return (obj == null) ? "null" : obj.toString(); } |
そして、AbstractCollection<E> クラスの次のメソッドが実行されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
public String toString() { Iterator<E> it = iterator(); if (! it.hasNext()) return "[]"; StringBuilder sb = new StringBuilder(); sb.append('['); for (;;) { E e = it.next(); sb.append(e == this ? "(this Collection)" : e); if (! it.hasNext()) return sb.append(']').toString(); sb.append(',').append(' '); } } } |
おおっ! Iterator でぶん回してますね。
問題の hashCode() メソッド同様に内部で Iterator を使ってます。
このように間接的に Iterator を使っていることに気づかずに致命的なミスをおかしてしまう可能性があります。
Collection の取り扱いには細心の注意が必要です。
それではこのプログラムの修正はどうすればいいでしょうか?
この問題のプログラムは Collection への要素の追加、削除を次のように synchronized メソッドで排他制御を行ってます。
|
public synchronized void add(String name) { friends.add(name); } public synchronized void remove(String name) { friends.remove(name); } |
これでは Collection をイテレーションしている間に別のスレッドによる変更操作が行われてしまいます。
排他制御を行うには public void update() メソッドを public synchronized void update() メソッドに変更すれば OK です。
要素の追加、削除のメソッドは排他制御を行う必要はこれでなくなります。
synchronized による排他制御を行いたくない場合は ReentrantLock を使う方法があります。
|
Lock lock = new ReentrantLock(); @Override public void run() { lock.lock(); try { update(); } finally { lock.unlock(); } } |
最後に CopyOnWriteArrayList<E> を使う方法があります。
ただし、これは今までの排他制御とは異なります。
CopyOnWriteArrayList<E> の Javadoc を読んでみましょう。
public class CopyOnWriteArrayList<E> extends Object implements List<E>, RandomAccess, Cloneable, Serializable
基になる配列の新しいコピーを作成することにより、すべての推移的操作(add、setなど)が実装されるArrayListのスレッド・セーフな変数です。
通常、これは非常に効率が悪いのですが、トラバーサル操作が変更を数の点で大幅に上回る場合には、代替手段よりも効率が良い場合があります。
また、これは、トラバーサルを同期できない場合や、同期することを望まないが、並行スレッド間の干渉を排除する必要がある場合に有用です。
「スナップショット」スタイルのイテレータ・メソッドは、イテレータの作成時点での配列状態への参照を使用します。
この配列がイテレータの有効期間中に変更されることは決してないため、干渉は不可能であり、イテレータはConcurrentModificationExceptionをスローしないことが保証されます。
イテレータは、イテレータの作成以降のリストへの追加、削除、または変更を反映しません。
イテレータ自体に対する要素変更操作(remove、setおよびadd)はサポートされません。これらのメソッドは、UnsupportedOperationExceptionをスローします。
nullを含むすべての要素が許可されます。
メモリー整合性効果: ほかの並行処理コレクションと同様、オブジェクトをCopyOnWriteArrayListに配置する前のスレッド内のアクションは、別のスレッドでのその要素へのアクセスまたはCopyOnWriteArrayListからの削除に続くアクションよりも前に発生します。
このクラスは、Java Collections Frameworkのメンバーです。
つまり、スナップショット(コピー)を作ってそれをイテレーションすることによってスレッド・セーフを可能にしているようです。
private final List<String> friends = new ArrayList<>(16); を
private final CopyOnWriteArrayList<String> friends = new CopyOnWriteArrayList<>(); に変更するだけのお手軽仕様です。
ただし、CopyOnWriteArrayList<E> は初期容量を設定できないのでお間違いなく!
いかがでしたでしょうか?
本当に古典的なありふれたパズルですが懐かしさと初めて java.util.ConcurrentModificationException を投げられたときの ??? ってのを思い出していただければ幸いです。
ここでネタばらし・・・ この問題は 「Java 並行処理プログラミング」という古い本にも載っているほど超有名なネタでした。
これだけだと CoffeeBreak ネタとしては面白みに欠けていますのでグリコのキャラメルのおまけのような問題をもう一問どうぞ。
ていうか、これが本題です。
グリコのキャラメルのおまけのような問題
次のプログラムをコンパイル、実行すると標準出力にどんな結果を表示するでしょうか?
ただし、public static void parkUntil(long deadline) メソッドの理由無き復帰はないものとする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
|
package jp.yucchi.coffeebreak; import java.util.concurrent.locks.LockSupport; public class CoffeeBreak { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { int workCount = -1; long start = System.nanoTime(); while (true) { long stop = System.nanoTime(); if ((int) ((stop - start) * 1e-9 % 60) > 10) { break; } else { int count = (int) ((stop - start) * 1e-9 % 60); if (workCount != count) { workCount = count; if (workCount == 0) { System.out.println("今日も一日がんばるぞい!"); } else { System.out.print(workCount + "時 "); } if (workCount == 3) { final long t = System.currentTimeMillis() + 6_000L; System.out.println("\n□フo(^-^)コーヒーブレイク"); while (true) { LockSupport.parkUntil(t); if (System.currentTimeMillis() >= t) { System.out.println("コーヒーブレイク終了"); break; } else { System.out.println((int) ((System.nanoTime() - start) * 1e-9 % 60) + "時やん。 " + "まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~"); } } } } } } }); worker.start(); LockSupport.unpark(worker); Thread.sleep(4_500); System.out.println("そろそろ休憩おわりにしようか? "); LockSupport.unpark(worker); Thread.sleep(2_000); System.out.println("(゙ `-´)/ コラッ!! 休憩は3時間だぞ。働け!"); worker.interrupt(); while (worker.isAlive()); System.out.println("\n仕事終わった! 愛する家族の元へ帰ろう!"); } } |
(1)
今日も一日がんばるぞい!
1時 2時 3時
□フo(^-^)コーヒーブレイク
そろそろ休憩おわりにしようか?
4時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
(゙ `-´)/ コラッ!! 休憩は3時間だぞ。働け!
6時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
6時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
.
.
.
7時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
.
.
.
8時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
8時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
コーヒーブレイク終了
9時 10時
仕事終わった! 愛する家族の元へ帰ろう!
(2)
今日も一日がんばるぞい!
1時 2時 3時
□フo(^-^)コーヒーブレイク
3時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
そろそろ休憩おわりにしようか?
4時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
(゙ `-´)/ コラッ!! 休憩は3時間だぞ。働け!
6時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
6時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
.
.
.
7時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
.
.
.
8時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
8時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
コーヒーブレイク終了
9時 10時
仕事終わった! 愛する家族の元へ帰ろう!
(3)
実行時例外
(4)
コンパイルエラー
答え
正解は‥‥
(1) ではありません。
(2) です。
|
worker.start(); LockSupport.unpark(worker); Thread.sleep(4_500); System.out.println("そろそろ休憩おわりにしようか? "); LockSupport.unpark(worker); Thread.sleep(2_000); System.out.println("(゙ `-´)/ コラッ!! 休憩は3時間だぞ。働け!"); worker.interrupt(); while (worker.isAlive()); System.out.println("\n仕事終わった! 愛する家族の元へ帰ろう!"); |
worker スレッドが開始された直後に LockSupport.unpark(worker); が実行されます。(45 行目)
これは引数で指定したスレッドにパーミットを与えます。
このパーミットは一つだけ蓄積可能となります。
よって、worker スレッドの LockSupport.parkUntil(t); (30 行目) はパーミットを消費してすぐにリターンされます。
故に標準出力に
今日も一日がんばるぞい!
1時 2時 3時
□フo(^-^)コーヒーブレイク
3時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
と表示されます。
そして 48行目の LockSupport.unpark(worker); が実行されて
そろそろ休憩おわりにしようか?
4時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
と続きます。
while ループで再び LockSupport.parkUntil(t); が実行されます。
指定された待機時間を経過してないのでスレッドは再び待機します。
51 行目で worker.interrupt(); と割り込みをかけます。
ところがこの割り込みに対して InterruptedExceptionがスローされません。
割り込みによる処理をなにもしていないので LockSupport.parkUntil(t); による指定された待機時間までの待機は解除されすぐにリターンされます。
while ループで指定された待機時間まで延々と標準出力に次のように出力されます。
(゙ `-´)/ コラッ!! 休憩は3時間だぞ。働け!
6時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
6時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
.
.
.
7時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
.
.
.
8時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
8時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
コーヒーブレイク終了
9時 10時
仕事終わった! 愛する家族の元へ帰ろう!
これをスッキリした出力にするためには 45 行目の LockSupport.unpark(worker); を削除し、
worker スレッドへの割り込みの対処を次のようにすれば期待する出力が得られます。
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
if (workCount == 3) { final long t = System.currentTimeMillis() + 6_000L; System.out.println("\n□フo(^-^)コーヒーブレイク"); while (true) { LockSupport.parkUntil(t); if (System.currentTimeMillis() >= t) { System.out.println("コーヒーブレイク終了"); break; } else { if (Thread.currentThread().isInterrupted()) { System.out.println("はい、はい。"); break; } System.out.println((int) ((System.nanoTime() - start) * 1e-9 % 60) + "時やん。 " + "まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~"); } } } |
今日も一日がんばるぞい!
1時 2時 3時
□フo(^-^)コーヒーブレイク
そろそろ休憩おわりにしようか?
4時やん。 まだ、コーヒーブレイク タイム。 □フo(^O^)プハァ~
(゙ `-´)/ コラッ!! 休憩は3時間だぞ。働け!
はい、はい。
6時 7時 8時 9時 10時 仕事終わった! 愛する家族の元へ帰ろう!
参考にこのプログラムで使った java.lang.Object java.util.concurrent.locks.LockSupport クラスのメソッドの Javadoc を載せておきます。
public static void unpark(Thread thread)
指定されたスレッドのパーミットが使用可能でない場合に、使用可能にします。
スレッドがparkでブロックされた場合は、ブロックを解除します。
それ以外の場合は、そのparkの次回の呼出しがブロックされないよう保証されます。
指定されたスレッドが起動していない場合、この操作の効果は一切保証されません。
パラメータ:thread – unparkを実行するスレッドまたはnull。その場合、この操作に効果はない
public static void parkUntil(long deadline)
パーミットが利用可能でない場合、指定された期限まで、スレッドのスケジューリングに関して現在のスレッドを無効にします。
パーミットが使用可能な場合、これは消費され、呼出しはただちに復帰します。
それ以外の場合、現在のスレッドは、スレッド・スケジューリングに関して無効になり、次の4つのいずれかが起きるまで待機します。
•ほかのスレッドが、現在のスレッドをターゲットとしてunparkを呼び出す。
•ほかのスレッドが現在のスレッドに割り込みを行う。または
•指定された期限が経過する。または
•呼出しが、見せかけで(理由もなく)復帰する。
このメソッドは、これらのどれがメソッド復帰の原因となったかはレポートしません。
呼出し側は、スレッドの初回parkの原因となった状態を再チェックする必要があります。
呼出し側は、スレッドの割込み状態や、復帰時の現在時刻なども判定できます。
パラメータ:deadline – 待機用の、元期からのミリ秒単位の絶対時間
以上、Coffee Break ネタでした。(^_^)
さらに、おまけ
次のプログラムの出力結果を答えよ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
package jp.yucchi.wisecalculation; public class WiseCalculation { public static void main(String[] args) { boolean accuracy = false; for (double i = 1.0; i < Double.MAX_VALUE; i++) { if (Math.log(i) != StrictMath.log(i)) { accuracy = true; break; } } if (accuracy) { System.out.println("Results of Math and StrictMath were not the same."); } else { System.out.println("Results of Math and StrictMath were the same."); } System.out.println(Math.floorDiv(-4, 3)); System.out.println(Math.floorDiv(Long.MIN_VALUE, -1) == Long.MIN_VALUE); System.out.println(Math.floorMod(-4, 3) == Math.floorMod(4, -3)); } } |
答え
Results of Math and StrictMath were not the same.
-2
true
false
これはクイズ要素も引っかけ要素もまるで無いです。
強いてあげれば3行目の出力結果くらいですね。
よくあるオーバーフローねたです。(^_^;
API ドキュメントにすべて解説してありますので知らなくて興味のある人は覗いてみましょう。
java.lang.Object java.lang.Math public static double log(double a)
java.lang.Object java.lang.StrictMath public static double log(double a)
public static int floorDiv(int x, int y)
public static int floorMod(int x, int y)
以上、お終い!
TAGS: Java |
2016年12月21日3:36 AM |
JavaFX
このエントリーは、JavaFX Advent Calendar 2016 の10日目です。
昨日は @nodamushi さんの「JavaFX9が良い感じになってきた件」でした。
明日は @skrb さんの「何か書きます」です。
私は英語がよく解らないので2015年にこんなものを作ろうとしました。(^_^;
英文サイトを読み込んでテキスト化し、英単語の上にマウスをあてるとツールチップで日本語訳を表示するという安易な発想のプログラムです。
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 オブジェクトから取得したデータなどを表示させるというシンプルなプログラムを作ってみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
|
package jp.yucchi.hitinfoword; import com.sun.javafx.scene.control.skin.TextAreaSkin; import com.sun.javafx.scene.text.HitInfo; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Field; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Application; import static javafx.application.Application.launch; import javafx.application.Platform; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.util.Duration; import jp.yucchi.Dictionary4MorphologicalAnalysis.MorphologicalAnalysis; /** * * @author Yucchi */ public class HitInfoWord extends Application { private final MorphologicalAnalysis morphologicalAnalysis = new MorphologicalAnalysis(); private String word; private final boolean debug = true; // Tooltip Timer private static final int TOOLTIP_ACTIVATION_TIME = 500; private static final int TOOLTIP_HIDE_TIME = 10_000; private static final String TEXT_DATA = "Minimal Value Types\n" + "\n" + "The specific features of our minimum (but viable) support for value types can be summarized as follows:\n" + "A few value-capable classes (Int128, etc.) from which the VM may derive value types. " + "These can be standard POJO class files.\n" + "Descriptor syntax (“Q-types”) for describing new value types in class-files.\n" + "Enhanced constants in the constant pool, to interoperate with these descriptors.\n" + "Three bytecode instructions (vload, etc.) for moving value types between JVM locals and stack.\n" + "Limited reflection for value types (similar to int.class).\n" + "Boxing and unboxing, to represent values (like primitives) in terms of Java’s universal Object type.\n" + "Method handle factories to provide access to value operations (member access, etc.)\n" + "Standard Java source code, including generic classes and methods, " + "will be able to refer to values only in their boxed form. " + "However, both method handles and specially-generated bytecodes " + "will be able to work with values in their native, unboxed form.\n" + "This work relates to the JVM, not to the language. Therefore non-goals include:\n" + "Syntax for defining or using value types directly from Java code.\n" + "Specialized generics in Java code which can store or process unboxed values (or primitives).\n" + "Library value types or evolved versions of value-based classes like java.util.Optional.\n" + "Access to value types from arbitrary modules. (Typically, value-capable classes will not be exported.)\n" + "Given the slogan “codes like a class, works like an int,” " + "which captures the overall vision for value types, this minimal set will deliver something more like " + "“works like an int, if you can catch one”.\n" + "By limiting the scope of this work, we believe useful experimentation can be enabled in a production " + "JVM much earlier than if the entire value-type stack were delivered all at once.\n" + "The rest of this document goes into the proposed features in detail."; @Override public void start(Stage primaryStage) { int sceneWidth = 800; int sceneHeight = 250; StackPane root = new StackPane(); TextArea textArea = new TextArea(); textArea.setWrapText(true); textArea.setEditable(false); textArea.setStyle("-fx-text-fill: black;" + "-fx-font-weight: normal;" + "-fx-font-size: 24;"); textArea.setText(TEXT_DATA); final Tooltip tooltip = new Tooltip(); myTooltipTimer(tooltip); try { Optional<String> text = Optional.ofNullable(textArea.getText()); morphologicalAnalysis.setText(text.orElseThrow((() -> new Exception()))); } catch (Exception ex) { exceptionOccured(ex); } textArea.layoutBoundsProperty().addListener(e -> { textArea.setScrollTop(0); }); textArea.scrollTopProperty().addListener(e -> { Tooltip.uninstall(textArea, tooltip); }); textArea.setOnMouseMoved(e -> { TextAreaSkin textAreaSkin = (TextAreaSkin) textArea.getSkin(); HitInfo hitInfo = textAreaSkin.getIndex(e.getX(), e.getY() + textArea.scrollTopProperty().getValue()); // 文字データ取得 word = null; Optional.ofNullable(morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex())) .ifPresent(morpheme -> { word = morpheme.word; }); // TextArea コンテンツ内で文字上にキャレットがある場合に Tooltipを表示 if (morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex()) != null) { Tooltip.install(textArea, tooltip); } else { Tooltip.uninstall(textArea, tooltip); } tooltip.setText("X: " + e.getX() + "\n" + "Y: " + (e.getY() + textArea.scrollTopProperty().getValue()) + "\n" + "getCharIndex: " + hitInfo.getCharIndex() + "\n" + "getInsertionIndex: " + hitInfo.getInsertionIndex() + "\n" + "isLeading: " + hitInfo.isLeading() + "\n" + word); if (debug) { System.out.println("X: " + e.getX() + "\n" + "Y: " + (e.getY() + textArea.scrollTopProperty().getValue()) + "\n" + "getCharIndex: " + hitInfo.getCharIndex() + "\n" + "getInsertionIndex: " + hitInfo.getInsertionIndex() + "\n" + "isLeading: " + hitInfo.isLeading() + "\n" + word + "\n"); } }); root.getChildren().add(textArea); Scene scene = new Scene(root, sceneWidth, sceneHeight); primaryStage.setTitle(this.getClass().getSimpleName()); primaryStage.setScene(scene); primaryStage.show(); } /** * @param args the command line arguments */ public static void main(String[] args) { launch(args); } // TooltipTimer 変更 private void myTooltipTimer(Tooltip tooltip) { try { Field fieldBehavior = tooltip.getClass().getDeclaredField("BEHAVIOR"); fieldBehavior.setAccessible(true); Object objBehavior = fieldBehavior.get(tooltip); Field activationTimer = objBehavior.getClass().getDeclaredField("activationTimer"); activationTimer.setAccessible(true); Timeline activationTimeline = (Timeline) activationTimer.get(objBehavior); activationTimeline.getKeyFrames().clear(); activationTimeline.getKeyFrames().add(new KeyFrame(new Duration(TOOLTIP_ACTIVATION_TIME))); Field hideTimer = objBehavior.getClass().getDeclaredField("hideTimer"); hideTimer.setAccessible(true); Timeline hideTimeline = (Timeline) hideTimer.get(objBehavior); hideTimeline.getKeyFrames().clear(); hideTimeline.getKeyFrames().add(new KeyFrame(new Duration(TOOLTIP_HIDE_TIME))); } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) { Logger.getLogger(HitInfoWord.class.getName()).log(Level.SEVERE, null, ex); exceptionOccured(ex); } } private void exceptionOccured(Exception ex) { Alert alert = new Alert(Alert.AlertType.ERROR); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); ex.printStackTrace(pw); pw.flush(); String stackTrace = sw.toString(); TextArea textArea = new TextArea(stackTrace); textArea.setEditable(false); alert.getDialogPane().setExpandableContent(textArea); alert.initStyle(StageStyle.TRANSPARENT); // alert.setTitle("ERROR"); alert.setHeaderText("Error!\n" + ex.getClass().getSimpleName()); alert.setContentText("Exit the application."); alert.showAndWait() .filter(response -> response == ButtonType.OK) .ifPresent(response -> { Platform.exit(); System.exit(0); }); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
package jp.yucchi.Dictionary4MorphologicalAnalysis; import java.text.BreakIterator; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * * @author Yucchi */ public class MorphologicalAnalysis { private final List<Morpheme> morphemeList = new ArrayList<>(); private Morpheme morpheme; public void setText(String text) { morphemeList.clear(); BreakIterator boundary = BreakIterator.getWordInstance(Locale.ENGLISH); boundary.setText(text); int start = boundary.first(); int end = boundary.next(); while (end != BreakIterator.DONE) { String word = text.substring(start, end); if (Character.isLetterOrDigit(word.charAt(0))) { morphemeList.add(new Morpheme(start, end, word)); } start = end; end = boundary.next(); } } public Morpheme getMorpheme(int charIndex) { morpheme = null; morphemeList.stream() .filter(e -> e.range(charIndex)) .findFirst() .ifPresent(e -> { morpheme = e; }); return morpheme; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
package jp.yucchi.Dictionary4MorphologicalAnalysis; /** * * @author Yucchi */ public class Morpheme { public int start; public int end; public String word; public Morpheme(int start, int end, String word) { this.start = start; this.end = end; this.word = word; } boolean range(int charIndex) { return start <= charIndex && charIndex < end; } } |
このプログラムを実行して Minimal という英単語の M の中央より左の位置にマウスカーソルをもっていくと次のように表示されます。
中央より右の位置にマウスカーソルをもっていくと次のように表示されます。
それではこれらがどういったデータなのかプログラムをみていきます。
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 内でマウスカーソルの移動が検出されたときに実行されるようにしてます。
|
textArea.setOnMouseMoved(e -> { TextAreaSkin textAreaSkin = (TextAreaSkin) textArea.getSkin(); HitInfo hitInfo = textAreaSkin.getIndex(e.getX(), e.getY() + textArea.scrollTopProperty().getValue()); |
これで 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() メソッドは挿入位置インデックスを返しています。
|
/** * Returns the index of the insertion position. */ public int getInsertionIndex() { return leading ? charIndex : charIndex + 1; } |
プログラムでは Tooltip にこれらのメソッドにより取得したデータを表示させるようにしています。
|
tooltip.setText("X: " + e.getX() + "\n" + "Y: " + (e.getY() + textArea.scrollTopProperty().getValue()) + "\n" + "getCharIndex: " + hitInfo.getCharIndex() + "\n" + "getInsertionIndex: " + hitInfo.getInsertionIndex() + "\n" + "isLeading: " + hitInfo.isLeading() + "\n" + word); |
最後の行にある word はマウスカーソル上の単語を HitInfo オブジェクトを利用して取得したものです。
|
// 文字データ取得 word = null; Optional.ofNullable(morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex())) .ifPresent(morpheme -> { word = morpheme.word; }); |
さて、Tooltip はマウスカーソルが文字上にある場合だけ表示させたいので単純に次のような条件式を実装しました。
|
if (morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex()) != null) { Tooltip.install(textArea, tooltip); } else { Tooltip.uninstall(textArea, tooltip); } |
ところがこんな単純に期待通りの結果は得ることができませんでした。
マウスカーソルが文字上にないところでも Tooltip が表示されてしまいます。(×_×)
とりあえずの対策として下記のように修正しました。
public int getInsertionIndex() を利用して最後の文字の挿入インデックスに(最後の文字の次のインデックス)Morpheme オブジェクトが存在するかの判定を追加しました。
|
if (morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex()) != null && morphologicalAnalysis.getMorpheme(hitInfo.getInsertionIndex()) != null) { Tooltip.install(textArea, tooltip); } else { Tooltip.uninstall(textArea, tooltip); } |
当然このコードでは文字の最後の右半分上にマウスカーソルがヒットしていても Tooltip は表示されません。
この不具合もすぐに解決しなければいけないのですが他にも問題があるのでとりあえず後回しとします。
次に解決しなければいけない問題は下図のようなものです。
マウスカーソルが右の余白部分、上の余白部分にあっても Tooltip が表示されてしまいます。
これら余白部分で Tooltip を表示させないためには TextArea のデフォルトの余白の値を取得することが必要となります。
これは仕様だとあきらめようとしたけど・・・ どうもこれでは眠れなくなりそうなので妖しい TextAreaSkin クラスのソースを覗いてみました。
たぶんこれだと思うので使ってみることにします。
|
private double getTextTranslateX() { return contentView.snappedLeftInset(); } private double getTextTranslateY() { return contentView.snappedTopInset(); } |
private メソッドなのでリフレクションを利用してデータを取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
// TextArea Default Inset 取得 この二つの変数はフィールドです。 leftInset = getLeftInset(textArea); topInset = getTopInset(textArea); //////////////////////////////////////////////////////////////////////// private double getLeftInset(TextArea textArea) { TextAreaSkin textAreaSkin = (TextAreaSkin) textArea.getSkin(); Method method = null; try { method = textAreaSkin.getClass().getDeclaredMethod("getTextTranslateX"); method.setAccessible(true); } catch (NoSuchMethodException | SecurityException ex) { Logger.getLogger(HitInfoWord.class.getName()).log(Level.SEVERE, null, ex); exceptionOccured(ex); } double left = 0; try { left = (double) method.invoke(textAreaSkin); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { Logger.getLogger(HitInfoWord.class.getName()).log(Level.SEVERE, null, ex); exceptionOccured(ex); } return left; } |
private double getTextTranslateY() メソッドのデータの取得は同様にしますのでコードは省略させていただきました。
これでデフォルトの余白データは取得できるので Tooltip の表示を制御することができました。
スクロールさせてしまえば上部の余白は隠れてしまうのですが文字が中途半端に見切れているのに Tooltip を表示させる必要はないので常に上下左右の余白分を表示させないようにしました。
|
// TextArea コンテンツ内で文字上にキャレットがある場合に Tooltipを表示 if (morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex()) != null && morphologicalAnalysis.getMorpheme(hitInfo.getInsertionIndex()) != null && e.getX() > leftInset && e.getY() > topInset && e.getX() < textArea.getWidth() - leftInset && e.getY() < textArea.getHeight() - topInset) { Tooltip.install(textArea, tooltip); } else { Tooltip.uninstall(textArea, tooltip); } |
一応これでも動くのですがもっとスマートな方法があります。
javafx.scene.Parent public Node lookup(String selector) メソッドにて引数で指定した CSS セレクタに基づいてノードを検索します。
そして返されたノードのレイアウト情報を取得すればいいだけです。
|
Text textNode = (Text) textArea.lookup(".text"); // TextArea Default Inset 取得 leftInset = textNode.getLayoutX(); topInset = textNode.getLayoutY(); |
こちらのほうが簡単ですね!
さて、デフォルトの余白の対処はこれでいいのですが、
textArea.setPadding(new Insets(50, 50, 50, 50)); //(top/right/bottom/left)
のようにプログラム上で設定すればどうなるでしょうか。
さっそく試してみましょう。
なんじゃ、こりゃ!
テキストがパディングによりレイアウト変更されているので座標データとコンテンツとのマッピングが狂ってしまってます。
そこで HitInfo オブジェクトの生成コード、Tooltip の表示制御をパディングによってずれてしまう分の補正を考慮し次のように変更しました。
|
textArea.setOnMouseMoved(e -> { TextAreaSkin textAreaSkin = (TextAreaSkin) textArea.getSkin(); HitInfo hitInfo = textAreaSkin.getIndex(e.getX() - textArea.getPadding().getLeft(), e.getY() + textArea.scrollTopProperty().getValue() - textArea.getPadding().getTop()); |
|
// TextArea コンテンツ内で文字上にキャレットがある場合に Tooltipを表示 if (morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex()) != null && morphologicalAnalysis.getMorpheme(hitInfo.getInsertionIndex()) != null && e.getX() > textArea.getPadding().getLeft() + leftInset && e.getY() > textArea.getPadding().getTop() + topInset && e.getX() < textArea.getWidth() - textArea.getPadding().getRight() - leftInset && e.getY() < textArea.getHeight() - textArea.getPadding().getBottom() - topInset) { Tooltip.install(textArea, tooltip); } else { Tooltip.uninstall(textArea, tooltip); } |
これで OK !
こんなシンプルなことをさせようとしているだけなのに一筋縄ではいかないですね。
ここでさらに疑問が浮上してきました。
テキストを中央表示させたらどうなるの?
下記のような CSS ファイルを追加してみました。
|
.text-area *.text { -fx-text-alignment: center; } |
いやな予感的中です。
左の余白部分で Tooltip が表示されています。
テキストが中央表示にレイアウト変更されているのにそれが反映された結果となっていません。
この問題を解決するには JavaFX で javax.swing.text.JTextComponent public Rectangle modelToView(int pos) throws BadLocationException に相当する機能が必須となります。
文字上にマウスカーソルが有るか無いかの判定がどうしても必要となるからです。
これが可能となればこれまで誤魔化していた全ての問題が解決できます。
「どうしたもんじゃろのう」とNHK連続テレビ小説「とと姉ちゃん」のように考え込みましたが答えは簡単に見つかりました。
TextArea にはハイライト表示の機能があるから絶対 Rectangle modelToView(int pos) メソッドと同じような機能が備わっているはずだ。
TextArea のレンダリング関係と言えば、TextAreaSkin クラスですよね。
ありました!(^_^)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
@Override public Rectangle2D getCharacterBounds(int index) { TextArea textArea = getSkinnable(); int paragraphIndex = paragraphNodes.getChildren().size(); int paragraphOffset = textArea.getLength() + 1; Text paragraphNode = null; do { paragraphNode = (Text)paragraphNodes.getChildren().get(--paragraphIndex); paragraphOffset -= paragraphNode.getText().length() + 1; } while (index < paragraphOffset); int characterIndex = index - paragraphOffset; boolean terminator = false; if (characterIndex == paragraphNode.getText().length()) { characterIndex--; terminator = true; } characterBoundingPath.getElements().clear(); characterBoundingPath.getElements().addAll(paragraphNode.impl_getRangeShape(characterIndex, characterIndex + 1)); characterBoundingPath.setLayoutX(paragraphNode.getLayoutX()); characterBoundingPath.setLayoutY(paragraphNode.getLayoutY()); Bounds bounds = characterBoundingPath.getBoundsInLocal(); double x = bounds.getMinX() + paragraphNode.getLayoutX() - textArea.getScrollLeft(); double y = bounds.getMinY() + paragraphNode.getLayoutY() - textArea.getScrollTop(); // Sometimes the bounds is empty, in which case we must ignore the width/height double width = bounds.isEmpty() ? 0 : bounds.getWidth(); double height = bounds.isEmpty() ? 0 : bounds.getHeight(); if (terminator) { x += width; width = 0; } return new Rectangle2D(x, y, width, height); } |
このメソッドは指定されたインデックスにある文字の境界を返します。
これで全てクリアです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
// Rectangle2D private double x; private double y; private double width; private double height; ////////////////////////////////////////////////////////////////////////////////////////// // 文字および Rectangle2D データ取得 word = null; Optional.ofNullable(morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex())) .ifPresent(morpheme -> { word = morpheme.word; int startIndex = morpheme.start; Rectangle2D startRect = textAreaSkin.getCharacterBounds(startIndex); int endIndex = morpheme.end; if (endIndex > 0) { endIndex--; } Rectangle2D endRect = textAreaSkin.getCharacterBounds(endIndex); // OnMouse Word Rect Coordinate x = startRect.getMinX() + textArea.getPadding().getLeft(); y = startRect.getMinY() + textArea.getPadding().getTop(); width = endRect.getMaxX() - startRect.getMinX(); height = startRect.getHeight(); }); // TextArea コンテンツ内で文字上にキャレットがある場合に Tooltip、Rect を表示 if (morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex()) != null && e.getX() > x && e.getY() > y && e.getX() < x + width && e.getY() < y + height && e.getX() > textArea.getPadding().getLeft() + leftInset && e.getY() > textArea.getPadding().getTop() + topInset && e.getX() < textArea.getWidth() - textArea.getPadding().getRight() - leftInset && e.getY() < textArea.getHeight() - textArea.getPadding().getBottom() - topInset) { Tooltip.install(textArea, tooltip); } else { Tooltip.uninstall(textArea, tooltip); } |
文字の最後のインデックスはそのままだと一つ多くなってしまうので -1 オフセットしてます。
最終的には TextArea の背景を透明にしてその下に Canvas を置き選択された文字の Rectangle2D データを使って 文字を囲むように Rectangle を表示させています。
これで全ての問題は解決! めでたし! めでたし!
最終的なプログラムのコードは次のようになります。
jp.yucchi.Dictionary4MorphologicalAnalysis パッケージはそのまま変更はありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
|
package jp.yucchi.hitinfoword; import com.sun.javafx.scene.control.skin.TextAreaSkin; import com.sun.javafx.scene.text.HitInfo; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Field; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Application; import static javafx.application.Application.launch; import javafx.application.Platform; import javafx.geometry.Insets; import javafx.geometry.Rectangle2D; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.text.Text; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.util.Duration; import jp.yucchi.Dictionary4MorphologicalAnalysis.MorphologicalAnalysis; /** * * @author Yucchi */ public class HitInfoWord extends Application { private final MorphologicalAnalysis morphologicalAnalysis = new MorphologicalAnalysis(); private String word; private final boolean debug = true; // Tooltip Timer private static final int TOOLTIP_ACTIVATION_TIME = 500; private static final int TOOLTIP_HIDE_TIME = 10_000; // TextArea Default Inset private double leftInset; private double topInset; private static final String TEXT_DATA = "Minimal Value Types\n" + "\n" + "The specific features of our minimum (but viable) support for value types can be summarized as follows:\n" + "A few value-capable classes (Int128, etc.) from which the VM may derive value types. " + "These can be standard POJO class files.\n" + "Descriptor syntax (“Q-types”) for describing new value types in class-files.\n" + "Enhanced constants in the constant pool, to interoperate with these descriptors.\n" + "Three bytecode instructions (vload, etc.) for moving value types between JVM locals and stack.\n" + "Limited reflection for value types (similar to int.class).\n" + "Boxing and unboxing, to represent values (like primitives) in terms of Java’s universal Object type.\n" + "Method handle factories to provide access to value operations (member access, etc.)\n" + "Standard Java source code, including generic classes and methods, " + "will be able to refer to values only in their boxed form. " + "However, both method handles and specially-generated bytecodes " + "will be able to work with values in their native, unboxed form.\n" + "This work relates to the JVM, not to the language. Therefore non-goals include:\n" + "Syntax for defining or using value types directly from Java code.\n" + "Specialized generics in Java code which can store or process unboxed values (or primitives).\n" + "Library value types or evolved versions of value-based classes like java.util.Optional.\n" + "Access to value types from arbitrary modules. (Typically, value-capable classes will not be exported.)\n" + "Given the slogan “codes like a class, works like an int,” " + "which captures the overall vision for value types, this minimal set will deliver something more like " + "“works like an int, if you can catch one”.\n" + "By limiting the scope of this work, we believe useful experimentation can be enabled in a production " + "JVM much earlier than if the entire value-type stack were delivered all at once.\n" + "The rest of this document goes into the proposed features in detail."; // Rectangle2D private double x; private double y; private double width; private double height; @Override public void start(Stage primaryStage) { int sceneWidth = 800; int sceneHeight = 250; StackPane root = new StackPane(); Canvas canvas = new Canvas(sceneWidth, sceneHeight); GraphicsContext gc = canvas.getGraphicsContext2D(); gc.setStroke(Color.BLUE); gc.setLineWidth(3); TextArea textArea = new TextArea(); textArea.setWrapText(true); textArea.setEditable(false); textArea.setStyle("-fx-text-fill: black;" + "-fx-font-weight: normal;" + "-fx-font-size: 24;"); textArea.setPadding(new Insets(50, 50, 50, 50)); //(top/right/bottom/left) textArea.setText(TEXT_DATA); final Tooltip tooltip = new Tooltip(); myTooltipTimer(tooltip); try { Optional<String> text = Optional.ofNullable(textArea.getText()); morphologicalAnalysis.setText(text.orElseThrow((() -> new Exception()))); } catch (Exception ex) { exceptionOccured(ex); } textArea.layoutBoundsProperty().addListener(e -> { textArea.setScrollTop(0); gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); gc.setFill(Color.WHITE); gc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); }); textArea.setOnMouseExited(e -> { gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); gc.setFill(Color.WHITE); gc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); }); textArea.scrollTopProperty().addListener(e -> { gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); gc.setFill(Color.LIGHTYELLOW); gc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); Tooltip.uninstall(textArea, tooltip); }); textArea.setOnMouseMoved(e -> { TextAreaSkin textAreaSkin = (TextAreaSkin) textArea.getSkin(); HitInfo hitInfo = textAreaSkin.getIndex(e.getX() - textArea.getPadding().getLeft(), e.getY() + textArea.scrollTopProperty().getValue() - textArea.getPadding().getTop()); gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); gc.setFill(Color.LIGHTYELLOW); gc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); // 文字および Rectangle2D データ取得 word = null; Optional.ofNullable(morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex())) .ifPresent(morpheme -> { word = morpheme.word; int startIndex = morpheme.start; Rectangle2D startRect = textAreaSkin.getCharacterBounds(startIndex); int endIndex = morpheme.end; if (endIndex > 0) { endIndex--; } Rectangle2D endRect = textAreaSkin.getCharacterBounds(endIndex); // OnMouse Word Rect Coordinate x = startRect.getMinX() + textArea.getPadding().getLeft(); y = startRect.getMinY() + textArea.getPadding().getTop(); width = endRect.getMaxX() - startRect.getMinX(); height = startRect.getHeight(); }); // TextArea コンテンツ内で文字上にキャレットがある場合に Tooltip、Rect を表示 if (morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex()) != null && e.getX() > x && e.getY() > y && e.getX() < x + width && e.getY() < y + height && e.getX() > textArea.getPadding().getLeft() + leftInset && e.getY() > textArea.getPadding().getTop() + topInset && e.getX() < textArea.getWidth() - textArea.getPadding().getRight() - leftInset && e.getY() < textArea.getHeight() - textArea.getPadding().getBottom() - topInset) { gc.setFill(Color.LIGHTYELLOW); gc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); gc.strokeRoundRect(x, y, width, height, 10, 10); Tooltip.install(textArea, tooltip); } else { Tooltip.uninstall(textArea, tooltip); } tooltip.setText("X: " + e.getX() + "\n" + "Y: " + (e.getY() + textArea.scrollTopProperty().getValue()) + "\n" + "getCharIndex: " + hitInfo.getCharIndex() + "\n" + "getInsertionIndex: " + hitInfo.getInsertionIndex() + "\n" + "isLeading: " + hitInfo.isLeading() + "\n" + word); if (debug) { System.out.println("X: " + e.getX() + "\n" + "Y: " + (e.getY() + textArea.scrollTopProperty().getValue()) + "\n" + "getCharIndex: " + hitInfo.getCharIndex() + "\n" + "getInsertionIndex: " + hitInfo.getInsertionIndex() + "\n" + "isLeading: " + hitInfo.isLeading() + "\n" + word + "\n"); } }); root.getChildren().addAll(canvas, textArea); Scene scene = new Scene(root, sceneWidth, sceneHeight); scene.getStylesheets().add(getClass().getResource("myCSS.css").toExternalForm()); canvas.widthProperty().bind(textArea.widthProperty()); canvas.heightProperty().bind(textArea.heightProperty()); primaryStage.setTitle(this.getClass().getSimpleName()); primaryStage.setScene(scene); primaryStage.show(); gc.setFill(Color.WHITE); gc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); Text textNode = (Text) textArea.lookup(".text"); // TextArea Default Inset 取得 leftInset = textNode.getLayoutX(); topInset = textNode.getLayoutY(); } /** * @param args the command line arguments */ public static void main(String[] args) { launch(args); } // TooltipTimer 変更 private void myTooltipTimer(Tooltip tooltip) { try { Field fieldBehavior = tooltip.getClass().getDeclaredField("BEHAVIOR"); fieldBehavior.setAccessible(true); Object objBehavior = fieldBehavior.get(tooltip); Field activationTimer = objBehavior.getClass().getDeclaredField("activationTimer"); activationTimer.setAccessible(true); Timeline activationTimeline = (Timeline) activationTimer.get(objBehavior); activationTimeline.getKeyFrames().clear(); activationTimeline.getKeyFrames().add(new KeyFrame(new Duration(TOOLTIP_ACTIVATION_TIME))); Field hideTimer = objBehavior.getClass().getDeclaredField("hideTimer"); hideTimer.setAccessible(true); Timeline hideTimeline = (Timeline) hideTimer.get(objBehavior); hideTimeline.getKeyFrames().clear(); hideTimeline.getKeyFrames().add(new KeyFrame(new Duration(TOOLTIP_HIDE_TIME))); } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) { Logger.getLogger(HitInfoWord.class.getName()).log(Level.SEVERE, null, ex); exceptionOccured(ex); } } private void exceptionOccured(Exception ex) { Alert alert = new Alert(Alert.AlertType.ERROR); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); ex.printStackTrace(pw); pw.flush(); String stackTrace = sw.toString(); TextArea textArea = new TextArea(stackTrace); textArea.setEditable(false); alert.getDialogPane().setExpandableContent(textArea); alert.initStyle(StageStyle.TRANSPARENT); // alert.setTitle("ERROR"); alert.setHeaderText("Error!\n" + ex.getClass().getSimpleName()); alert.setContentText("Exit the application."); alert.showAndWait() .filter(response -> response == ButtonType.OK) .ifPresent(response -> { Platform.exit(); System.exit(0); }); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
|
/* Author : Yucchi */ .text-area .content{ -fx-background-color: transparent; } .text-area { -fx-background-color: transparent; } .text-area .scroll-pane { -fx-background-color: transparent; } .text-area .scroll-pane .viewport{ -fx-background-color: transparent; }/* */.text-area .scroll-pane .content{ -fx-background-color: transparent; } .text-area *.text { -fx-text-alignment: center; } .scroll-bar:vertical { -fx-background-color: TRANSPARENT; } .scroll-bar:vertical *.thumb { -fx-background-color: pink; } .scroll-bar:vertical *.track { -fx-background-color: TRANSPARENT; } .scroll-bar *.increment-button { -fx-background-color: TRANSPARENT; } .scroll-bar *.decrement-button { -fx-background-color: TRANSPARENT; } .scroll-bar:vertical *.increment-arrow { -fx-background-color: hotpink; } .scroll-bar:vertical *.decrement-arrow { -fx-background-color: hotpink; } .scroll-bar:vertical .thumb:hover, .scroll-bar:vertical .thumb:pressed{ -fx-background-color: linear-gradient(from 0% 0% to 100% 0%,hotpink 0%, white 50%, hotpink 100%); } .scroll-bar:vertical *.increment-arrow:hover { -fx-background-color: aqua; } .scroll-bar:vertical *.decrement-arrow:hover { -fx-background-color: aqua; } .scroll-bar:vertical *.increment-arrow:pressed { -fx-background-color: red; } .scroll-bar:vertical *.decrement-arrow:pressed { -fx-background-color: red; } |
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 の虫食い文にドラッグアンドドロップするプログラムを作ってみました。
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.setOnMousePressed(e -> { HitInfo hitInfo = text.hitTest(new Point2D(e.getX() - text.getTranslateX(), e.getY() - text.getTranslateY())); |
そう、コンテナの TextFlow じゃなくて Text ノードで hitTest(Point2D point) メソッドを実行して HitInfo オブジェクトを生成しています。
Text ノード上でないと HitInfo オブジェクトは生成されないんですね。
もう余白のことは考えなくていいようです。
しかし、Text ノードを移動させた場合マッピングが狂ってしまうのでその補正は必要です。
上記のコードは X, Y 座標の移動を考慮して getTranslateX(), text.getTranslateY() メソッドを利用しています。(このプログラムでは getTranslateX() は必要ないです。)
次に JavaFX 9 の新機能を使えるところは選択された Text ノードの単語をドラッグアンドドロップする時ですね。
Text ノードの単語をドラッグで TextArea 内の文字列の任意の場所を選択してキャレットを移動させるための処理です。
|
textArea.setOnDragOver(e -> { if (e.getGestureSource() != textArea && e.getDragboard().hasString()) { e.acceptTransferModes(TransferMode.COPY_OR_MOVE); TextAreaSkin textAreaSkin = (TextAreaSkin) textArea.getSkin(); HitInfo hitInfo = textAreaSkin.getIndex(e.getX(), e.getY() + textArea.scrollTopProperty().getValue()); // int insertionPoint = hitInfo.getInsertionIndex(); // JavaFX 8 // textArea.positionCaret(insertionPoint); // JavaFX 8 textAreaSkin.positionCaret(hitInfo, false); // JavaFX 9 } e.consume(); }); |
これは JavaFX 8 の場合はコメントアウトしてあるコードでいけます。
JavaFX 9 ならもっとスマートに処理コードが書けてしまいます。
TextAreaSkin クラスの public void positionCaret(HitInfo hit, boolean select) メソッドが優秀です。
このメソッドの第一引数は HitInfo オブジェクトです。第二引数が何か気になりますね。
API ドキュメントによると whether to extend selection to the new position. とあります。
オレオレ翻訳をすると「選択を新しい位置に拡張するべきかどうか。」ですかね?
こういうときは試して動作確認してみましょう。
第二引数の値を true に設定してプログラムの動作確認を行います。
ちょっと見づらいですけど TextArea 内のキャレットが一番左端の上部の隅にあります。
Text ノードから単語を選んでドラッグしています。キャレットがマウスポインタのある位置まで移動しています。
はじめにキャレットがあった場所から新たに移動した場所までが選択されている状態となりました。
今度はキャレットの位置を All という単語の左隣まで移動させておきました。
今度はそこからドラッグ操作により新たなキャレットの位置まで選択表示されています。
第二引数が true の時の動作はキャレットの移動先まで選択するようです。
今回のプログラムでこのような機能は必要としないので false と設定しました。
痒いところに手が届くような地味なアップデートですね。
あまり、派手な API ではないですけどこういうことができるようですね。
最後にこのプログラムのコードを載せておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
|
package jp.yucchi.hitinfoinjavafx9; //import com.sun.javafx.scene.control.skin.TextAreaSkin; // JavaFX8 //import com.sun.javafx.scene.text.HitInfo; // JavaFX 8 import java.io.PrintWriter; import java.io.StringWriter; import java.util.Optional; import java.util.stream.Collectors; import javafx.application.Application; import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.TextArea; import javafx.scene.control.skin.TextAreaSkin; // JavaFX 9 import javafx.scene.input.ClipboardContent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.scene.text.FontSmoothingType; import javafx.scene.text.HitInfo; // JavaFX 9 import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; import javafx.scene.text.TextFlow; import javafx.stage.Stage; import javafx.stage.StageStyle; import jp.yucchi.Dictionary4MorphologicalAnalysis.MorphologicalAnalysis; /** * * @author Yucchi */ public class HitInfoInJavaFX9 extends Application { private final MorphologicalAnalysis morphologicalAnalysis = new MorphologicalAnalysis(); private String word; @Override public void start(Stage primaryStage) { double sceneWidth = 750; double sceneHeight = 370; HBox root = new HBox(); root.setStyle("-fx-background-color: #111111;"); Text text = new Text("Force love know why believe"); text.setFill(Color.YELLOW); text.setFont(Font.loadFont(this.getClass().getResourceAsStream("resources/fonts/STARWARS.TTF"), 18)); text.setFontSmoothingType(FontSmoothingType.LCD); TextFlow textFlow = new TextFlow(text); textFlow.setStyle("-fx-background-color: black;"); textFlow.setTextAlignment(TextAlignment.CENTER); textFlow.setMinWidth(text.boundsInParentProperty().getValue().getWidth() / 2); ObservableList<Node> nodes = textFlow.getChildren(); String words = nodes.stream().map(e -> ((Text) e).getText()).collect(Collectors.joining()); try { Optional<String> wordsText = Optional.ofNullable(words); morphologicalAnalysis.setText(wordsText.orElseThrow((() -> new Exception()))); } catch (Exception ex) { exceptionOccured(ex); } TextArea textArea = new TextArea(); textArea.setWrapText(true); String textData = "I ( ) you.\n" + "I ( ).\n" + "\n" + "All right. I'll give it a try.\n" + "Try not. Do or do not. There is no try.\n" + "I don't... I don't ( ) it.\n" + "That is ( ) you fail.\n" + "\n" + "May the ( ) be with you.\n"; textArea.setText(textData); textArea.setStyle("-fx-text-fill: yellow;"); textArea.setFont(Font.loadFont(this.getClass().getResourceAsStream("resources/fonts/STARWARS.TTF"), 18)); double adjustmentWidth = 30; textArea.setMaxWidth(sceneWidth / 2 + adjustmentWidth); HBox.setMargin(textFlow, new Insets(10, 10, 10, 10)); HBox.setMargin(textArea, new Insets(10, 10, 10, 10)); HBox.setHgrow(textFlow, Priority.ALWAYS); root.getChildren().addAll(textFlow, textArea); Scene scene = new Scene(root, sceneWidth, sceneHeight); primaryStage.setMinWidth(sceneWidth / 2); primaryStage.setMinHeight(sceneHeight / 2); primaryStage.setTitle("Complete the famous lines of Star Wars."); primaryStage.setScene(scene); primaryStage.show(); textArea.lookup(".content").setStyle("-fx-background-color: black;"); text.translateYProperty().bind(textFlow.heightProperty(). subtract(text.layoutBoundsProperty().get().getHeight()).divide(2)); textFlow.setOnMousePressed(e -> { HitInfo hitInfo = text.hitTest(new Point2D(e.getX() - text.getTranslateX(), e.getY() - text.getTranslateY())); word = null; Optional.ofNullable(morphologicalAnalysis.getMorpheme(hitInfo.getCharIndex())) .ifPresent(morpheme -> { word = morpheme.word; }); }); text.setOnDragDetected(e -> { Dragboard dragboard = text.startDragAndDrop(TransferMode.ANY); ClipboardContent clipboardContent = new ClipboardContent(); clipboardContent.putString(word); dragboard.setContent(clipboardContent); e.consume(); }); textArea.setOnDragOver(e -> { if (e.getGestureSource() != textArea && e.getDragboard().hasString()) { e.acceptTransferModes(TransferMode.COPY_OR_MOVE); TextAreaSkin textAreaSkin = (TextAreaSkin) textArea.getSkin(); HitInfo hitInfo = textAreaSkin.getIndex(e.getX(), e.getY() + textArea.scrollTopProperty().getValue()); // int insertionPoint = hitInfo.getInsertionIndex(); // JavaFX 8 // textArea.positionCaret(insertionPoint); // JavaFX 8 textAreaSkin.positionCaret(hitInfo, false); // JavaFX 9 } e.consume(); }); textArea.setOnDragDropped(e -> { Dragboard dragboard = e.getDragboard(); boolean success = false; if (dragboard.hasString()) { textArea.insertText(textArea.getCaretPosition(), dragboard.getString()); success = true; } e.setDropCompleted(success); e.consume(); }); } /** * @param args the command line arguments */ public static void main(String[] args) { launch(args); } private void exceptionOccured(Exception ex) { Alert alert = new Alert(Alert.AlertType.ERROR); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); ex.printStackTrace(pw); pw.flush(); String stackTrace = sw.toString(); TextArea textArea = new TextArea(stackTrace); textArea.setEditable(false); alert.getDialogPane().setExpandableContent(textArea); alert.initStyle(StageStyle.TRANSPARENT); // alert.setTitle("ERROR"); alert.setHeaderText("Error!\n" + ex.getClass().getSimpleName()); alert.setContentText("Exit the application."); alert.showAndWait() .filter(response -> response == ButtonType.OK) .ifPresent(response -> { Platform.exit(); System.exit(0); }); } } |
フォントは STARWARS.TTF フォントを使ってます。(何処で入手してか忘れました。)
長くダラダラとしたエントリーを最後まで読んでくださってありがとうございます。
間違いがありましたらコメントいただけるとありがたいです。
TAGS: JavaFX |
2016年12月10日2:12 AM |