JavaFX
Java JavaFX NetBeans
このエントリーは、JavaFX Advent Calendar 2017 の 20 日目です。
現時点でもエントリーが無いので急遽先日遭遇した問題に解決策を教えていただいたのでそれを記事にしました。
昨日は @aoetk さんの「Bean ValidationのJavaFX対応 」でした。
明日も、この記事を書いている現時点ではまだ空いてます。(^_^; きっと誰かが素敵な記事を投稿してくれると楽しみにしています。
11 月から IBM Cloud (Bluemix)ライト・アカウントが気楽に使えるようになり Watson Personality Insights を利用した人格診断プログラムを組んで楽しんでいました。
その際に Canvas に文字を描くときの位置決めに難儀したので FontMetrics を取得する方法をφ(..)メモメモ
まず、Java で FontMetrics を取得するとしたら java.awt.FontMetrics クラスを使うことができます。
Abstract Window Toolkit を使って下記のようなプログラムを組んでみました。
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
package jp . yucchi . textofcanvas ;
import java . awt . BorderLayout ;
import java . awt . Canvas ;
import java . awt . Color ;
import java . awt . Font ;
import java . awt . FontMetrics ;
import java . awt . Frame ;
import java . awt . Graphics ;
import java . awt . GraphicsEnvironment ;
import java . awt . event . WindowAdapter ;
import java . awt . event . WindowEvent ;
import java . util . stream . Stream ;
import jp . yucchi . textofcanvas . TextOfCanvas . Configuration ;
/**
*
* @author Yucchi
*/
public class TextOfCanvas extends Frame {
protected enum Configuration {
WIDTH ( 800 ) ,
HEIGHT ( 300 ) ,
FONT_SIZE ( 100 ) ;
int settingValue ;
private Configuration ( int settingValue ) {
this . settingValue = settingValue ;
}
public int getSettingValue ( ) {
return settingValue ;
}
}
public static void main ( String [ ] args ) {
TextOfCanvas textOfCanvas = new TextOfCanvas ( ) ;
// // 使用可能なフォント
// Stream.of(GraphicsEnvironment.getLocalGraphicsEnvironment()
// .getAvailableFontFamilyNames())
// .forEach(System.out::println);
}
private Frame frame ;
TextOfCanvas ( ) {
initGUI ( ) ;
}
private void initGUI ( ) {
frame = new Frame ( "Java AWT Text of Canvas" ) ;
frame . setSize ( Configuration . WIDTH . settingValue , Configuration . HEIGHT . settingValue ) ;
frame . setLayout ( new BorderLayout ( ) ) ;
frame . addWindowListener ( new WindowAdapter ( ) {
@Override
public void windowClosing ( WindowEvent we ) {
System . exit ( 0 ) ;
}
} ) ;
TextCanvas textCanvas = new TextCanvas ( ) ;
frame . add ( textCanvas , BorderLayout . CENTER ) ;
frame . setLocationRelativeTo ( null ) ;
frame . setVisible ( true ) ;
}
}
class TextCanvas extends Canvas {
@Override
public void paint ( Graphics g ) {
g . setColor ( Color . LIGHT_GRAY ) ;
g . drawLine ( 0 , Configuration . HEIGHT . settingValue / 2 , Configuration . WIDTH . settingValue , Configuration . HEIGHT . settingValue / 2 ) ;
g . drawLine ( Configuration . WIDTH . settingValue / 4 , 0 , Configuration . WIDTH . settingValue / 4 , Configuration . HEIGHT . settingValue ) ;
// Font font = new java.awt.Font("あくびん", java.awt.Font.BOLD, Configuration.FONT_SIZE.settingValue); // Leading: 0.0
Font font = new java . awt . Font ( "SansSerif" , java . awt . Font . BOLD , Configuration . FONT_SIZE . settingValue ) ;
g . setFont ( font ) ;
g . setColor ( Color . RED ) ;
g . drawString ( "Japan 日本" , Configuration . WIDTH . settingValue / 4 , Configuration . HEIGHT . settingValue / 2 ) ;
FontMetrics fontmetrics = g . getFontMetrics ( ) ;
double height = fontmetrics . getHeight ( ) ; // テキスト1行の標準の高さ Height = Ascent + Descent + Leading
double width = fontmetrics . stringWidth ( "Japan 日本" ) ;
double ascent = fontmetrics . getAscent ( ) ; // ベースラインからの高さ
double descent = fontmetrics . getDescent ( ) ; // ベースラインから下にはみ出る量
// double maxDecent = fontmetrics.getMaxDecent(); // スペルミスによる @Deprecated (^_^;
double leading = fontmetrics . getLeading ( ) ; // 前の行の descent のラインと次の行の ascent のラインの間に必要な「行間」の量
System . out . println ( "Height: " + height ) ;
System . out . println ( "Width: " + width ) ;
System . out . println ( "Ascent: " + ascent ) ;
System . out . println ( "Descent: " + descent ) ;
System . out . println ( "Leading: " + leading ) ;
Font currentFont = g . getFont ( ) ;
Font shrinkFont = currentFont . deriveFont ( currentFont . getSize ( ) * 0.3F ) ;
g . setFont ( shrinkFont ) ;
g . setColor ( Color . GREEN ) ;
g . drawLine ( 0 , ( int ) ( Configuration . HEIGHT . settingValue / 2 - ascent ) ,
Configuration . WIDTH . settingValue , ( int ) ( Configuration . HEIGHT . settingValue / 2 - ascent ) ) ;
g . drawString ( "Ascent: " + ascent , 0 , ( int ) ( Configuration . HEIGHT . settingValue / 2 - height + descent ) ) ;
g . setColor ( Color . BLUE ) ;
g . drawLine ( 0 , ( int ) ( Configuration . HEIGHT . settingValue / 2 + descent ) ,
Configuration . WIDTH . settingValue , ( int ) ( Configuration . HEIGHT . settingValue / 2 + descent ) ) ;
g . drawString ( "Descent: " + descent , 0 , ( int ) ( Configuration . HEIGHT . settingValue / 2 + descent - leading ) ) ;
g . setColor ( Color . magenta ) ;
g . drawLine ( 0 , ( int ) ( Configuration . HEIGHT . settingValue / 2 + leading + descent ) ,
Configuration . WIDTH . settingValue , ( int ) ( Configuration . HEIGHT . settingValue / 2 + leading + descent ) ) ;
g . drawString ( "Leading: " + leading ,
0 , ( int ) ( Configuration . HEIGHT . settingValue / 2 + leading * 2 + descent + g . getFontMetrics ( ) . getAscent ( ) - g . getFontMetrics ( ) . getDescent ( ) ) ) ;
}
}
懐かしいコードですね。
83 行目で FontMetrics を Graphics コンテキストより取得してフォントの構造データをそれぞれの取得メソッドで取得しています。
原点の X, Y 座標に薄いグレーでラインをひいてます。
フォントの高さ関するデータもそれぞれラインをひいてみました。
Abstract Window Toolkit を使って FontMetrics を得ることは簡単にできることが確認できました。
それでは JavaFX 9 ではどうでしょうか?
同じようなプログラムを組んでしました。
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
package jp . yucchi . textcoordinates ;
import com . sun . javafx . tk . FontMetrics ;
import com . sun . javafx . tk . Toolkit ;
import java . awt . GraphicsEnvironment ;
import java . util . stream . Stream ;
import javafx . application . Application ;
import javafx . geometry . VPos ;
import javafx . scene . Group ;
import javafx . scene . Scene ;
import javafx . scene . canvas . Canvas ;
import javafx . scene . canvas . GraphicsContext ;
import javafx . scene . paint . Color ;
import javafx . scene . text . Font ;
import javafx . scene . text . FontSmoothingType ;
import javafx . scene . text . FontWeight ;
import javafx . scene . text . Text ;
import javafx . stage . Stage ;
/**
*
* @author Yucchi
*/
public class TextCoordinates extends Application {
private enum Configuration {
WIDTH ( 800 ) ,
HEIGHT ( 300 ) ,
FONT_SIZE ( 100 ) ;
private final int settingValue ;
private Configuration ( int settingValue ) {
this . settingValue = settingValue ;
}
public int getSettingValue ( ) {
return settingValue ;
}
}
@Override
public void start ( Stage primaryStage ) {
// // 使用可能なフォント
// Stream.of(GraphicsEnvironment.getLocalGraphicsEnvironment()
// .getAvailableFontFamilyNames())
// .forEach(System.out::println);
Group root = new Group ( ) ;
Canvas canvas = new Canvas ( Configuration . WIDTH . settingValue , Configuration . HEIGHT . settingValue ) ;
GraphicsContext gc = canvas . getGraphicsContext2D ( ) ;
drawShapes ( gc ) ;
root . getChildren ( ) . add ( canvas ) ;
Scene scene = new Scene ( root , Configuration . WIDTH . settingValue , Configuration . HEIGHT . settingValue ) ;
primaryStage . setTitle ( "JavaFX Text of Canvas" ) ;
primaryStage . setScene ( scene ) ;
primaryStage . show ( ) ;
}
public static void main ( String [ ] args ) {
launch ( args ) ;
}
private void drawShapes ( GraphicsContext gc ) {
Text text = new Text ( "Japan 日本" ) ;
// Font font = Font.font("あくびん", FontWeight.BOLD, Configuration.FONT_SIZE.settingValue); // Leading: 0.0
Font font = Font . font ( "SansSerif" , FontWeight . BOLD , Configuration . FONT_SIZE . settingValue ) ;
text . setFont ( font ) ;
double width = text . getBoundsInLocal ( ) . getWidth ( ) ;
double height = text . getBoundsInLocal ( ) . getHeight ( ) ;
double baselineOffset = text . getBaselineOffset ( ) ;
double minY = text . getLayoutBounds ( ) . getMinY ( ) ;
double maxY = text . getLayoutBounds ( ) . getMaxY ( ) ;
System . out . println ( "Width: " + width ) ;
System . out . println ( "Height: " + height ) ; // Ascent + Descent
System . out . println ( "BaselineOffset: " + baselineOffset ) ; // Ascent
System . out . println ( "MinY: " + minY ) ; // -Ascent
System . out . println ( "MaxY: " + maxY ) ; // Descent
final FontMetrics fontMetrics = Toolkit . getToolkit ( ) . getFontLoader ( ) . getFontMetrics ( font ) ;
// float fontMetricsWidth = fontMetrics.computeStringWidth(text.getText()); // JavaFX9 では無くなった?
// このフォントでのテキスト行のための最大行高
float fontMetricsLineHeight = fontMetrics . getLineHeight ( ) ;
// 平均の小文字の最上部へのベースラインから距離
float fontMetricsXHeight = fontMetrics . getXheight ( ) ;
// 平均の最大文字高さまでベースラインからの距離。 この値は常に正の値です
float fontMetricsAscent = fontMetrics . getAscent ( ) ;
// ベースラインから最大文字高さまでの距離。 この値は常に正の値です
float fontMetricsMaxAscent = fontMetrics . getMaxAscent ( ) ;
// ベースラインは、デセンダーのない文字(例えば、小文字の「a」)が置かれている仮想線です。
// フォントメトリックに関しては、この点から他のすべてのメトリックが導出されます。 この点は暗黙的にゼロとして定義されています。
int fontMetricsBaseline = fontMetrics . getBaseline ( ) ;
// ベースラインから最低平均値までの距離。 ディセンダー。 この値は常に正の値です
float fontMetricsDescent = fontMetrics . getDescent ( ) ;
// ベースラインから絶対値の最も低いディセンダーまでの距離。 この値は常に正の値です
float fontMetricsMaxDescent = fontMetrics . getMaxDescent ( ) ;
// このフォントのテキスト行間のスペース量。
// これは、1行のmaxDecentと次のmaxAscentの間のスペース量です。
// この数値は、lineHeightに含まれています。
float fontMetricsLeading = fontMetrics . getLeading ( ) ;
// System.out.println("FontMetricsWidth: " + fontMetricsWidth);
System . out . println ( "FontMetricsLineHeight: " + fontMetricsLineHeight ) ; // Ascent + Descent + Leading
System . out . println ( "FontMetricsXHeight: " + fontMetricsXHeight ) ;
System . out . println ( "FontMetricsAscent: " + fontMetricsAscent ) ;
System . out . println ( "FontMetricsMaxAscent: " + fontMetricsMaxAscent ) ;
System . out . println ( "FontMetricsBaseline: " + fontMetricsBaseline ) ;
System . out . println ( "FontMetricsDescent: " + fontMetricsDescent ) ;
System . out . println ( "FontMetricsMaxDescent: " + fontMetricsMaxDescent ) ;
System . out . println ( "FontMetricsLeading: " + fontMetricsLeading ) ;
gc . setFontSmoothingType ( FontSmoothingType . LCD ) ;
gc . setStroke ( Color . LIGHTGRAY ) ;
gc . setLineWidth ( 1.0 ) ;
gc . strokeLine ( 0.0 , Configuration . HEIGHT . settingValue / 2.0 , Configuration . WIDTH . settingValue , Configuration . HEIGHT . settingValue / 2.0 ) ;
gc . strokeLine ( Configuration . WIDTH . settingValue / 4.0 , 0.0 , Configuration . WIDTH . settingValue / 4.0 , Configuration . HEIGHT . settingValue ) ;
// テキスト
gc . setFont ( font ) ;
gc . setFill ( Color . RED ) ;
gc . setTextBaseline ( VPos . BASELINE ) ;
gc . fillText ( text . getText ( ) , Configuration . WIDTH . settingValue / 4.0 , Configuration . HEIGHT . settingValue / 2.0 ) ;
// FontMetrics データ表示用フォント
gc . setFont ( Font . font ( "SansSerif" , FontWeight . BOLD , Configuration . FONT_SIZE . settingValue * 0.3 ) ) ;
// gc.setFont(Font.font("あくびん", FontWeight.BOLD, Configuration.FONT_SIZE.settingValue * 0.3));
final FontMetrics shrinkFontMetrics = Toolkit . getToolkit ( ) . getFontLoader ( ) . getFontMetrics ( gc . getFont ( ) ) ;
// Ascent
gc . setStroke ( Color . GREEN ) ;
gc . strokeLine ( 0.0 , Configuration . HEIGHT . settingValue / 2.0 - fontMetricsAscent ,
Configuration . WIDTH . settingValue , Configuration . HEIGHT . settingValue / 2.0 - fontMetricsAscent ) ;
gc . setFill ( Color . GREEN ) ;
gc . fillText ( "Ascent: " + String . format ( "%.1f" , fontMetricsAscent ) , 0.0 , Configuration . HEIGHT . settingValue / 2.0 - fontMetricsAscent - fontMetricsLeading ) ;
// XHeight
gc . setStroke ( Color . PURPLE ) ;
gc . strokeLine ( 0.0 , Configuration . HEIGHT . settingValue / 2.0 - fontMetricsXHeight ,
Configuration . WIDTH . settingValue , Configuration . HEIGHT . settingValue / 2.0 - fontMetricsXHeight ) ;
gc . setFill ( Color . PURPLE ) ;
gc . fillText ( "XHeight: " + String . format ( "%.1f" , fontMetricsXHeight ) , 0.0 , Configuration . HEIGHT . settingValue / 2.0 - fontMetricsXHeight - fontMetricsLeading ) ;
// Descent
gc . setStroke ( Color . BLUE ) ;
gc . strokeLine ( 0.0 , Configuration . HEIGHT . settingValue / 2.0 + fontMetricsDescent ,
Configuration . WIDTH . settingValue , ( int ) ( Configuration . HEIGHT . settingValue / 2.0 + fontMetricsDescent ) ) ;
gc . setFill ( Color . BLUE ) ;
gc . fillText ( "Descent: " + String . format ( "%.1f" , fontMetricsDescent ) , 0.0 , Configuration . HEIGHT . settingValue / 2.0 + fontMetricsDescent - fontMetricsLeading ) ;
// Leading
gc . setStroke ( Color . MAGENTA ) ;
gc . strokeLine ( 0.0 , ( Configuration . HEIGHT . settingValue / 2.0 + fontMetricsLeading + fontMetricsDescent ) ,
Configuration . WIDTH . settingValue , Configuration . HEIGHT . settingValue / 2.0 + fontMetricsLeading + fontMetricsDescent ) ;
gc . setFill ( Color . MAGENTA ) ;
gc . fillText ( "Leading: " + String . format ( "%.1f" , fontMetricsLeading ) ,
0.0 , Configuration . HEIGHT . settingValue / 2.0 + fontMetricsDescent + shrinkFontMetrics . getAscent ( ) + shrinkFontMetrics . getLeading ( ) ) ;
}
}
残念ながらコンパイルエラーです。
「名前のないモジュールにエクスポートされていません」ってなんのことですか?
JavaFX 9 では java.awt.FontMetrics クラスは使えないようなので com.sun.javafx.tk.FontMetrics, com.sun.javafx.tk.Toolkit を使用しました。
確か、JavaFX 8 では使えていたような記憶があるんだけど・・・
そう言えば、Project Jigsaw の影響で com.sun ではじまるパッケージが使えないものがあるようです。
このプログラムで使っている com.sun.javafx.tk.FontMetrics, com.sun.javafx.tk.Toolkit も JDK 内部(モジュール)にちゃんとあるのにデフォルトで使えなくしてあります。
困りました!
Windows 環境なら JavaFX 9 だったら HiDPI 対応の恩恵を享受することが可能なのに。
ちなみに JavaFX 8 だったら問題なく動きました。
さて、どうしたものか。。。
以前 Twitter で 「hoge は fuga ができないからクソッ!」もしくは女子高生を装ってヘルプをつぶやくと優秀なプログラマが解決策を提案してくれるという法則を学んだ。
私には役者の才能も無いし、小賢しいことをするのは面倒なので素直に Twitter でつぶやいたところ心優しい Java プログラマーが助けてくれました。
ありがとうございます!
早速 NetBeans に javacとjavaのオプションを設定してコンパイル、実行をしたところ無事に動きました。(^_^)
ちなみに、テキストの挿入位置の Y 座標の位置はデフォルトでは VPos.BASELINE となっています。
gc.setTextBaseline(VPos.BASELINE);
これは次のように変更することができます。
gc.setTextBaseline(VPos.TOP);
gc.setTextBaseline(VPos.CENTER);
gc.setTextBaseline(VPos.BOTTOM);
ああっ・・・ しまった。(>_<) 左のテキストの位置の修正忘れた。見なかったことにしてください。(ごめんなさい)
ついでに Leading が無いフォントの表示も見ておきます。
AWT と JavaFX では FontMetrics の扱い方に違いがあるので注意が必要ですね。
まとめ
さて、ここで JavaFX で FontMetrics を扱うために com.sun.javafx.tk.FontMetrics, com.sun.javafx.tk.Toolkit を使用します。
これを JavaFX 9 で使うためには javacとjavaのオプションに –add-exports=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED の設定が必要です。
参考
JEP 261: Module System
カプセル化を破る
モジュールシステムによって定義されたアクセス制御境界に違反し、コンパイラと仮想マシンによって強制されて、 あるモジュールが別のモジュールの一部の非通知タイプにアクセスできるようにする必要があることがあります。 これは、例えば、内部型のホワイトボックステストを可能にするため、 またはサポートされていない内部APIをそれらに依存するようになったコードに公開するために望ましいことがある。 これを行うには、コンパイル時と実行時の両方で–add-exportsオプションを使用できます。 構文は次のとおりです。
–add-exports <source-module>/<package>=<target-module>(,<target-module>)*
<source-module>と<target-module>はモジュール名で、<package>はパッケージ名です。
–add-exportsオプションは、複数回使用できますが、ソースモジュールとパッケージ名の特定の組み合わせに対して最大で1回使用できます。 各インスタンスの効果は、指定されたパッケージの修飾されたエクスポートをソースモジュールからターゲットモジュールに追加することです。 これは基本的に、モジュール宣言内のエクスポート句のコマンドライン形式、またはModule :: addExportsメソッドの無制限な形式の呼び出しです。 結果として、ターゲットモジュールがソースモジュールの名前付きパッケージ内のパブリックタイプにアクセスできるようになります。 ターゲットモジュールはソースモジュールをモジュール宣言のrequires節、Module :: addReadsメソッド、または–add-readsオプションのインスタンスです。 たとえば、jmx.wbtestモジュールに、java.managementモジュールの非エクスポートcom.sun.jmx.remote.internalパッケージのホワイトボックス・テストが含まれている場合、それが必要とするアクセスはオプションを使用して許可することができます。
–add-exports java.management/com.sun.jmx.remote.internal=jmx.wbtest
特殊なケースとして、<target-module>がALL-UNNAMEDの場合、ソースパッケージは、最初に存在するか、後で作成されるかに関係なく、名前のないすべてのモジュールにエクスポートされます。 したがって、java.managementモジュールのsun.managementパッケージへのアクセスは、オプションを介してクラスパス上のすべてのコードに与えることができます。
–add-exports java.management/sun.management=ALL-UNNAMED
–add-exportsオプションを使用すると、指定されたパッケージのパブリックタイプにアクセスできます。 コアリフレクションAPIのsetAccessibleメソッドを使用して、非公開のすべての要素にさらにアクセスしてアクセスできるようにする必要があることがあります。 これを行うには、実行時に–add-opensオプションを使用することができます。 –add-exportsオプションと同じ構文です:
–add-opens <source-module>/<package>=<target-module>(,<target-module>)*
<source-module>と<target-module>はモジュール名で、<package>はパッケージ名です。 –add-opensオプションは複数回使用できますが、ソースモジュールとパッケージ名の特定の組み合わせに対して最大で1回使用できます。 各インスタンスの効果は、名前付きパッケージの修飾されたオープンをソースモジュールからターゲットモジュールに追加することです。 これは基本的に、モジュール宣言のopens節のコマンドライン形式、またはModule :: addOpensメソッドの無制限な形式の呼び出しです。 結果として、ターゲットモジュール内のコードは、ターゲットモジュールがソースモジュールを読み取る限り、 ソースリフレクションAPIを使用して、ソースモジュールの名前付きパッケージ内のパブリックなどのすべてのタイプにアクセスできます。 オープンパッケージは、コンパイル時にエクスポートされていないパッケージと区別できないため、 –add-opensオプションはそのフェーズでは使用できません。
–add-exportsと–add-opensオプションは、細心の注意を払って使用する必要があります。 それらを使用して、ライブラリモジュールの内部API、またはJDK自体のアクセス権を取得することはできますが、自己責任で行ってください。 内部APIが変更または削除された場合、ライブラリまたはアプリケーションは失敗します 。
((((;゚Д゚)))))))
I wish you a Merry Christmas.
TAGS: Java ,JavaFX ,NetBeans |
2017年12月20日10:24 AM |
Java JavaFX NetBeans
このエントリーは、JavaFX Advent Calendar 2017 の 6 日目です。
昨日は @planet-az さんの「簡単なミュージックプレーヤーを作ってみた 」でした。
明日はこの記事を書いている現時点ではまだ空いてます。(^_^; きっと誰かが素敵な記事を投稿してくれると楽しみにしています。
今さらですが JavaFX の非同期処理を復習がてら簡単にみていきたいと思います。
では、次のようなプログラムを作ってみます。
long result = 1;
for (int i = 0; i < repeatProcessingNumber; i++) {
result += result;
初期値 1 で10回ループするプログラムです。
計算終了後は 1024 と結果を表示します。
これだけでは時間のかかる処理とはならないので
TimeUnit.MILLISECONDS.sleep(500);
とスレッドをスリープさせています。
プログラムの状態、計算中の値の表示やキャンセルなどのメッセージ表示、プログレスバーによるプログラムの進捗状態の可視化などを実装します。
思考停止状態でギナギナっと作ったプログラムは次のようなものです。
Warning! This code will make you headache.
You do not have to read it.
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
package jp . yucchi . badasynchronousprocessing4javafx ;
import java . util . concurrent . TimeUnit ;
import java . util . logging . Level ;
import java . util . logging . Logger ;
import javafx . application . Application ;
import javafx . geometry . Insets ;
import javafx . scene . Scene ;
import javafx . scene . control . Button ;
import javafx . scene . control . Label ;
import javafx . scene . control . ProgressBar ;
import javafx . scene . layout . FlowPane ;
import javafx . stage . Stage ;
/**
*
* @author Yucchi
*/
public class BadAsynchronousProcessing4JavaFX extends Application {
private Label statusLabel ;
private Label messageLabel ;
private Label interimResult ;
private Label resultLabel ;
private ProgressBar progressBar ;
private Button startButton ;
private Button cancelButton ;
private boolean cancelFlag ;
@Override
public void start ( Stage primaryStage ) {
System . out . println ( cancelFlag ) ;
statusLabel = new Label ( "STATUS" ) ;
statusLabel . setMinWidth ( 200 ) ;
messageLabel = new Label ( "MESSAGE" ) ;
messageLabel . setMinWidth ( 200 ) ;
interimResult = new Label ( "INTERIM RESULT" ) ;
interimResult . setMinWidth ( 200 ) ;
resultLabel = new Label ( "RESULT" ) ;
resultLabel . setMinWidth ( 200 ) ;
progressBar = new ProgressBar ( 0 ) ;
progressBar . setMinWidth ( 200 ) ;
startButton = new Button ( "START" ) ;
startButton . setMinWidth ( 200 ) ;
cancelButton = new Button ( "CANCEL" ) ;
cancelButton . setMinWidth ( 200 ) ;
cancelButton . setDisable ( true ) ;
FlowPane root = new FlowPane ( ) ;
root . setPadding ( new Insets ( 10 ) ) ;
root . setHgap ( 10 ) ;
root . setVgap ( 10 ) ;
root . getChildren ( ) . addAll ( statusLabel , messageLabel , interimResult , resultLabel , progressBar , startButton , cancelButton ) ;
Scene scene = new Scene ( root , 220 , 225 ) ;
primaryStage . setTitle ( this . getClass ( ) . getSimpleName ( ) ) ;
primaryStage . setScene ( scene ) ;
primaryStage . show ( ) ;
primaryStage . setOnCloseRequest ( we -> {
} ) ;
startButton . setOnAction ( ae -> {
resultLabel . setText ( "RESULT" ) ;
executeTask ( ) ;
} ) ;
cancelButton . setOnAction ( ae -> {
startButton . setDisable ( false ) ;
cancelButton . setDisable ( true ) ;
statusLabel . setText ( "CANCELED" ) ;
messageLabel . setText ( "Cancelled!" ) ;
progressBar . setProgress ( 0.0 ) ;
interimResult . setText ( "INTERIM RESULT" ) ;
cancelFlag = true ;
} ) ;
}
/**
* @param args the command line arguments
*/
public static void main ( String [ ] args ) {
launch ( args ) ;
}
private void executeTask ( ) {
startButton . setDisable ( true ) ;
cancelButton . setDisable ( false ) ;
int repeatProcessingNumber = 10 ;
long result = 1 ;
statusLabel . setText ( "RUNNING" ) ;
messageLabel . setText ( "Start!" ) ;
interimResult . setText ( String . valueOf ( result ) ) ;
progressBar . setProgress ( 0 ) ;
for ( int i = 0 ; i < repeatProcessingNumber ; i ++ ) {
if ( cancelFlag ) {
System . out . println ( "This program has been canceled." ) ;
break ;
}
try {
TimeUnit . MILLISECONDS . sleep ( 500 ) ;
} catch ( InterruptedException ex ) {
Logger . getLogger ( BadAsynchronousProcessing4JavaFX . class . getName ( ) ) . log ( Level . SEVERE , null , ex ) ;
if ( cancelFlag ) {
System . out . println ( "This program has been canceled." ) ;
break ;
}
}
result += result ;
messageLabel . setText ( String . format ( "%d/%d" , i + 1 , repeatProcessingNumber ) ) ;
interimResult . setText ( String . valueOf ( result ) ) ;
progressBar . setProgress ( i ) ;
}
if ( ! cancelFlag ) {
statusLabel . setText ( "SUCCEEDED" ) ;
resultLabel . setText ( String . valueOf ( result ) ) ;
} else {
cancelFlag = false ;
}
startButton . setDisable ( false ) ;
cancelButton . setDisable ( true ) ;
}
}
それではこのプログラムを実行してみましょう。
「START」 ボタンを押した瞬間プロブラムがフリーズしたようになりました。
そして暫くすると結果が表示されました。
これは駄目ですね。(>_<。)
プログラムがビジー状態で応答しなくなってしまっては使い物になりません。
進捗状態がわからないのは致命的です。
そして唐突に計算が終了してプログラムが完了する。
そう言えば遠い昔にこのような状態のプログラムが当たり前のように存在していたような気がする。
今の時代こんなのは絶対に許されません。
何がいけないのか?
それは時間のかかる処理を JavaFX アプリケーションスレッド上で行っているからです。
NetBeans のプロファイラで確認してみます。
JavaFX Application Thread タイムラインを見てください。
START ボタンを押した瞬間に時間のかかる処理(スレッドスリープ)が走ってます。(緑色から紫色に変わっているところ)
これでは UI の更新処理などはできませんね。
時間のかかる処理をしている間プログラムの応答は無くなり、ビジー状態のようになってしまいます。
つまり、時間のかかる処理用にスレッドをもう一つ起こせばこの問題は解決するはずです。
この問題を解決するのに最適な方法は javafx.concurrentパッケージを使用することです。
javafx.concurrent パッケージは、Worker インタフェースと、2つの具体的な実装である Task および Service クラスで構成されています。
Worker インタフェースは、バックグラウンドスレッド上の Worker オブジェクトの状態、進捗を監視可能なプロパティで公開しています。
状態は ReadOnlyObjectProperty<Worker.State> stateProperty で確認できます。
次のように Enum Worker.State で定義されています。
READY Workerがまだ実行されておらず、実行の準備ができているか、またはWorkerが再初期化されたことを示します。
SCHEDULED Workerの実行がスケジュールされているが、現在は実行中ではないことを示します。
RUNNING このWorkerが実行中であることを示します。
SUCCEEDED このWorkerが正常に完了しており、valueプロパティから読み取る準備ができている有効な結果があることを示します。
CANCELLED このWorkerがWorker.cancel()メソッドによって取り消されたことを示します。
FAILED 通常は予期しない条件が発生したことによって、このWorkerが失敗したことを示します。
これらは JavaFX アプリケーションスレッドから使用できます。
Worker オブジェクトによって実行される処理の進捗は、totalWork、workDone、progress など、3つの異なるプロパティを通じて取得できます。
Worker オブジェクトの状態が変化するときに発生するイベントは WorkerStateEventクラスによって指定されます。
これは Task クラスと Service クラスの両方に EventTarget インタフェースが実装され状態イベントのリスニングがサポートされているからです。
今回は再利用の必要が無い Worker オブジェクトを生成するので Task クラスを利用します。
上記のように JavaFX の Task クラスは FutureTask クラスの完全に監視可能にした実装となっています。
この Task クラスを拡張しバックグラウンドスレッドで実行するロジックを実装します。
懐かしのSwingWorker クラスを思い出させてくれますね。(もう、ほとんど覚えてないけどね)
SwingWorker では doInBackground() メソッドにてバックグラウンドスレッドロジックを呼び出します。
JavaFX の場合は call() メソッドです。
doInBackground() メソッド、call() メソッドともにどちらも抽象メソッドなのでオーバーライドして使います。
call() メソッドは Task クラスが実行されるときに呼び出され、call() メソッドに実装されたバックグラウンドスレッドロジックを実行します。
つまり、仕組み的には SwingWorker とほとんど変わりはないので馴染みやすいかも知れません。
さて、JavaFX のために用意されたのにこれだけではあまり意味がありません。
この Task クラスの実行にあたり updateProgress() メソッド、updateMessage() メソッド、updateValue() メソッド、updateTitle() メソッドを呼び出すことができます。
これらを使用するとプログラムの状態、進捗状況、実行中の処理の結果の一部を返すなどを可視化できます。
ちょっと updateValue() メソッドの実装をみてみましょう。
protected void updateValue ( V value ) {
if ( isFxApplicationThread ( ) ) {
this . value . set ( value ) ;
} else {
// As with the workDone, it might be that the background thread
// will update this value quite frequently, and we need
// to throttle the updates so as not to completely clobber
// the event dispatching system.
if ( valueUpdate . getAndSet ( value ) == null ) {
runLater ( ( ) -> Task . this . value . set ( valueUpdate . getAndSet ( null ) ) ) ;
}
}
}
呼び出しが JavaFX アプリケーションスレッドかそうでないかで分岐処理をおこなっています。
JavaFX アプリケーションスレッドからだったらそのまま値を更新します。
void runLater ( Runnable r ) {
Platform . runLater ( r ) ;
}
でなければ runLater()メソッドで javafx.application.Platform クラスの runLater(Runnable runnable) メソッドを呼び出して値を更新します。
何故なら、JavaFX Sceneグラフは、スレッドセーフではなく、JavaFXアプリケーションスレッドのみアクセスおよび変更できます。
これは Swing では javax.swing.SwingUtilities クラスの invokeLater(Runnable doRun) メソッドと同じようなものです。
JDK1.3 からは java.awt.EventQueue.invokeLater() を呼び出すように作られています。
このinvokeLater(Runnable doRun) メソッドはどのスレッドからも呼び出しが可能となっています。
javafx.application.Platform クラスの runLater(Runnable runnable) メソッドも任意のスレッドから呼び出し可能となっています。
updateProgress() メソッド、updateMessage() メソッド、updateValue() メソッド、updateTitle() メソッドの API ドキュメントには「このメソッドは、任意のスレッドから安全に呼び出すことができます。」と記載されています。
ちなみに updateValue() メソッド は JavaFX 8 からなので上記のようにラムダ式を使い綺麗に書かれています。
古くからある updateMessage() メソッドは次のように Java SE 7 の時代のコードのままとなっています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void updateMessage ( String message ) {
if ( isFxApplicationThread ( ) ) {
this . message . set ( message ) ;
} else {
// As with the workDone, it might be that the background thread
// will update this message quite frequently, and we need
// to throttle the updates so as not to completely clobber
// the event dispatching system.
if ( messageUpdate . getAndSet ( message ) == null ) {
runLater ( new Runnable ( ) {
@Override public void run ( ) {
final String message = messageUpdate . getAndSet ( null ) ;
Task . this . message . set ( message ) ;
}
} ) ;
}
}
}
このようなのを目にすると Java は開発者のために進化し続けている言語なんだなぁって感慨深いものがありますね。
SwingWorker のように publish() メソッド、process() メソッドなんてのは JavaFX には必要なくなってます。
このようにありがたい機能を標準で実装されている Task クラスですがさらに便利なプロパティを持っています。
これから先程のプログラムを改善するのにいくつかの Task クラスのプロパティを使用します。
それらプロパティは JavaFX のバインドという機能により快適に使用することが可能です。
JSR 295 Beans Binding と同じようなものです。
実際にどのように利用するかというと Task クラスの実行状態(Worker オブジェクト)のプロパティを利用して「START」ボタン、「CANCEL」ボタンの活性化、非活性化をおこないます。
また、バックグラウンドスレッド(Worker オブジェクト)の状態、計算結果の一部を返す処理、プログレスバーによるプログラムの進捗状態の表示もおこないます。
これらの対応を施したプログラムは次のようになります。
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
package jp . yucchi . asynchronousprocessing4javafx ;
import java . util . concurrent . ExecutorService ;
import java . util . concurrent . Executors ;
import java . util . concurrent . TimeUnit ;
import java . util . logging . Level ;
import java . util . logging . Logger ;
import javafx . application . Application ;
import javafx . concurrent . Task ;
import static javafx . concurrent . Worker . State . RUNNING ;
import javafx . geometry . Insets ;
import javafx . scene . Scene ;
import javafx . scene . control . Button ;
import javafx . scene . control . Label ;
import javafx . scene . control . ProgressBar ;
import javafx . scene . layout . FlowPane ;
import javafx . stage . Stage ;
/**
*
* @author Yucchi
*/
public class AsynchronousProcessing4JavaFX extends Application {
private final ExecutorService executorService = Executors . newSingleThreadExecutor ( ) ;
private Label statusLabel ;
private Label messageLabel ;
private Label interimResult ;
private Label resultLabel ;
private ProgressBar progressBar ;
private Button startButton ;
private Button cancelButton ;
@Override
public void start ( Stage primaryStage ) {
statusLabel = new Label ( "STATUS" ) ;
statusLabel . setMinWidth ( 200 ) ;
messageLabel = new Label ( "MESSAGE" ) ;
messageLabel . setMinWidth ( 200 ) ;
interimResult = new Label ( "INTERIM RESULT" ) ;
interimResult . setMinWidth ( 200 ) ;
resultLabel = new Label ( "RESULT" ) ;
resultLabel . setMinWidth ( 200 ) ;
progressBar = new ProgressBar ( 0 ) ;
progressBar . setMinWidth ( 200 ) ;
startButton = new Button ( "START" ) ;
startButton . setMinWidth ( 200 ) ;
cancelButton = new Button ( "CANCEL" ) ;
cancelButton . setMinWidth ( 200 ) ;
cancelButton . setDisable ( true ) ;
FlowPane root = new FlowPane ( ) ;
root . setPadding ( new Insets ( 10 ) ) ;
root . setHgap ( 10 ) ;
root . setVgap ( 10 ) ;
root . getChildren ( ) . addAll ( statusLabel , messageLabel , interimResult , resultLabel , progressBar , startButton , cancelButton ) ;
Scene scene = new Scene ( root , 220 , 225 ) ;
primaryStage . setTitle ( this . getClass ( ) . getSimpleName ( ) ) ;
primaryStage . setScene ( scene ) ;
primaryStage . show ( ) ;
primaryStage . setOnCloseRequest ( we -> {
executorService . shutdownNow ( ) ;
} ) ;
startButton . setOnAction ( ae -> {
resultLabel . setText ( "RESULT" ) ;
executeBackgroundTask ( ) ;
} ) ;
}
public static void main ( String [ ] args ) {
launch ( args ) ;
}
private void executeBackgroundTask ( ) {
Task <Long> task = new Task <> ( ) {
@Override
public Long call ( ) {
int repeatProcessingNumber = 10 ;
long result = 1 ;
updateMessage ( "Start!" ) ;
updateValue ( result ) ;
updateProgress ( 0 , repeatProcessingNumber ) ;
for ( int i = 0 ; i < repeatProcessingNumber ; i ++ ) {
if ( isCancelled ( ) ) {
System . out . println ( "This program has been canceled." ) ;
break ;
}
try {
TimeUnit . MILLISECONDS . sleep ( 500 ) ;
} catch ( InterruptedException ex ) {
Logger . getLogger ( AsynchronousProcessing4JavaFX . class . getName ( ) ) . log ( Level . SEVERE , null , ex ) ;
if ( isCancelled ( ) ) {
System . out . println ( "Canceled by InterruptedException." ) ;
break ;
}
}
result += result ;
updateMessage ( String . format ( "%d/%d" , i + 1 , repeatProcessingNumber ) ) ; ;
updateValue ( result ) ;
updateProgress ( i + 1 , repeatProcessingNumber ) ;
}
return result ;
}
// Workerオブジェクトの状態が変化したときのコールバックメソッド(5種類)
@Override
protected void scheduled ( ) {
super . succeeded ( ) ;
updateMessage ( "scheduled!" ) ;
System . out . println ( "Call scheduled()" ) ;
}
@Override
protected void running ( ) {
super . running ( ) ;
updateMessage ( "running!" ) ;
System . out . println ( "Call running()" ) ;
}
@Override
protected void succeeded ( ) {
super . succeeded ( ) ;
updateMessage ( "Succeeded!" ) ;
System . out . println ( "Call succeeded()" ) ;
resultLabel . setText ( "RESULT: " + getValue ( ) . toString ( ) ) ;
}
@Override
protected void cancelled ( ) {
super . cancelled ( ) ;
updateMessage ( "Cancelled!" ) ;
System . out . println ( "Call cancelled()" ) ;
progressBar . progressProperty ( ) . unbind ( ) ;
progressBar . setProgress ( 0.0 ) ;
interimResult . textProperty ( ) . unbind ( ) ;
interimResult . setText ( "INTERIM RESULT" ) ;
}
@Override
protected void failed ( ) {
super . failed ( ) ;
updateMessage ( "Failed!" ) ;
System . out . println ( "Call failed()" ) ;
}
} ;
// WorkerStateEven を使ってWorkerオブジェクトの状態を監視
task . setOnScheduled ( wse -> System . out . println ( "setOnScheduled" ) ) ;
task . setOnRunning ( wse -> System . out . println ( "setOnRunning" ) ) ;
task . setOnSucceeded ( wse -> System . out . println ( "setOnSucceeded" ) ) ;
task . setOnCancelled ( wse -> System . out . println ( "setOnCancelled" ) ) ;
task . setOnFailed ( wse -> System . out . println ( "setOnFailed" ) ) ;
// textProperty と Workerオブジェクトの stateProperty をバインド
statusLabel . textProperty ( ) . bind ( task . stateProperty ( ) . asString ( ) ) ;
// textProperty と Workerオブジェクトの messageProperty をバインド
messageLabel . textProperty ( ) . bind ( task . messageProperty ( ) ) ;
// textProperty と Workerオブジェクトの valueProperty をバインド
interimResult . textProperty ( ) . bind ( task . valueProperty ( ) . asString ( ) ) ;
// progressProperty と Workerオブジェクトの progressProperty をバインド
progressBar . progressProperty ( ) . bind ( task . progressProperty ( ) ) ;
// disableProperty と Workerオブジェクトの stateProperty をバインド
startButton . disableProperty ( ) . bind ( task . stateProperty ( ) . isEqualTo ( RUNNING ) ) ;
cancelButton . disableProperty ( ) . bind ( task . stateProperty ( ) . isNotEqualTo ( RUNNING ) ) ;
cancelButton . setOnAction ( ae -> {
task . cancel ( ) ;
} ) ;
// ExecutorService を利用してバックグラウンドスレッドを開始
executorService . submit ( task ) ;
}
}
このプログラムの実行結果を見てみましょう。
ついでに NetBeans のプロファイラでバックグラウンドスレッドが生成されているのを確認しましょう。
pool-2-thread-1 がバックグラウンドスレッドとして起動されています。
これで JavaFX アプリケーションスレッドはイベント処理が可能となります。
画面が固まることなくバックグラウンドスレッドの状態、計算結果の一部(途中経過)、プログレスバーも表示されています。
また、「START」ボタン、「CANCEL」ボタンの活性化、非活性化もちゃんとできています。
まず、Task クラスをみていきましょう。
今回はcall() メソッドを実行するために java.util.concurrent の ExecutorService インタフェースを利用しました。
184行目です。
executorService.submit(task);
タスクをパラメータとして指定したスレッドの開始としては次の方法もあります。
Thread th = new Thread(task); th.setDaemon(true); th.start();
call() メソッドに時間のかかる処理を記述します。バックグラウンドスレッドロジックです。
call() メソッドが呼ばれたら初期値をセットします。
updateMessage() メソッド、updateValue() メソッド、updateProgress() メソッドはプロパティバインドを利用します。
バックグラウンドスレッドが開始されたらキャンセル操作がリクエストされたかも監視しなければいけません。
キャンセルは java.util.concurrent.FutureTask クラスの isCancelled() メソッドを使います。
今回、スレッドをスリープさせているので InterruptedException をスローする場合があります。
また InterruptedException は Task のキャンセルの結果として発生する場合があるため、確実に InterruptedException を処理し、キャンセル状態を調べる必要があります。
それに対応するために 102 行目の catch 節のところにも isCancelled() メソッドを使用しています。
このようにバックグラウンドスレッドロジック内にブロッキング・コールがある場合は注意が必要となります。
バックグラウンドスレッドでの処理の結果の一部を返すために 110 行目に updateValue() メソッドを使います。
同様に 109 行目で messageLabel 、111 行目で progressBar の更新も行っています。
これらの更新は JavaFX のバインドという便利な機能を使います。
Task クラスのプロパティと Label のテキストプロパティ、ProgressBar の プログレスプロパティとバインドします。
168 行目から次のようにバインドしています。
// textProperty と Workerオブジェクトの stateProperty をバインド statusLabel.textProperty().bind(task.stateProperty().asString()); // textProperty と Workerオブジェクトの messageProperty をバインド messageLabel.textProperty().bind(task.messageProperty()); // textProperty と Workerオブジェクトの valueProperty をバインド interimResult.textProperty().bind(task.valueProperty().asString()); // progressProperty と Workerオブジェクトの progressProperty をバインド progressBar.progressProperty().bind(task.progressProperty());
これで自動的に Task クラス(Workerオブジェクト)のプロパティ updateXXX() メソッドで更新されたら Label 、ProgressBar も更新されます。
もし、キャンセル操作が実行され、プログラムの実行結果の一部が表示されたままになるのが嫌なら
141 行目の Worker オブジェクトの状態が変化したときのコールバックメソッドである cancelled() メソッドの処理でいったん unbind() メソッドによりバインドを解除して初期値を設定し直すといいでしょう。
@Override protected void cancelled() { super.cancelled(); updateMessage(“Cancelled!”); System.out.println(“Call cancelled()”); progressBar.progressProperty().unbind(); progressBar.setProgress(0.0); interimResult.textProperty().unbind(); interimResult.setText(“INTERIM RESULT”); }
このようなコールバックメソッドはこの他にもあり、全部で5種類あります。
必要に応じて便利に使えます。
118 行目から 158 行目を参照ください。
バックグラウンドタスクが無事に終了したら 134 行目の succeeded() メソッドがコールバックされます。
@Override protected void succeeded() { super.succeeded(); updateMessage(“Succeeded!”); System.out.println(“Call succeeded()”); resultLabel.setText(“RESULT: ” + getValue().toString()); }
ここで Task クラスに戻り値がある場合それを取得します。
戻り値の型はジェネリクスの型パラーメターで指定されたものです。
今回のプログラムでは Long となっています。
必然的に Call() メソッドの戻り値も Long です。
計算結果は 138 行目で javafx.concurrent.Task クラスの getValue() メソッドで取得しています。
resultLabel.setText(“RESULT: ” + getValue().toString());
そして、計算結果を Label のテキストとしてセットしています。
これらのコールバックメソッドは JavaFX アプリケションスレッド上で実行されるので RuntimeException も出ません。
コールバックメソッドを利用する以外にも WorkerStateEvent を使ってイベント処理として扱うこともできます。
160 行目から 165 行目のようにイベントハンドラを登録して使用します。
// WorkerStateEven を使ってWorkerオブジェクトの状態を監視 task.setOnScheduled(wse -> System.out.println(“setOnScheduled”)); task.setOnRunning(wse -> System.out.println(“setOnRunning”)); task.setOnSucceeded(wse -> System.out.println(“setOnSucceeded”)); task.setOnCancelled(wse -> System.out.println(“setOnCancelled”)); task.setOnFailed(wse -> System.out.println(“setOnFailed”));
では、このプログラムを動かしたときの Worker オブジェクトの state プロパティ がどのように変化するか確認します。
プログラムが完する場合次のように出力されます。
setOnScheduled Call scheduled() setOnRunning Call running() setOnSucceeded Call succeeded()
この出力結果から
SCHEDULED RUNNING SUCCEEDED
と遷移しているのが解ります。
プログラムを途中でキャンセルしてみる場合も確認します。
setOnScheduled Call scheduled() setOnRunning Call running() setOnCancelled Call cancelled() Canceled by InterruptedException.
予想通りの結果ですね。
SCHEDULED RUNNING CANCELLED
上記のように遷移しています。これらの動作は想像通りでした。
コールバックメソッドより WorkerStateEvent を使ってイベントハンドラを登録したほうが早い結果となっています。
だからといってメリット、デメリットがあるかどうかは私には解りません。
さて、最後に、「START」ボタン、「CANCEL」ボタンの活性化、非活性化をみてみましょう。
175 行目から 177 行目です。
// disableProperty と Workerオブジェクトの stateProperty をバインド startButton.disableProperty().bind(task.stateProperty().isEqualTo(RUNNING)); cancelButton.disableProperty().bind(task.stateProperty().isNotEqualTo(RUNNING));
Button の disableProperty と Workerオブジェクトの stateProperty をバインドしているだけです。
startButton は Workerオブジェクトの stateProperty が RUNNING なら非活性化となります。
cancelButton は Workerオブジェクトの stateProperty が RUNNING でなければ非活性化となります。
つまり、Workerオブジェクトの stateProperty が RUNNING の場合だけ活性化します。
これでいちいち操作を行うたびに Button を活性化、非活性化処理を記述しなくてすみます。
このように JavaFX には非同期処理を簡単に扱うことができるようになっています。
SwingWorker を使って非同期処理プログラムを組んでいるのなら JavaFX の javafx.concurrentパッケージを使ってみて時代の流れを感じ取ってみてはいかがでしょうか。
お終い!
I wish you a Merry Christmas.
TAGS: Java ,JavaFX ,NetBeans |
2017年12月6日2:28 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 |
JavaFX
この記事は、JavaFX Advent Calendar 2015 の14日目の記事です。
昨日は tomo_taka01 さんの 「Server Event Client Sample 」 です。明日は kimukou さんの「試用もふくめて basilisk-fw あたりの話を書こうかと」です。
今年は Windows 10 がリリースされ生体認証でログインできる Windows Hello が実装されました。
Windows OS にこのような機能が搭載されてしまったからにはこういった時代になるんだろうなってことで、
とりあえず WebCam でも買って試してみようかと思ったら特殊なカメラ機能が必要で量販家電店で売っているものでは駄目でした。
少し調べてみたら Intel RealSense 3Dカメラ(F200) が intel のサイトから購入可能だったので買ってしまいました。
このカメラで何ができるかはこちらをご覧ください。
インテル® RealSense™ テクノロジー
このカメラを購入した理由の一つに Intel® RealSense™ SDK が開発者登録さえすれば無償で入手可能であり、Java もサポートしているということでした。
購入はこちらからとなります。
Intel® RealSense™ Developer Kit
これは開発者向けのキットということで一般的に市販されている webCam のように便利なアプリケーションは付属してません。
ただし、一般的な WebCam には無い機能を搭載しているのでいろんなことができるようです。
私は新しい物好きな性格から Windows Hello を使いたかっただけなのですが、開発環境に Java 言語もサポートとあったのでちょっと試してみることにしました。
はじめに、Intel RealSense 3Dカメラ(F200) は次のような特徴をもっています。
Specifications
Shorter range (0.2 meters – 1.2 meters, indoors only) Depth/IR: 640×480 resolution at 60fps RGB: 1080p at 30fps USB 3.0 required Developer Kit Dimensions: 150mm x 30mm x 58mm
Targeted Usages
Full hand-skeletal tracking and gesture control 3D segmentation Facial analysis Depth-enhanced Augmented Reality Speech 3D Capture for faces
SYSTEM REQUIREMENTS
Ports: USB 3.0 Supported CPUs: 4th generation (or later) Intel® Core™ processor Supported Operating Systems: Microsoft Windows 8.1* (or later) (64-bit)
私はまさか CPU が第4世代の Haswell 以降を要求されるとは思ってなかったので Intel RealSense 3Dカメラ(F200) が届いて箱に書いてある Minimum System Requirements を見た瞬間、目が点になりました。
私の パソコンは Sandy Bridge なので・・・
大丈夫だろうとインストールを始めるも、Depth Camera Manager (DCM) のインストールでファームウェアがアップデートできないとかのエラーで撃沈しました。
しかたなく Skylake 環境にパソコンをアップグレートしてインストールを完了しました。(>_<。)
しかし、このファームウェアのアップデートさえ完了してしまえば Sandy Bridge でもインストールできてしまうんじゃないかと思い試してみたらあっけなくインストールは完了しました。
サポート外 CPU なので動作に一抹の不安を覚えます。
現状としては、Facial analysis 関係は問題無さそうです。(この発言に責任は持てません!)
さて、Intel RealSense 3Dカメラ(F200) についてはこのくらいにして私の大好きな Java で動かしてみましょう。
当然、今だと Swing なんて過去の技術なんか使わずにリッチな UI を提供してくれる JavaFX を使います。
JavaFX を使って WebCam を動かすにはどうすればいいのか?
早速、悩みます。
ここで Intel RealSense 3Dカメラ(F200) は置いといて、まず一般的に Java で WebCam を使うにはどうすればいいのか考えてみました。
まず、思いうかんだのは標準ライブラリに webCam やマイクなどハードウェアを扱う API が存在するかです。
記憶に無いです。ふと、Java Media Framework (JMF) が脳裏をよぎりました。(^_^;
それは無かったことにして他を調べてみました。(ヲヒ!
1. OpenCV を使う
2. sarxos Webcam-Capture API を使う
この二つが良さげでした。
これら二つの方法はどちらも顔検出が容易にできます。
OpenCV で顔検出のプログラムはビデオファイルを使った物は下記動画サイトにアップロードしてあります。
https://youtu.be/Zjc6-IF3rtM
WebCam を使って顔検出するプログラムを載せようと思ったけど長くなるので GitHub にアップしておきます。
https://github.com/Yucchi-1995/OpenCV_with_JavaFX_WebCam
sarxos Webcam-Capture API を使う方法は Example がたくさんあるし、JavaFX を利用する方法もあるので省略します。
たぶん、これが一番人気なんじゃないだろうか。
それでは、Intel RealSense SDK を使って JavaFX で Intel RealSense 3Dカメラ(F200) を動かしてみることにしましょう。
とりあえずカメラ画像を映すだけのプログラムを組むことにします。
Intel RealSense 3Dカメラ(F200) は、Color, Depth, IR と3種類のストリームを得ることができます。
なのでラジオボタンで切り替えられるようにします。
いちおう、開始、停止ボタンもつけておきます。
下図のようなプログラムがなんとかできあがりました。
動画はこちらになります。
https://youtu.be/jywwwYcwZA8
Intel RealSense SDK のドキュメントは英語で書かれていて日本語のものは現在ありません。
英語が解らない私は翻訳支援ソフトを頼りに悩みながらプログラムを組むことになりました。
それに残念なことに比較的新しい Windows OS 専用ということもあり、わざわざマルチプラットフォーム対応の Java 言語で開発するといった人もほとんど見られません。
Intel も本気で Java 言語をサポートする気は無いらしく Intel RealSense SDK のドキュメントは間違いだらけで唖然とします。
12月に入って新しく R5 バージョンがリリースされましたが既存のバグが直るどころか増えていたので今回は R4 を使ってます。
頼りのグーグル先生に聞いて教えてもらう情報は私と同じようにバグを踏んで困っている Java 開発者のどうしたらいいんだ?ってのが多いです。
こういう状況なので、いつものことですが間違いやおかしなことをしているかも知れませんのであしからず! (^_^;
ということで、次のようなコードでプログラムを組みました。
全部載せると長くなるので RealSenseController.java だけです。
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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
package jp . yucchi . intelrealsense ;
import intel . rssdk . PXCMCapture ;
import intel . rssdk . PXCMImage ;
import intel . rssdk . PXCMSenseManager ;
import intel . rssdk . pxcmStatus ;
import java . awt . image . BufferedImage ;
import java . net . URL ;
import java . util . ResourceBundle ;
import javafx . animation . KeyFrame ;
import javafx . animation . KeyValue ;
import javafx . animation . Timeline ;
import javafx . application . Platform ;
import javafx . beans . property . DoubleProperty ;
import javafx . beans . property . SimpleDoubleProperty ;
import javafx . beans . property . SimpleStringProperty ;
import javafx . beans . property . StringProperty ;
import javafx . concurrent . Service ;
import javafx . concurrent . Task ;
import javafx . embed . swing . SwingFXUtils ;
import javafx . event . ActionEvent ;
import javafx . event . EventHandler ;
import javafx . fxml . FXML ;
import javafx . fxml . Initializable ;
import javafx . scene . control . Alert ;
import javafx . scene . control . Button ;
import javafx . scene . control . ButtonType ;
import javafx . scene . control . RadioButton ;
import javafx . scene . control . TextArea ;
import javafx . scene . control . ToggleGroup ;
import javafx . scene . image . Image ;
import javafx . scene . image . ImageView ;
import javafx . scene . layout . AnchorPane ;
import javafx . stage . StageStyle ;
import javafx . util . Duration ;
/**
*
* @author Yucchi
*/
public class RealSenseController implements Initializable {
@FXML
private AnchorPane anchorPane ;
@FXML
private ImageView imageView ;
@FXML
private ToggleGroup toggleGroup ;
@FXML
private RadioButton colorRadioButton ;
@FXML
private RadioButton depthRadioButton ;
@FXML
private RadioButton irRadioButton ;
@FXML
private Button startButton ;
@FXML
private Button stopButton ;
@FXML
private Button exitButton ;
private PXCMSenseManager senseManager ;
private StreamService streamService ;
private pxcmStatus pxcmStatus ;
private static final int WIDTH = 640 ;
private static final int HEIGHT = 480 ;
private Image image ;
private final StringProperty errorContent = new SimpleStringProperty ( ) ;
@FXML
private void handleStartButtonAction ( ActionEvent event ) {
streamService = new StreamService ( ) ;
streamService . setOnSucceeded ( wse -> {
if ( streamService . getValue ( ) != null ) {
imageView . setImage ( image ) ;
streamService . restart ( ) ;
}
} ) ;
streamService . setOnFailed ( wse -> {
if ( errorContent . getValue ( ) == null ) {
errorContent . setValue ( "Error!\n" + "StreamService Failed." ) ;
}
errorProcessing ( ) ;
} ) ;
// SenseManagerを生成する
senseManager = PXCMSenseManager . CreateInstance ( ) ;
// カラーストリームを有効にする
senseManager . EnableStream ( PXCMCapture . StreamType . STREAM_TYPE_COLOR , WIDTH , HEIGHT ) ;
// Depth ストリームを有効にする
senseManager . EnableStream ( PXCMCapture . StreamType . STREAM_TYPE_DEPTH , WIDTH , HEIGHT ) ;
// IR ストリームを有効にする
senseManager . EnableStream ( PXCMCapture . StreamType . STREAM_TYPE_IR , WIDTH , HEIGHT ) ;
// PXCM_STATUS 初期化
pxcmStatus = senseManager . Init ( ) ;
// ミラーモードにする
senseManager . QueryCaptureManager ( ) . QueryDevice ( ) . SetMirrorMode ( PXCMCapture . Device . MirrorMode . MIRROR_MODE_HORIZONTAL ) ;
if ( ! streamService . isRunning ( ) ) {
streamService . reset ( ) ;
streamService . start ( ) ;
}
startButton . disableProperty ( ) . bind ( streamService . runningProperty ( ) ) ;
stopButton . disableProperty ( ) . bind ( streamService . runningProperty ( ) . not ( ) ) ;
}
@FXML
private void handleStoptButtonAction ( ActionEvent event ) {
if ( streamService . isRunning ( ) ) {
streamService . cancel ( ) ;
}
senseManager . Close ( ) ;
imageView . setImage ( new Image ( this . getClass ( ) . getResourceAsStream ( "resources/duke_cake.jpg" ) ) ) ;
}
@FXML
private void handleExittButtonAction ( ActionEvent event ) {
exitProcessing ( ) ;
}
@Override
public void initialize ( URL url , ResourceBundle rb ) {
imageView . setImage ( new Image ( this . getClass ( ) . getResourceAsStream ( "resources/duke_cake.jpg" ) ) ) ;
// ストリーミングタイプ選択ラジオスイッチ
colorRadioButton . setUserData ( "Color" ) ;
depthRadioButton . setUserData ( "Depth" ) ;
irRadioButton . setUserData ( "IR" ) ;
}
class StreamService extends Service <Image> {
@Override
protected Task <Image> createTask ( ) {
Task <Image> task = new Task <Image> ( ) {
@Override
protected Image call ( ) throws Exception {
if ( pxcmStatus == pxcmStatus . PXCM_STATUS_NO_ERROR ) {
// フレーム取得
if ( senseManager . AcquireFrame ( true ) . isSuccessful ( ) ) {
// フレームデータ取得
PXCMCapture . Sample sample = senseManager . QuerySample ( ) ;
// 選択されたストリームによる画像データ処理
switch ( toggleGroup . getSelectedToggle ( ) . getUserData ( ) . toString ( ) ) {
case "Color" :
if ( sample . color != null ) {
// データ取得
PXCMImage . ImageData cData = new PXCMImage . ImageData ( ) ;
// アクセス権を取得(アクセス権の種類、画像フォーマット、データ)
pxcmStatus = sample . color . AcquireAccess ( PXCMImage . Access . ACCESS_READ , PXCMImage . PixelFormat . PIXEL_FORMAT_RGB32 , cData ) ;
if ( pxcmStatus . compareTo ( pxcmStatus . PXCM_STATUS_NO_ERROR ) < 0 ) {
errorContent . setValue ( "Error!\n" + "Failed to AcquireAccess of ColorImage Data." ) ;
throw new Exception ( ) ;
}
// BufferedImage に変換 1ピクセルあたり4バイトに注意、PXCMImage.PixelFormat.PIXEL_FORMAT_RGB24 だと3バイト
int cBuff [ ] = new int [ cData . pitches [ 0 ] / 4 * HEIGHT ] ;
cData . ToIntArray ( 0 , cBuff ) ;
BufferedImage bImage = new BufferedImage ( WIDTH , HEIGHT , BufferedImage . TYPE_INT_RGB ) ;
bImage . setRGB ( 0 , 0 , WIDTH , HEIGHT , cBuff , 0 , cData . pitches [ 0 ] / 4 ) ;
// ImageView にセットできるように Image に変換
image = SwingFXUtils . toFXImage ( bImage , null ) ;
// データを解放
pxcmStatus = sample . color . ReleaseAccess ( cData ) ;
if ( pxcmStatus . compareTo ( pxcmStatus . PXCM_STATUS_NO_ERROR ) > 0 ) {
errorContent . setValue ( "Error!\n" + "Failed to ReleaseAccess of ColorImage Data." ) ;
throw new Exception ( ) ;
}
}
break ;
case "Depth" :
if ( sample . depth != null ) {
PXCMImage . ImageData dData = new PXCMImage . ImageData ( ) ;
sample . depth . AcquireAccess ( PXCMImage . Access . ACCESS_READ , PXCMImage . PixelFormat . PIXEL_FORMAT_RGB32 , dData ) ;
if ( pxcmStatus . compareTo ( pxcmStatus . PXCM_STATUS_NO_ERROR ) < 0 ) {
errorContent . setValue ( "Error!\n" + "Failed to AcquireAccess of DepthImage Data." ) ;
throw new Exception ( ) ;
}
int dBuff [ ] = new int [ dData . pitches [ 0 ] / 4 * HEIGHT ] ;
dData . ToIntArray ( 0 , dBuff ) ;
BufferedImage bImage = new BufferedImage ( WIDTH , HEIGHT , BufferedImage . TYPE_INT_RGB ) ;
bImage . setRGB ( 0 , 0 , WIDTH , HEIGHT , dBuff , 0 , dData . pitches [ 0 ] / 4 ) ;
image = SwingFXUtils . toFXImage ( bImage , null ) ;
pxcmStatus = sample . depth . ReleaseAccess ( dData ) ;
if ( pxcmStatus . compareTo ( pxcmStatus . PXCM_STATUS_NO_ERROR ) < 0 ) {
errorContent . setValue ( "Error!\n" + "Failed to ReleaseAccess of DepthImage Data." ) ;
throw new Exception ( ) ;
}
}
break ;
case "IR" :
if ( sample . ir != null ) {
PXCMImage . ImageData dData = new PXCMImage . ImageData ( ) ;
sample . ir . AcquireAccess ( PXCMImage . Access . ACCESS_READ , PXCMImage . PixelFormat . PIXEL_FORMAT_RGB32 , dData ) ;
if ( pxcmStatus . compareTo ( pxcmStatus . PXCM_STATUS_NO_ERROR ) < 0 ) {
errorContent . setValue ( "Error!\n" + "Failed to AcquireAccess of IRImage Data." ) ;
throw new Exception ( ) ;
}
int dBuff [ ] = new int [ dData . pitches [ 0 ] / 4 * HEIGHT ] ;
dData . ToIntArray ( 0 , dBuff ) ;
BufferedImage bImage = new BufferedImage ( WIDTH , HEIGHT , BufferedImage . TYPE_INT_RGB ) ;
bImage . setRGB ( 0 , 0 , WIDTH , HEIGHT , dBuff , 0 , dData . pitches [ 0 ] / 4 ) ;
image = SwingFXUtils . toFXImage ( bImage , null ) ;
pxcmStatus = sample . ir . ReleaseAccess ( dData ) ;
if ( pxcmStatus . compareTo ( pxcmStatus . PXCM_STATUS_NO_ERROR ) < 0 ) {
errorContent . setValue ( "Error!\n" + "Failed to ReleaseAccess of IRImage Data." ) ;
throw new Exception ( ) ;
}
}
break ;
default :
}
// 次のフレームデータを呼び出すためにフレームを解放する
senseManager . ReleaseFrame ( ) ;
} else {
// 極まれにフレーム取得失敗する
// errorContent.setValue("Failed to acquire frame.");
// errorProcessing();
}
} else {
errorContent . setValue ( "Error!\n" + "Failed to Initialize." ) ;
errorProcessing ( ) ;
}
return image ;
}
} ;
return task ;
}
}
private void errorProcessing ( ) {
Alert alert = new Alert ( Alert . AlertType . ERROR ) ;
TextArea textArea = new TextArea ( errorContent . get ( ) ) ;
textArea . setEditable ( false ) ;
alert . getDialogPane ( ) . setExpandableContent ( textArea ) ;
alert . initStyle ( StageStyle . TRANSPARENT ) ;
alert . setTitle ( "ERROR" ) ;
alert . setHeaderText ( "Error!\n"
+ "An unexpected error has occurred." ) ;
alert . setContentText ( "Exit the application." ) ;
alert . showAndWait ( )
. filter ( response -> response == ButtonType . OK )
. ifPresent ( response -> exitProcessing ( ) ) ;
}
private void exitProcessing ( ) {
// クロージングアニメーション
DoubleProperty closeOpacityProperty = new SimpleDoubleProperty ( 1.0 ) ;
anchorPane . getScene ( ) . getWindow ( ) . opacityProperty ( ) . bind ( closeOpacityProperty ) ;
Timeline closeTimeline = new Timeline (
new KeyFrame (
new Duration ( 100 ) ,
new KeyValue ( closeOpacityProperty , 1.0 )
) , new KeyFrame (
new Duration ( 2_500 ) ,
new KeyValue ( closeOpacityProperty , 0.0 )
) ) ;
EventHandler <ActionEvent> eh = ae -> {
if ( streamService != null && streamService . isRunning ( ) ) {
streamService . cancel ( ) ;
}
if ( pxcmStatus != null ) {
senseManager . Close ( ) ;
}
Platform . exit ( ) ;
System . exit ( 0 ) ;
} ;
closeTimeline . setOnFinished ( eh ) ;
closeTimeline . setCycleCount ( 1 ) ;
closeTimeline . play ( ) ;
}
}
プログラム全体は下記 GitHub でご覧ください。
https://github.com/Yucchi-1995/Intel_RealSense_First
このプログラムは START ボタンを押すとカメラ画像を ImageView にセットするためにバックグランドタスクを javafx.concurrent.Service<V> クラス を利用して処理するインスタンスを生成します。
Service<V> クラスはこのような繰り返し処理をおこなうバックグランドタスクを便利に安全に利用するには最適です。
Service<V> クラスはインタフェースWorker<V> を実装していてバックグラウンドタスクの状態を監視し、必要に応じて操作を取り消すことができます。
Service<V> クラスは再利用可能な Worker であり、リセットおよび再起動できます。
これらの機能を利用することにより Service<V> クラスによって得られた結果が STOP ボタンが押されるかプログラムを終了させるまで繰り返し ImageView にセットされます。
それでは ImageView にセットされるカメラから取得された画像はどのように処理されるのか見ていきましょう。
Intel RealSense SDK の機能を使うために SenseManager インスタンスを生成します。
senseManager = PXCMSenseManager.CreateInstance();
使用するストリームを有効にする。
// カラーストリームを有効にする senseManager.EnableStream(PXCMCapture.StreamType.STREAM_TYPE_COLOR, WIDTH, HEIGHT); // Depth ストリームを有効にする senseManager.EnableStream(PXCMCapture.StreamType.STREAM_TYPE_DEPTH, WIDTH, HEIGHT); // IR ストリームを有効にする senseManager.EnableStream(PXCMCapture.StreamType.STREAM_TYPE_IR, WIDTH, HEIGHT);
Intel RealSense SDK の初期化
// PXCM_STATUS 初期化 pxcmStatus = senseManager.Init();
ミラーモードに設定
// ミラーモードにする senseManager.QueryCaptureManager().QueryDevice().SetMirrorMode(PXCMCapture.Device.MirrorMode.MIRROR_MODE_HORIZONTAL);
ここまではカメラ関係の初期化処理といったところですね。
ここからは Service によるバックグランドタスクによる処理がはじまります。
if (!streamService.isRunning()) { streamService.reset(); streamService.start(); }
Intel RealSense SDK の初期化処理が正常におこなわれたか判定します。
if (pxcmStatus == pxcmStatus.PXCM_STATUS_NO_ERROR) { …
フレームが正常に取得できたか判定します。
if (senseManager.AcquireFrame(true).isSuccessful()) { …
AcquireFrame(true) でフレームの更新を行います。
このメソッドは引数無し、引数一つ、引数二つの3種類あります。
今回は全てのモジュールの更新処理を待つように引数一つのもを true にして渡しています。
引数二つのものはさらにタイムアウトミリ秒 ( int )で設定できるようです。
判定に boolean isSuccessful() メソッドを使っています。
フレームデータを取得します。
PXCMCapture.Sample sample = senseManager.QuerySample();
QuerySample() メソッドで全ての(Color, Depth, IR) データを取得します。
QuerySample() メソッドには引数で取得するデータを選択できる QuerySample(int mid) もあります。
ラジオボタンによって選択されたストリームの画像データを処理します。カラーモードだけ抜粋し見ていきす。
if (sample.color != null) { // データ取得 PXCMImage.ImageData cData = new PXCMImage.ImageData(); // アクセス権を取得(アクセス権の種類、画像フォーマット、データ) pxcmStatus = sample.color.AcquireAccess(PXCMImage.Access.ACCESS_READ, PXCMImage.PixelFormat.PIXEL_FORMAT_RGB32, cData);
if (pxcmStatus.compareTo(pxcmStatus.PXCM_STATUS_NO_ERROR) < 0) { errorContent.setValue(“Error!\n” + “Failed to AcquireAccess of ColorImage Data.”); throw new Exception(); }
// BufferedImage に変換 1ピクセルあたり4バイトに注意、PXCMImage.PixelFormat.PIXEL_FORMAT_RGB24 だと3バイト int cBuff[] = new int[cData.pitches[0] / 4 * HEIGHT]; cData.ToIntArray(0, cBuff); BufferedImage bImage = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); bImage.setRGB(0, 0, WIDTH, HEIGHT, cBuff, 0, cData.pitches[0] / 4);
// ImageView にセットできるように Image に変換 image = SwingFXUtils.toFXImage(bImage, null);
// データを解放 pxcmStatus = sample.color.ReleaseAccess(cData);
if (pxcmStatus.compareTo(pxcmStatus.PXCM_STATUS_NO_ERROR) > 0) { errorContent.setValue(“Error!\n” + “Failed to ReleaseAccess of ColorImage Data.”); throw new Exception(); } }
カラーストリームのフレームデータが null でないか判定してから処理をしています。
PXCMImage.ImageData インスタンスを生成
pxcmStatus AcquireAccess(Access access, PixelFormat format, ImageData data) メソッドでアクセス権を取得します。
このプログラムではアクセス権に読み取り、画像フォーマットは 32bit RGB を引数により指定しています。
三つ目の引数 PXCMImage.ImageData cData に出力されます。
アクセス権の種類や画像フォーマットに関してはドキュメントをご覧ください。
また、AcquireAccess メソッドは引数の数が違う物も用意されています。
こちらも詳しくはドキュメントをご覧ください。
このプログラムでは引数三つのこの pxcmStatus AcquireAccess(Access access, PixelFormat format, ImageData data) メソッドを利用しました。
これで 32bit RGB データ取得完了
そして処理が正常にできたか判定
BufferedImage に変換準備
1ピクセルあたり4バイト、cData.pitches[0] に1ラインあたりのバイト数が格納されているので4で割って WIDTH 値を算出
データコンバート用 int[] を生成
int cBuff[] = new int[cData.pitches[0] / 4 * HEIGHT];
32bit RGB データを int[] に変換
BufferedImage インスタンス生成
public void setRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) メソッドで BufferedImage にデータをセット
BufferedImage のままでは ImageView にセットできないので public static WritableImage toFXImage(BufferedImage bimg, WritableImage wimg) メソッドを使う
image = SwingFXUtils.toFXImage(bImage, null);
このメソッドは Swing/AWTとJavaFXのフォーマットの間でデータ型を変換するユーティリティクラスである SwingFXUtils の二つのうちの一つである。
もう一つは、public static BufferedImage fromFXImage(Image img, BufferedImage bimg) メソッドで、
指定されたJavaFX Imageオブジェクトのスナップショットを取得し、そのピクセルのコピーをBufferedImageオブジェクトに格納し、必要に応じて新しいオブジェクトを作成します。
ただし、Image.getPixelReader()メソッドの条件に従って読み取ることができるJavaFX Imageのみを変換します。(不可なら null を返す)
BufferedImage を扱う上で SwingFXUtils クラスのこの二つのメソッドを覚えておきましょう。
これで Image を返してバックグランドタスクは終了となるのですが、AcquireFrame(true) で取得したフレームを解放するの忘れてはいけません。
解放しないと次のフレームが取得できないからです。
pxcmStatus = sample.color.ReleaseAccess(cData);
Service よるバックグランドタスクが成功したら ImageView に ImageView に Image をセットして Service を再び開始します。
streamService.setOnSucceeded(wse -> { if (streamService.getValue() != null) { imageView.setImage(image); streamService.restart(); } });
STOP ボタンによる停止処理では
if (streamService.isRunning()) { streamService.cancel(); }
senseManager.Close();
cancel() メソッドにてバックグランドタスクを取り消します。
そして、PXCMSenseManager senseManager を Close() メソッドを使って解放します。
メソッド名の先頭が大文字なのに注意してください。
これで Intel RealSence SDK を使って WebCam によるストリーミング処理はできるようになりました。
思ったより簡単ですね。
これで終わりだとすると JavaFX 成分が少ないようなので超小ネタを・・・(^_^;
START ボタンと STOP ボタンの活性化状態を自動的にするには
プログラムが起動して STOP ボタンは非活性化状態、 START ボタンは活性化状態
そして START ボタンが押されると STOP ボタンが活性化状態に、 START ボタンは非活性化状態にしたい。
これって void bind(ObservableValue<? extends T> observable) メソッドを使えば簡単に実現できます。
startButton.disableProperty().bind(streamService.runningProperty()); stopButton.disableProperty().bind(streamService.runningProperty().not());
START ボタンは、Service の runningProperty() と一方向バインディングを設定してしまえばいいのです。
STOP ボタンは START ボタンと相反する動作にすればいいだけなので runningProperty() に not() メソッドをつけて否定計算させてしまえばいいだけです。
プログラム起動時にフェードインアニメーションをするには
JavaFX の FX は映画などの特殊撮影、トリック撮影効果、特殊効果などの英語の略称から付けられたらしいです。
なのでなるだけ少しでも FX 要素を含ませようと日々努力しています。(ヲヒ!
比較的簡単にできるのがプログラムの起動、終了時のアニメーションです。
起動時のアニメーション処理(フェードイン)をみてみます。
// オープニングアニメーション DoubleProperty openOpacityProperty = new SimpleDoubleProperty(0.0); stage.opacityProperty().bind(openOpacityProperty); Timeline openTimeline = new Timeline( new KeyFrame( new Duration(100), new KeyValue(openOpacityProperty, 0.0) ), new KeyFrame( new Duration(2_500), new KeyValue(openOpacityProperty, 1.0) )); openTimeline.setCycleCount(1); openTimeline.play();
アニメーションを Timeline を使っておこないます。
Timeline は、API ドキュメントに次のように記載されています。
Timelineを使用すると、すべてのJavaFXプロパティなど、あらゆるWritableValueの自由形式アニメーションを定義できます。 Timelineは、1つ以上のKeyFrameで定義し、個々のKeyFrameをKeyFrame.timeで指定した順序で順番に処理するために使用します。
アニメーション化されるプロパティは、KeyFrame.valuesでキー値として定義し、Timelineの初期位置(Timelineの方向によって異なる)を基準にしてKeyFrameの指定時間にターゲット・キー値に(またはターゲット・キー値から)補間されます。
つまり、キーフレームと時間と値でアニメーションを制御しているだけです。
Stage stage の opacityProperty() を 初期値 0.0 に設定された DoubleProperty openOpacityProperty にバインドしておきます。
Timeline で new Duration(100) によって100 ミリ秒間かけて opacityProperty() の値を KeyValue(openOpacityProperty, 0.0) で設定された 0.0 にアニメーションしょりします。(この場合 0.0 から 0.0 変化無しです)
次のフレームでは new Duration(2_500) で 2500 ミリ秒かけて opacityProperty() の値を new KeyValue(openOpacityProperty, 1.0) で設定された 1.0 までアニメーション処理しています。
起動時処理なので複数回の実行は必要なく一度だけでいいので setCycleCount(1); を指定します。
あとは play() メソッドでアプリケーションの開始時に実行されるだけです。(start(Stage stage) メソッド内に記述)
プログラム終了の時は closeTimeline.setOnFinished(eh); を利用してハンドラにアニメーション終了時にプログラム終了処理を渡してあげればいいだけです。
以上、超小ネタでした。
特殊効果ではないですが JavaFX は CSS によってプログラムの見栄えを変えることができます。
デザインセンスのある方は強力な武器となるでしょう。
あいにく私はデザインセンスは無いので心優しい人が提供している CSS を使わせていただいてます。
このプログラムでは Pixel Duke さんの JMetro を使っています。
今時の Windows OS によく似合います。
さて、カメラ画像を映すだけのことはできたのですが顔検出や、顔認識、顔の傾き具合、心拍数、表情などのデータも取得して利用したくなりますよね。
で、先ほどのプログラムをベースにこの付加価値を実装できるようなのでやってみました。
顔のパーツ情報(LandMark データ)は以前からのバグが放置されているので取得できませんでした。
最新バージョンの R5 (SDK version 7.0) ではランドマークに加えて FacialExpression データも取れなくなりました。
なので R4 (SDK version 6.0) を使用しています。
本当は音声認識もできるはずなのですがハンドラクラスの OnRecognition(PXCMSpeechRecognition.RecognitionData data) メソッドが悲しいことに動きません。
OnAlert(PXCMSpeechRecognition.AlertData data) は音声の開始や終了、音量が低いとかちゃんと返してくれただけに残念です。
これは R5 で直っているか確認していません。(たぶんこれも放置でしょう。)
音声認識ができればリッチな UI の JavaFX を音声で操る未来的なプログラムも容易に可能になるかなって思っていただけに残念です。
とりあえずできる範囲でトライしてできあがったのが次のようなプログラムです。
プログラムは GitHub にアップロードしてあります。
https://github.com/Yucchi-1995/IntelRealSenseFacialExpression
私、残念ながらもの凄くシャイでコミュ障なので作り物の顔でプログラムを動かしてます。
VIDEO
このプログラムですがフェイストラッキングモードを Depth モードを使わないように設定すると一般的な WebCam でも動きます。
faceConfig.SetTrackingMode(PXCMFaceConfiguration.TrackingModeType.FACE_MODE_COLOR_PLUS_DEPTH); を
faceConfig.SetTrackingMode(PXCMFaceConfiguration.TrackingModeType.FACE_MODE_COLOR); に変更します。
ただし、全ての機能が正しく動くわけではありません。
ちなみに顔認識はでたらめになります。
プログラムの動作の説明は長くなるのでしません。
コード内のコメントを参考に推理(ヲヒ! してください。
あと、Intel RealSense SDK の面白いドキュメントを参考にしてください。
まだまだ JavaFX 慣れていないので雑いことをいろいろやっていると思います。
バックグランドタスクで Image 返して バックグランドタスク内で Platform.runLater() を使っていろいろ更新かけてます。
Task<Void> として密な関係にしたほうがよかったのかなとか思ったりもします。
なにかおかしなところがあったらご指摘いただけるとうれしいです。
最後にもう一つ超小ネタを!
JavaFX で Dialog が標準で使えるようになって便利に使っているとほんの些細なことが気になります。
TextInputDialog なんですが TextField にユーザーからの入力をもらう時に使いますよね。
なのに TextField が空の状態でも OK ボタンが押せるようにデフォルトではなっています。
このプログラムでは TextField が空でなく、顔検出されていれば押せるようにすると変更しています。
とりあえず、ネタとして TextField が空では OK ボタンを押せないようにしてみましょう。
TextInputDialog dialog = new TextInputDialog(“”); dialog.getDialogPane().lookupButton(ButtonType.OK).disableProperty().bind(dialog.getEditor().textProperty().isEmpty());
簡単ですね!
OK ボタンの disableProperty() を textProperty().isEmpty() で返される値とバインドすればいいだけです。
これ簡単に使えるのに意外と使われてないんですよね。
ということでいろいろ楽しませてくれた Intel さんに Java が得意なサンタクロースがたくさんのプレゼントを届けることを祈って終わりにします。
TAGS: JavaFX |
2015年12月14日12:06 AM |
JavaFX
JDK8u40 で追加された Spinner と Dialog を試してみた。
JDK8u40 で追加された Spinner と Dialog を試してみた。 とりあえず回避方法をみつけた。
このエントリーは上記の続きです。
と言っても大したことはやっていません。
ただ、新しく追加されたこれらの情報が見つからないので適当にグリグリした記録です。(^_^;)
今回は、Formatted Text を Spinner に適用してみました。
前回までは Spinner に表示されるテキストは左よりになっていて違和感のかたまりがありました。
これを右よりにします。
そして、Spinner に入力される値によりテキストのスタイルも変更してみます。
私はどうしたらいいのか解らなかったのでリファクタリングを使い、Spinner クラスの TextField を下記のように設定していました。
Field field = spinner . getClass ( ) . getDeclaredField ( "textField" ) ;
field . setAccessible ( true ) ;
tf = ( TextField ) field . get ( spinner ) ;
tf . setStyle ( "-fx-text-fill: black; -fx-font: 14pt 'serif'; -fx-alignment: CENTER_RIGHT;" ) ;
これでも動いたのですが spinner.getEditor() で TextField にアクセスできることが後で発覚しました。((((;゜Д゜)))))))
public final TextField getEditor()
Gets the value of the property editor. Property description:The editor used by the Spinner control.
API ドキュメントはちゃんと見なければいけませんね。(^_^;)
これで Font 関係は設定できます。
次に Formatted Text を使ってみます。
これも全然情報が見当たらない。
○racle さん、早く新しいチュートリアルをだしてください!
今回は日本円の書式設定を試しました。
CurrencyStringConverter を使えばいいようです。
他にも IntegerStringConverter や ShortStringConverter やら Converter がたくさんあります。
今イチ使い方が良く解らないので適当に手探りでやってみました。
本当は、TextFormatter<Number>とspinner.valueProperty()のバインドさせたかったのですが何やら例外吐くので無理にバインドしないことにしました。(ヲヒ
とりあえず下記コードで目的は果たしました。
コードは部分的なものですが残りは今までとほぼ変わりはないので省略します。
全て見たい方は冒頭の関連エントリーのリンクを辿ってくだしませ。
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
@Override
public void initialize ( URL url , ResourceBundle rb ) {
spinner . setEditable ( true ) ;
// 書式設定
CurrencyStringConverter currencyStringConverter = new CurrencyStringConverter ( ) ;
TextFormatter <Number> formatter = new TextFormatter <> ( currencyStringConverter ) ;
// // 動くけど例外吐くからダメ (>_<。)
// formatter.valueProperty().bindBidirectional((Property)spinner.valueProperty());
// スピナーに書式設定
spinner . getEditor ( ) . setTextFormatter ( formatter ) ;
// バインド例外吐くから・・・
formatter . setValue ( INITAL_VALUE ) ;
// アプリケーション起動時のスタイル
spinner . getEditor ( ) . setStyle ( "-fx-text-fill: black; -fx-font: 14pt 'serif'; -fx-alignment: CENTER_RIGHT;" ) ;
// Spinner のデザインを変更
spinner . getStyleClass ( ) . add ( Spinner . STYLE_CLASS_SPLIT_ARROWS_VERTICAL ) ;
spinner . valueProperty ( ) . addListener ( ( ov , oldValue , newValue ) -> {
// バインド例外吐くから・・・
formatter . setValue ( newValue ) ;
// RT-40257 https://javafx-jira.kenai.com/browse/RT-40257
if ( beh == null ) {
beh = ( SpinnerBehavior ) ( ( SpinnerSkin ) ( spinner . getSkin ( ) ) ) . getBehavior ( ) ;
spinner . focusedProperty ( ) . addListener ( ( obs , b , b1 ) -> {
if ( b && ! b1 ) {
beh . stopSpinning ( ) ;
}
} ) ;
}
// スピナーのスタイルを変更する
if ( newValue <= 30_000 ) {
spinner . getEditor ( ) . setStyle ( "-fx-text-fill: red; -fx-font: italic bold 14pt 'serif'; -fx-alignment: CENTER_RIGHT;" ) ;
} else {
if ( newValue >= 70_000 ) {
spinner . getEditor ( ) . setStyle ( "-fx-text-fill: blue; -fx-font: italic bold 14pt 'serif'; -fx-alignment: CENTER_RIGHT;" ) ;
} else {
spinner . getEditor ( ) . setStyle ( "-fx-text-fill: black; -fx-font: 14pt 'serif'; -fx-alignment: CENTER_RIGHT;" ) ;
}
}
if ( newValue <= 30_000 && poor . getValue ( ) ) {
poor . setValue ( Boolean . FALSE ) ;
showPoorDialog ( ) ;
}
if ( newValue >= 70_000 && rich . getValue ( ) ) {
rich . setValue ( Boolean . FALSE ) ;
showRichDialog ( ) ;
}
} ) ;
}
プログラムを起動します。
右寄りでフォントスタイルも設定通りになってます。
Formatted Text もちゃんと機能しています。
日本円のシンボルマークの \ も表示され、三桁ごとに , も表示されています。
Spinner の下向きの矢印ボタンを押して値を減らします。
30,000円以下になると Spinner の文字が赤色になり、イタリックで太文字に変更されます。
円のシンボルマークはイタリック、太字にはならない仕様のようですね。
Dialog も出すようにしているのは今まで通りです。
最小値( 0 )はこのように表示されます。
上向きの矢印ボタンを押して Spinner の値を増やしていきます。
70,000円以上になると Spinner の文字が青色になり、イタリックで太文字に変更されます。
Dialog も出すようにしているのは今まで通りです。
最大値までしか増えません。
今回は Spinner の設定を編集可能としているので 500 を入力してみました。
ちゃんとテキストフォーマットが効いてます。
上向きの矢印ボタンを押して値を増加させてみます。
編集した値に追加設定値が足されていくようです。
実に良く出来ています。
しかし、ちょっと挙動がおかしなところがあります。
例えば、9999999999 と入力したら最大値を表示する場合と
Exception in thread “JavaFX Application Thread” java.lang.NumberFormatException: For input string: “9999999999”
という例外を吐く場合があります。
実際に数字以外を入力させても上記例外が発生します。
ここらへんの対応のセオリーとかそのうち出てくるだろうから今は気にしないでおこう。(ヲヒ
JavaFX 面白いからもっともっと情報があるといいのにね!
TAGS: JavaFX |
2015年3月26日8:57 PM |
« 古い記事
新しい記事 »