Search code examples
javajavafxutf-8printstreamjavafx-textarea

UTF-8 encoding for output from Console to JavaFX TextArea


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.


Solution

  • 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.