2

I want to redirect the output in Console to JavaFX TextArea, and I follow a suggestion here: JavaFX: Redirect console output to TextArea that is created in SceneBuilder

I tried to set charset to UTF-8 in PrintStream(), but it does not look so well. Setting the charset to UTF-16 improves it a bit, but it is still illegible.

In Eclipse IDE, the supposed text output in Console turns out fine:

KHA khởi đầu phiên giao dịch sáng nay ở mức 23600 điểm, khối lượng giao dịch trong ngày đạt 765 cổ phiếu, tương đương khoảng 18054000 đồng.

Controller.java

public class Controller {
    @FXML
    private Button button;

    public Button getButton() {
        return button;
    }

    @FXML
    private TextArea textArea;

    public TextArea getTextArea() {
        return textArea;
    }

    private PrintStream printStream;

    public PrintStream getPrintStream() {
        return printStream;
    }

    public void initialize() {
        textArea.setWrapText(true);
        printStream = new PrintStream(new UITextOutput(textArea), true, StandardCharsets.UTF_8);
    } // Encoding set to UTF-8

    public class UITextOutput extends OutputStream {
        private TextArea text;

        public UITextOutput(TextArea text) {
            this.text = text;
        }

        public void appendText(String valueOf) {
            Platform.runLater(() -> text.appendText(valueOf));
        }

        public void write(int b) throws IOException {
            appendText(String.valueOf((char) b));
        }
    }
}

UI.java

public class UI extends Application {
    @Override
    public void start(Stage stage) {
        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("Sample.fxml"));
            Parent root = loader.load();
            Controller control = loader.getController();

            stage.setTitle("Title");
            stage.setScene(new Scene(root));
            stage.show();

            control.getButton().setOnAction(new EventHandler<ActionEvent>() {
                public void handle(ActionEvent event) {
                    try {
                        System.setOut(control.getPrintStream());
                        System.setErr(control.getPrintStream());
                        System.out.println(
                                "KHA khởi đầu phiên giao dịch sáng nay ở mức 23600 điểm, khối lượng giao dịch trong ngày đạt 765 cổ phiếu, tương đương khoảng 18054000 đồng.");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

Sample.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.BorderPane?>


<BorderPane prefHeight="339.0" prefWidth="468.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/11.0.1" fx:controller="main.Controller">
   <center>
      <TextArea fx:id="textArea" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" />
   </center>
   <right>
      <Button fx:id="button" mnemonicParsing="false" onAction="#getButton" text="Button" BorderPane.alignment="CENTER" />
   </right>
</BorderPane>

I'm still new to Java so I'm unfamiliar to how exactly PrintStream or OutputStream works. Please excuse my ignorance.

Every suggestion is appreciated.

  • Well, I tried to add ```textArea.setFont(new Font("TimesRoman", 14)); ``` in initialize() in Controller but I still get the same result. What other fonts should I use? – Ha Trung Nam Hai Dec 22 '20 at 09:11
  • Hmm. Thing is, I'm not that familiar with the intricacies of encoding/decoding of characters. But I feel like the `String.valueOf((char) b)` could be a problem. The `b` is a single "byte" (a value between 0 and 255). Yet some characters in UTF-8 are represented by multiple bytes, if I'm not mistaken. – Slaw Dec 22 '20 at 09:22
  • That code being the problem seems to be supported by the fact calling `setText(...)` directly with your string literal works fine (at least when the source file is compiled with `-encoding UTF-8`). – Slaw Dec 22 '20 at 09:28
  • Is there any alternative to I could use ```setText(...)``` to push the Console output to TextArea? Maybe put the Console output into a String (or something else?) and then setText() it? – Ha Trung Nam Hai Dec 22 '20 at 09:41
  • It's not `setText` that fixes the problem. When I say the code works with `setText` the point is that the issue is not with the `TextArea` but with how you decode the bytes into characters. I added an answer with an example. – Slaw Dec 22 '20 at 20:30
  • _I'm still new to XX so I'm unfamiliar to how exactly YY works_ which defines one of your next TODO bullets: work through a tutorial about YY in XX .. just saying :) – kleopatra Dec 23 '20 at 10:02

3 Answers3

2

I believe your problem is caused by this code:

public void write(int b) throws IOException {
    appendText(String.valueOf((char) b));
}

This is converting each individual byte into a character. In other words, it's assuming each character is represented by a single byte. That's not necessarily true. Some encodings, such as UTF-8, may use multiple bytes to represent a single character. They have to if they want to be able to represent more than 256 characters.

You'll need to properly decode the incoming bytes. Rather than trying to do this yourself it would be better to find a way to use something like BufferedReader. Luckily that's possible with PipedInputStream and PipedOutputStream. For example:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.stage.Stage;

import static java.nio.charset.StandardCharsets.UTF_8;

public class Main extends Application {

  @Override
  public void start(Stage primaryStage) {
    TextArea area = new TextArea();
    area.setWrapText(true);

    redirectStandardOut(area);

    primaryStage.setScene(new Scene(area, 800, 600));
    primaryStage.show();

    System.out.println(
        "KHA khởi đầu phiên giao dịch sáng nay ở mức 23600 điểm, khối lượng giao dịch trong ngày đạt 765 cổ phiếu, tương đương khoảng 18054000 đồng.");
  }

  private void redirectStandardOut(TextArea area) {
    try {
      PipedInputStream in = new PipedInputStream();
      System.setOut(new PrintStream(new PipedOutputStream(in), true, UTF_8));

      Thread thread = new Thread(new StreamReader(in, area));
      thread.setDaemon(true);
      thread.start();
    } catch (IOException ex) {
      throw new UncheckedIOException(ex);
    }
  }

  private static class StreamReader implements Runnable {

    private final StringBuilder buffer = new StringBuilder();
    private boolean notify = true;

    private final BufferedReader reader;
    private final TextArea textArea;

    StreamReader(InputStream input, TextArea textArea) {
      this.reader = new BufferedReader(new InputStreamReader(input, UTF_8));
      this.textArea = textArea;
    }

    @Override
    public void run() {
      try (reader) {
        int charAsInt;
        while ((charAsInt = reader.read()) != -1) {
          synchronized (buffer) {
            buffer.append((char) charAsInt);
            if (notify) {
              notify = false;
              Platform.runLater(this::appendTextToTextArea);
            }
          }
        }
      } catch (IOException ex) {
        throw new UncheckedIOException(ex);
      }
    }

    private void appendTextToTextArea() {
      synchronized (buffer) {
        textArea.appendText(buffer.toString());
        buffer.delete(0, buffer.length());
        notify = true;
      }
    }
  }
}

The use of buffer above is an attempt to avoid flooding the JavaFX Application Thread with tasks.

Some other things you need to take into consideration:

  • Since you're using a string literal, make sure you're both saving the source file with UTF-8 and compiling the code with -encoding UTF-8.
  • Make sure the font you use with the TextArea can represent all the characters you want it to.
  • It's possible you also need to run the application with -Dfile.encoding=UTF-8 but I'm not sure. I did not and it still worked for me.
Slaw
  • 37,820
  • 8
  • 53
  • 80
  • That works wonderfully, thank you so much! – Ha Trung Nam Hai Dec 23 '20 at 06:01
  • Note: \ 1. You must **keeps consuming** from the Pipe -- otherwise the Thread will **hang(/freeze)**. https://stackoverflow.com/questions/40270375/writeto-pipedoutputstream-just-hangs \ 2. Do not put **too many** Text in TextArea -- otherwise it will be **Laggy** (8000 is ok in my case -- I was dynamically Outputting Text base on a Text Listener on another TextArea, and the Lag can go up to 2s, the more you type). \ 3. May use `TeeOutputStream` https://stackoverflow.com/questions/16237546/writing-to-console-and-text-file \ 4. May use `System.setErr()` too. – Nor.Z May 12 '23 at 23:17
  • 1
    @Nor.Z Are these general notes or do they address something specific in my answer? Because: **(1)** I do continuously consume from the input stream. Though it's possible for the thread to die, I suppose. But coding a fully robust solution was beyond the scope of this answer. **(2)** I only used `TextArea` here as that's what the OP used, but you can replace its use with a `ListView`, where every line is displayed in its own cell. That will reduce lag and scale to millions of lines. **(3)** Had not heard of `TeeOutputStream` before, but I like that idea. **(4)** Yes, that's true. – Slaw May 12 '23 at 23:40
  • @Slaw These just general notes. – Nor.Z May 13 '23 at 12:23
0

Try to set your default JVM encoding to UTF-8.

java -Dfile.encoding=UTF-8 -jar YourJarfile.jar

For more details look at this thread: Setting the default Java character encoding

If you don't want to export your file, go into your Eclipse Preferences > General > Workspace and set the Text file encoding to UTF-8 (or the encoding you'd like to have).

There are a few more details: How to change default text file encoding in Eclipse

Nickitiki
  • 16
  • 3
  • Thanks for the suggestion. Excuse me though, that means I have to package my project into an executable .jar file first, doesn't it? And when the jar is run on another computer, do I have to set the JVM again? – Ha Trung Nam Hai Dec 22 '20 at 09:09
  • If the default encoding on other platforms differs from your preferred, then you have to set the JVM arguments before execution. – Nickitiki Dec 22 '20 at 10:32
  • Well, setting Preferences in Eclipse IDE only affects the code editor and the Console, not the output to the TextArea however. – Ha Trung Nam Hai Dec 22 '20 at 12:09
0

To add a bit more from _ the above answer using PipedInputStream _

Here is how you can use with a TeeOutputStream to output to both the Console & TextArea::

  • vv

    • (the code should work, at least in my case)

    • (I dont think too many synchronized is needed, since the JavaFx Thread is a Single Thread?)

  • Update (misc)

    • //debug <strike> sysout_ori.println(str); this is removed

    • The code before relies on only 1 Thread TR -- the Thread that is Reading from the BufferReader
      -- to do the TextArea update
      -> this is problematic, cuz TR can block on reader.read()
      & the batch of bytes remained in StringBuffer will not be updated to TextArea until next .read()
      to fix that, a new Thread Executors.newSingleThreadScheduledExecutor() is introduced & thats why synchronized (sb) { is required now.

  // ############

  private static final int max_TextAllowed_InTextArea_ToAvoidLag = 8000;
  private static final int amount_TextPreserve_InTextArea_WhenMaxTextReached = 3000;

  private static void setup_OutputStream(TextArea textArea_info) {
    PipedInputStream pipe_in = new PipedInputStream();
    PipedOutputStream pipe_out = null;
    try {
      pipe_out = new PipedOutputStream(pipe_in);
    } catch (IOException e) {
      e.printStackTrace();
    }
    final PrintStream sysout_ori = System.out;
    TeeOutputStream teeOutputStream = new TeeOutputStream(sysout_ori, pipe_out);
    System.setOut(new PrintStream(teeOutputStream, true, StandardCharsets.UTF_8));
    System.setErr(new PrintStream(teeOutputStream, true, StandardCharsets.UTF_8));

    Thread thread_OutputStreamRedirectToJavafx = new Thread()
      {
        private final StringBuffer sb = new StringBuffer();

        private final BufferedReader reader = new BufferedReader(new InputStreamReader(pipe_in, StandardCharsets.UTF_8));

        private final ScheduledExecutorService executor_ExecIdleBatch = Executors.newSingleThreadScheduledExecutor();

        {
          executor_ExecIdleBatch.scheduleWithFixedDelay(
                                                        () -> {
                                                          if (sb.length() != 0) {
                                                            final String str;
                                                            synchronized (sb) {
                                                              str = sb.toString();
                                                              sb.setLength(0);
                                                            }
                                                            Platform.runLater(() -> {
                                                              String content_existing = textArea_info.getText();
                                                              int length = content_existing.length();
                                                              if (length > max_TextAllowed_InTextArea_ToAvoidLag) {
                                                                textArea_info.setText((content_existing + str).substring(length - amount_TextPreserve_InTextArea_WhenMaxTextReached));
                                                              }
                                                              else {
                                                                textArea_info.appendText(str);
                                                              }
                                                            });
                                                          }

                                                        }, 0, 200, TimeUnit.MILLISECONDS);
        }

        @Override
        public void run() {
          int b;
          while (true) {
            try {
              b = reader.read();
            } catch (IOException e) {
              throw new Error(e);
            }
            if (b == -1) {
              executor_ExecIdleBatch.shutdown();
              break;
            }
            else {
              synchronized (sb) {
                sb.append((char) b);
              }
            }
          }
        }

      };
    thread_OutputStreamRedirectToJavafx.start();

  }


Update (misc) [Pipe broken]

  • An error java.io.IOException: Pipe broken / java.io.IOException: Write end dead happened in my case.

    Due to the use of
    1. event.consume(); // consume the WindowEvent.WINDOW_CLOSE_REQUEST to prevent close (so that have time to properly handle the shutdown first) +
    2. stage.close() // close the stage when shutdown handle is completed
    inside window.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, (event) -> { for Shutdown handling

    it was fixed by not using stage.close(), but re-dispatching the consumed event by window.fireEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSE_REQUEST));

    Idk why, but guessing: the Javafx Thread is writing to the System.out -> the PipedOutputStream is linked to the Javafx Thread? --> the Javafx Thread is dead due to stage.close()? --> Write end dead + Pipe broken?
Nor.Z
  • 555
  • 1
  • 5
  • 13