SwingNode と TextFlow
このエントリーは、JavaFX Advent Calendar 2014, 11日目のおまけです。
昨日は @toruwest さんの「JavaFXのTreeViewでアニメーションしてみる」でした。
明日は @skrb さんの「Java Advent Calendarと一緒になにか書きます」です。
今年も JavaFX でいろいろなことを楽しんだのでそのうちの一部を紹介します。
はじめに、Swing では JTextPane ってのがあって普通の JTextArea よりも柔軟に扱えます。
テキストのスタイルを簡単に変更できたりしてとても便利でした。
JavaFX に JTextPane に相当するものがあるかと言えば無さそうです。
編集できなくて表示だけなら TextFlow がかなり強力に使えます。
私は TextFlow を使うのを躊躇ってなんとか JTextPane を JavaFX で使ってやろうと SwingNode に手を出してしまいました。
何をしたかったかというとテキスト検索で検索文字にヒットしたらその文字を良く解るように色づけ、太字表示をしたかっただけです。
SwingNode は javafx.embed.swing.SwingNode クラスでこれを使うには
public void setContent(JComponent content)
で Swing コンポーネントをセットします。
ただし、JavaFX アプリケーションスレッドを使わずに
public static void invokeLater(Runnable doRun) メソッドを使用して
doRun.run()を、AWTイベント・ディスパッチ・スレッドで非同期的に実行させます。
このメソッドは実際は java.awt.EventQueue.invokeLater()を呼び出しているだけです。
Swing コンポーネントの操作をするときには面倒だけど AWT のスレッドでってことのようです。
はっきり言ってよっぽどの事情がないかぎり SwingNode なんて使おうと思わない。(^_^;)
いつも通り雑なサンプルプログラムを載せておきます。
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 |
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.text.*?> <?import javafx.geometry.*?> <?import javafx.embed.swing.*?> <?import java.lang.*?> <?import java.util.*?> <?import javafx.scene.*?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="330.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="jp.yucchi.swingnodeexample.FXMLDocumentController"> <children> <VBox prefHeight="330.0" prefWidth="400.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> <children> <TextArea fx:id="textArea" prefHeight="125.0" prefWidth="400.0" wrapText="true"> <font> <Font size="16.0" /> </font> <VBox.margin> <Insets left="10.0" right="10.0" top="10.0" /> </VBox.margin> </TextArea> <HBox prefHeight="50.0" prefWidth="400.0"> <children> <TextField fx:id="textField" prefHeight="40.0" prefWidth="190.0"> <HBox.margin> <Insets bottom="5.0" right="5.0" top="5.0" /> </HBox.margin> <padding> <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" /> </padding> <font> <Font name="Monospaced Regular" size="16.0" /> </font> </TextField> <ToggleButton fx:id="search" mnemonicParsing="false" onAction="#handleSearch" prefHeight="40.0" prefWidth="190.0" text="Search"> <HBox.margin> <Insets bottom="5.0" left="5.0" top="5.0" /> </HBox.margin> <font> <Font size="16.0" /> </font> </ToggleButton> </children> <padding> <Insets left="10.0" right="10.0" /> </padding> </HBox> <SwingNode id="swingNode" fx:id="swingNode"> <VBox.margin> <Insets left="10.0" right="10.0" /> </VBox.margin> </SwingNode> </children> </VBox> </children> </AnchorPane> |
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 |
package jp.yucchi.swingnodeexample; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; /** * * @author Yucchi */ public class SwingNodeExample extends Application { @Override public void start(Stage stage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml")); Scene scene = new Scene(root); stage.setTitle("SwingNode Example"); stage.setScene(scene); stage.setResizable(false); stage.show(); } /** * @param args the command line arguments */ public static void main(String[] args) { launch(args); } } |
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 |
package jp.yucchi.swingnodeexample; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.net.URL; import java.util.ResourceBundle; import java.util.regex.Matcher; import java.util.regex.Pattern; import javafx.beans.binding.Bindings; import javafx.embed.swing.SwingNode; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javax.swing.JScrollPane; import javax.swing.JTextPane; import javax.swing.SwingUtilities; import javax.swing.text.DefaultEditorKit; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; /** * * @author Yucchi */ public class FXMLDocumentController implements Initializable { @FXML TextArea textArea; @FXML TextField textField; @FXML ToggleButton search; @FXML SwingNode swingNode; private JTextPane textPane; @FXML public void handleSearch(ActionEvent event) { if (search.isSelected()) { search.setText("Clear"); makeSearchText(); } else { search.setText("Search"); textArea.setText(""); textField.setText(""); SwingUtilities.invokeLater(() -> { textPane.setText(null); }); } } @Override public void initialize(URL url, ResourceBundle rb) { SwingUtilities.invokeLater(() -> { textPane = new JTextPane(); textPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 16)); textPane.setPreferredSize(new Dimension(390, 145)); JScrollPane scroll = new JScrollPane(textPane); scroll.setWheelScrollingEnabled(true); swingNode.setContent(scroll); }); search.disableProperty() .bind( Bindings.when(textArea.textProperty().isEmpty().or(textField.textProperty().isEmpty())) .then(true) .otherwise(false)); } private void makeSearchText() { SwingUtilities.invokeLater(() -> { textPane.setText(textArea.getText()); final SimpleAttributeSet set = new SimpleAttributeSet(); StyleConstants.setBold(set, true); StyleConstants.setBackground(set, Color.WHITE); StyleConstants.setForeground(set, Color.RED); final DefaultStyledDocument document = (DefaultStyledDocument) textPane.getDocument(); document.putProperty(DefaultEditorKit.EndOfLineStringProperty, "\n"); final Pattern thePattern = Pattern.compile(textField.getText()); final Matcher matcher = thePattern.matcher(textArea.getText()); while (matcher.find()) { int start = matcher.start(); int length = matcher.end() - start; document.setCharacterAttributes(start, length, set, false); } }); } } |
プログラムの実行結果は次のようになります。
いちおう動くけど見栄えが良いものではありませんね。
ちなみに私は疑り深い人間なので試しに AWT のスレッドを使わなければ本当に駄目?なのか試してみました。
コンパイルは通りました。
実行時エラーでも出るのかなと思いきや何も起こりませんでした。
このような小さなプログラムでははっきりした不具合が出ないようです。
裏を返せば非常に危険だということになるのかな?
もう一つ気がかりなことは時々プログラムの起動時に黒くなってしまうことがあります。
やっぱり何か無理があるのかもしれない。(単なるバグだと思う。)
そんなこんなでせっかく JavaFX でプログラム組むんだから古い Swing なんて面倒な思いしてまで使いたくないよね!
そこで TextFlow を使ってみることにしました。
TextFlow は JTextPane と違って編集ができません。
ただ表示するだけです。
使い方も至って簡単です。
javafx.scene.text.Text クラスのインスタンスを生成しそれにテキスト及び書式を設定します。
その Text オブジェクトを TextFlow インスタンス生成時のコンストラクタの引数として渡してあげればいいだけです。
もしくは textFlow.getChildren().add(text); として TextFlow インスタンスに加えることもできます。
簡単に使えてスタイルの自由度も JtextPane より遙かに高いので使い道はあるかと思います。
Text のスタイルは public final void setStyle(String value) を使って設定します。
引数はインライン CSS を与えるだけのお手軽仕様です。
TextFlow を使ったサンプルプログラムも載せておきます。
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 |
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.text.*?> <?import javafx.geometry.*?> <?import javafx.embed.swing.*?> <?import java.lang.*?> <?import java.util.*?> <?import javafx.scene.*?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="330.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="jp.yucchi.textflowexample.FXMLDocumentController"> <children> <VBox prefHeight="330.0" prefWidth="400.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> <children> <TextArea fx:id="textArea" prefHeight="125.0" prefWidth="400.0" wrapText="true"> <font> <Font size="16.0" /> </font> <VBox.margin> <Insets left="10.0" right="10.0" top="10.0" /> </VBox.margin> </TextArea> <HBox prefHeight="50.0" prefWidth="400.0"> <children> <TextField fx:id="textField" prefHeight="40.0" prefWidth="190.0"> <HBox.margin> <Insets bottom="5.0" right="5.0" top="5.0" /> </HBox.margin> <padding> <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" /> </padding> <font> <Font name="Monospaced Regular" size="16.0" /> </font> </TextField> <ToggleButton fx:id="search" mnemonicParsing="false" onAction="#handleSearch" prefHeight="40.0" prefWidth="190.0" text="Search"> <HBox.margin> <Insets bottom="5.0" left="5.0" top="5.0" /> </HBox.margin> <font> <Font size="16.0" /> </font> </ToggleButton> </children> <padding> <Insets left="10.0" right="10.0" /> </padding> </HBox> <ScrollPane prefHeight="145.0" prefWidth="380.0"> <content> <TextFlow fx:id="textFlow" prefHeight="140.0" prefWidth="380.0" style="-fx-background-color: white;" /> </content> <VBox.margin> <Insets left="10.0" right="10.0" /> </VBox.margin> </ScrollPane> </children> </VBox> </children> </AnchorPane> |
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 |
package jp.yucchi.textflowexample; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; /** * * @author Yucchi */ public class TextFlowExample extends Application { @Override public void start(Stage stage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml")); Scene scene = new Scene(root); stage.setTitle("TextFlow Example"); stage.setScene(scene); stage.show(); } /** * @param args the command line arguments */ public static void main(String[] args) { launch(args); } } |
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 |
package jp.yucchi.textflowexample; import java.net.URL; import java.util.ResourceBundle; import java.util.regex.Matcher; import java.util.regex.Pattern; import javafx.beans.binding.Bindings; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; /** * * @author Yucchi */ public class FXMLDocumentController implements Initializable { @FXML TextArea textArea; @FXML TextField textField; @FXML ToggleButton search; @FXML TextFlow textFlow; private Text text; @FXML public void handleSearch(ActionEvent event) { if (search.isSelected()) { search.setText("Clear"); makeSearchText(); } else { search.setText("Search"); textArea.setText(""); textField.setText(""); textFlow.getChildren().removeAll(textFlow.getChildren()); } } @Override public void initialize(URL url, ResourceBundle rb) { search.disableProperty() .bind( Bindings.when(textArea.textProperty().isEmpty().or(textField.textProperty().isEmpty())) .then(true) .otherwise(false)); } private void makeSearchText() { int pos = 0; String sourceText = textArea.getText(); final Pattern thePattern = Pattern.compile(textField.getText()); final Matcher matcher = thePattern.matcher(sourceText); int start; int end; while (matcher.find()) { start = matcher.start(); end = matcher.end(); if (start == 0) { this.text = new Text(sourceText.substring(start, end)); this.text.setStyle("-fx-font-size: 24px;-fx-fill: linear-gradient(from 0% 0% to 100% 200%, repeat, aqua 0%, red 50%);-fx-stroke: black;-fx-stroke-width: 1;"); textFlow.getChildren().add(this.text); pos = end; } else { if (pos != start) { this.text = new Text(sourceText.substring(pos, start)); this.text.setStyle("-fx-font-size: 16px;"); textFlow.getChildren().add(this.text); } this.text = new Text(sourceText.substring(start, end)); this.text.setStyle("-fx-font-size: 24px;-fx-fill: linear-gradient(from 0% 0% to 100% 200%, repeat, aqua 0%, red 50%);-fx-stroke: black;-fx-stroke-width: 1;"); textFlow.getChildren().add(this.text); pos = end; } } if (pos < sourceText.length()) { this.text = new Text(sourceText.substring(pos, sourceText.length())); this.text.setStyle("-fx-font-size: 16px;"); textFlow.getChildren().add(this.text); } } } |
このプログラムの実行結果は次のようになります。
やっぱ JavaFX オンリーでつくったプログラムのほうが見栄えがいいですね!
それにしても JavaFX っておもしろいね!
この二つのサンプルプログラムでは JavaFX Night で櫻庭さんが例を挙げていたようなことをこの二つのプログラムでやっています。
TextField と TextArea に文字が入力されてなければトグルボタンを非活性化にし、文字が入力されたら活性化させることを Bind を使って実装してみました。
search.disableProperty()
.bind(
Bindings.when(textArea.textProperty().isEmpty().or(textField.textProperty().isEmpty()))
.then(true)
.otherwise(false));
Java らしくない気がしないでもないですが便利です。
あっ、つまり検索してから TextArea か TextField をどちらかのテキストを空にしてしまったら Clear できないってことになるな。
まっ、いいか。
トグルボタン使っちゃったからなぁ・・・
ただのサンプルなのでそこらへんは見逃してくださいまし!(>_<。)
JavaFX 楽しい!
TAGS: JavaFX | 2014年12月11日1:07 AM | Comment : 0