JavaFX TextFlowのオートスクロール

TextAreaとTextFlow

JavaFXで複数行のテキストを出力する場合、TextAreaもしくはTextFlowを利用します。
TextAreaはなにもしなくともスクロールしてくれるので問題はありませんが、リッチテキストを利用できるTextFlowでは表示範囲より行数が多くなったときにあふれた部分が表示されなくなります。
対策としてTextFlowをScrollPaneの子供としたときのポイントをまとめました。

TextArea、TextFlowのみ、ScrollPane+TextFlowの横並びサンプル

以下のFXMLで3つの表示がどうなるか確認します。
f:id:misatospring:20190710213506j:plain

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が成長して他のコンテナに影響します。
  • 逆にminHeightmaxHeightの両方を設定すると成長はしませんが、ScrollPane併用時にスクロールがきかなくなります。
とくになにもせず実行

3列とも同じ文字列をながします。 f:id:misatospring:20190710213521j:plain 右列はスクロールしていますが、先頭行から表示されているため最新行が表示されていません。 データの流れをリアルに見たいので最新行を常に表示させるようにしたいところです。

リスナーを追加

表示を常に最下部にするためにはTextFlowの変化を監視するリスナーを作成し、発見時はScrollPaneを最下部にするアクションをinitializeメソッドに追加します。 f:id:misatospring:20190710213511j:plain

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);
            });
        }
    }
}