David Moll
David Moll

Reputation: 129

How do I update the input of a TextArea in real-time?

I am currently working on a JavaFX-program with SceneBuilder which also utilies the commandline of windows. To show the user that the program is doing something I want it to put the console-output into a textarea. So far it only updates after everything is finished, not in real-time, which is my goal.

Here is the code I have so far, where it enters "tree" as a test. "textareaOutput" is the textarea which shows the output:

try {
            Process p = Runtime.getRuntime().exec("cmd /C tree");
            BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
            String line = null;
            while ((line = in.readLine()) != null) {
                System.out.println(line);
                textareaOutput.appendText(line + "\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

I know of MessageConsole in Swing, but I don't know if something similar exists in JafaFX

Upvotes: 2

Views: 1664

Answers (2)

jewelsea
jewelsea

Reputation: 159341

Custom logging framework based solution

Spawn a thread to execute your command, and then use the answer to the following question:

Which will log the messages from the process launched by your spawned thread back to the UI in a thread-safe manner.

You can use an ExecutorService to assist with thread management if you wish. If you do, remember to shut it down cleanly (usually in the stop() method for your JavaFX app).

JavaFX concurrency utilities based solution

You can also make use of JavaFX concurrency utilities, such as Task, Service and Platform.runLater if you wish. See the "A Task Which Returns Partial Results" or "A Task Which Modifies The Scene Graph" sections of the Task java documentation for more information on this approach.

sample output

Example of using JavaFX concurrency utilities.

import javafx.application.*;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ConsoleLogger extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        TextArea outputArea = new TextArea();
        outputArea.setStyle("-fx-font-family: monospace");
        outputArea.setEditable(false);

        TextField commandField = new TextField();
        commandField.setOnAction(event ->
                executeTask(
                        commandField.getText(),
                        outputArea
                )
        );

        VBox layout = new VBox(
                10,
                new Label("Enter a command and press return to execute it."),
                commandField,
                outputArea
        );
        VBox.setVgrow(outputArea, Priority.ALWAYS);
        layout.setPadding(new Insets(10));
        stage.setScene(new Scene(layout));
        stage.show();
    }

    private void executeTask(String commandString, TextArea outputArea) {
        CommandExecutor commandExecutor = new CommandExecutor(
                commandString,
                outputArea
        );

        new Thread(commandExecutor).start();
    }

    public class CommandExecutor extends Task<Void> {
        private final String commandString;
        private final TextArea outputArea;
        private final ProcessBuilder processBuilder;

        public CommandExecutor(
                String commandString,
                TextArea outputArea
        ) {
            this.commandString = commandString;
            this.outputArea = outputArea;

            processBuilder = new ProcessBuilder(
                    commandString.trim().split("\\s+")
            )
                    .redirectErrorStream(true);

            exceptionProperty().addListener((observable, oldException, newException) -> {
                if (newException != null) {
                    newException.printStackTrace();
                }
            });
        }

        @Override
        protected Void call() throws IOException {
            Process process = processBuilder.start();
            try (
                    BufferedReader reader =
                            new BufferedReader(
                                    new InputStreamReader(
                                            process.getInputStream()
                                    )
                            )
            ) {
                String line;
                while ((line = reader.readLine()) != null) {
                    final String nextLine = line;
                    Platform.runLater(
                            () -> outputArea.appendText(nextLine + "\n")
                    );
                }
            }

            return null;
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Note that this is a naive solution which can flood the JavaFX runnable queue, does not include robust exception handling, has no thread pooling, does not deal with processes which take interactive input I/O, merges output from concurrent executing processes to a single text area, does not limit text area size, etc. In particular, you need to be careful if the spawned process does something like tail a large log file which is constantly being written to at a rapid pace, as the naive solution could flood the JavaFX runnable queue with many calls to Platform.runLater, which wouldn't be good.

For a more efficient solution, the custom logging system linked earlier may be better. However, for some applications, the Task based system logging to a TextArea in this example, or some small modifications to it, may be fine.

Additional Notes

In any case, be careful you don't try to modify the text area or any of its properties directly from another thread (use Platform.runLater to prevent that), otherwise the program may fail due to concurrency issues.

There are quite a few tricks and unexpected (undesirable) things which can happen when using the Java Process API, so if you are not very proficient in it, then google around to see what these are and how they may be addressed if you need a very robust solution.

Upvotes: 2

Shekhar Rai
Shekhar Rai

Reputation: 2058

Here is a simple application, which has the functionality of a real-time command-line binding with a text-area.

  • Enter command on input(tree, time, etc) TextField and hit Enter key
  • The results will be appended in the text-area constantly

Demo

public class CommandLineTextAreaDemo extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        BorderPane root = new BorderPane();
        root.setCenter(getContent());
        primaryStage.setScene(new Scene(root, 200, 200));
        primaryStage.show();
    }

    private BorderPane getContent() {
        TextArea textArea = new TextArea();
        TextField input = new TextField();

        input.setOnAction(event -> executeTask(textArea, input.getText()));

        BorderPane content = new BorderPane();
        content.setTop(input);
        content.setCenter(textArea);
        return content;
    }

    private void executeTask(TextArea textArea, String command) {
        Task<String> commandLineTask = new Task<String>() {
            @Override
            protected String call() throws Exception {
                StringBuilder commandResult = new StringBuilder();
                try {
                    Process p = Runtime.getRuntime().exec(command);
                    BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
                    String line;
                    while ((line = in.readLine()) != null) {
                        commandResult.append(line + "\n");
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return commandResult.toString();
            }
        };

        commandLineTask.setOnSucceeded(event -> textArea.appendText(commandLineTask.getValue()));

        new Thread(commandLineTask).start();
    }
}

If you want to use TextArea independently(without using input TextField), you can do something like this instead.

textArea.setOnKeyReleased(event -> {
    if(event.getCode() == KeyCode.ENTER) {
        String[] lines = textArea.getText().split("\n");
        executeTask(textArea, lines[lines.length - 1]);
    }
});

Upvotes: 1

Related Questions