TextAreaとTextFlow
JavaFXで複数行のテキストを出力する場合、TextAreaもしくはTextFlowを利用します。
TextAreaはなにもしなくともスクロールしてくれるので問題はありませんが、リッチテキストを利用できるTextFlowでは表示範囲より行数が多くなったときにあふれた部分が表示されなくなります。
対策としてTextFlowをScrollPaneの子供としたときのポイントをまとめました。
TextArea、TextFlowのみ、ScrollPane+TextFlowの横並びサンプル
以下のFXMLで3つの表示がどうなるか確認します。
mainWindow.fxml
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.Button?> <?import javafx.scene.control.ScrollPane?> <?import javafx.scene.control.SplitPane?> <?import javafx.scene.control.TextArea?> <?import javafx.scene.layout.AnchorPane?> <?import javafx.scene.layout.HBox?> <?import javafx.scene.text.TextFlow?> <SplitPane fx:id="splitpane" dividerPositions="0.3" minHeight="-Infinity" minWidth="-Infinity" orientation="VERTICAL" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="flowTest.MainController"> <items> <AnchorPane maxHeight="116.0" minHeight="116.0" minWidth="0.0" prefHeight="116.0" prefWidth="160.0"> <children> <Button fx:id="button_Start" layoutX="405.0" layoutY="64.0" mnemonicParsing="false" onAction="#onStartButtonClicked" prefHeight="26.0" prefWidth="55.0" text="Start" /> <Button fx:id="button_Stop" layoutX="477.0" layoutY="64.0" mnemonicParsing="false" onAction="#onStopButtonClicked" prefHeight="26.0" prefWidth="55.0" text="Stop" /> </children> </AnchorPane> <HBox fx:id="hbox" prefHeight="100.0" prefWidth="200.0"> <children> <TextArea fx:id="textarea" prefHeight="200.0" prefWidth="200.0" wrapText="true" HBox.hgrow="ALWAYS" /> <TextFlow fx:id="textflowWO" minHeight="0.0" prefWidth="200.0" HBox.hgrow="ALWAYS" /> <ScrollPane fx:id="scrollpane" fitToWidth="true" prefHeight="276.0" prefWidth="206.0" vbarPolicy="ALWAYS" HBox.hgrow="ALWAYS"> <content> <TextFlow fx:id="textflow" prefHeight="270.0" prefWidth="174.0" /> </content> </ScrollPane> </children> </HBox> </items> </SplitPane>
FXMLポイント:
- TextFlowのみ(真中)に
minHeight
を設定しておかないと、行数あふれ時にTextFlowが成長して他のコンテナに影響します。 - 逆に
minHeight
とmaxHeight
の両方を設定すると成長はしませんが、ScrollPane併用時にスクロールがきかなくなります。
とくになにもせず実行
3列とも同じ文字列をながします。 右列はスクロールしていますが、先頭行から表示されているため最新行が表示されていません。 データの流れをリアルに見たいので最新行を常に表示させるようにしたいところです。
Mastering JavaFX 10: Build advanced and visually stunning Java applications (English Edition)
posted with amazlet at 19.09.04
Packt Publishing (2018-05-31)
リスナーを追加
表示を常に最下部にするためにはTextFlowの変化を監視するリスナーを作成し、発見時はScrollPaneを最下部にするアクションをinitializeメソッドに追加します。
textflow.getChildren().addListener((ListChangeListener<Node>)((change) -> scrollpane.setVvalue(1.0f)));
FXMLとの関連でこれだけでは最下部に移動しない場合は再描画を追加して
textflow.getChildren().addListener((ListChangeListener<Node>)((change) -> {
scrollpane.layout();
scrollpane.setVvalue(1.0f)));
}
とすればとどめを刺せるかもしれません。
サンプルソース全文
ScrollTest.java
package flowTest; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class ScrollTest extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) throws Exception{ FXMLLoader loader = new FXMLLoader(getClass().getResource("mainWindow.fxml")); Parent root = loader.load(); Scene scene = new Scene(root); scene.getStylesheets().add(getClass().getResource("window.css").toExternalForm()); primaryStage.setTitle("Scroll Test"); primaryStage.setScene(scene); primaryStage.show(); } }
MainController.java
package flowTest; import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ScrollPane; import javafx.scene.control.SplitPane; import javafx.scene.control.TextArea; import javafx.scene.layout.HBox; import javafx.scene.text.TextFlow; import javafx.event.ActionEvent; public class MainController { private ExecuteThread executeThread; @FXML private SplitPane splitpane; @FXML private HBox hbox; @FXML private TextArea textarea; @FXML private ScrollPane scrollpane; @FXML private TextFlow textflow; @FXML private TextFlow textflowWO; @FXML private Button button_Start; @FXML private Button button_Stop; @FXML private void initialize() { fxID.textarea = textarea; fxID.textflow = textflow; fxID.textflowWO = textflowWO; // TextFlowがaddされたらScrollPaneを下に移動させる textflow.getChildren().addListener((ListChangeListener<Node>)((change) -> scrollpane.setVvalue(1.0f))); } @FXML private void onStartButtonClicked(ActionEvent event) { System.out.println("start button clicked"); textflow.getChildren().clear(); textarea.clear(); executeThread = new ExecuteThread(); executeThread.restart(); } @FXML private void onStopButtonClicked(ActionEvent event) { System.out.println("stop button clicked"); if(executeThread != null) executeThread.cancel(); } } class fxID { static TextArea textarea; static TextFlow textflow; static TextFlow textflowWO; }
ExecuteThread.java
package flowTest; import javafx.application.Platform; import javafx.concurrent.Service; import javafx.concurrent.Task; import javafx.scene.paint.Color; import javafx.scene.text.Text; public class ExecuteThread extends Service<Boolean> { @Override protected Task<Boolean> createTask() { Task<Boolean> task = new Task<Boolean>() { @Override protected Boolean call() throws Exception { printNumber(); return null; } }; return task; } private void printNumber() { int i = 0; Color color[] = {Color.BLUE, Color.GREEN, Color.ORANGE, Color.RED}; while(true) { try{ Thread.sleep(1000); } catch (InterruptedException e) { break; } String currentStr = String.format("%04d\n", i++); Text text1 = new Text(currentStr); text1.setFill(color[i % 4]); Text text2 = new Text(currentStr); text2.setFill(color[i % 4]); Platform.runLater(() -> { fxID.textarea.appendText(currentStr); fxID.textflow.getChildren().add(text1); fxID.textflowWO.getChildren().add(text2); }); } } }