【问题标题】:how to redirect system input to javaFX textfield?如何将系统输入重定向到 javaFX 文本字段?
【发布时间】:2022-10-16 00:09:26
【问题描述】:

找到解决方案

此代码适用于 fxml.files 和我使用的其余代码来自 Slaws 的回答。

主.java:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    final static ExecutorService threadPool = Executors.newFixedThreadPool(3, r -> {
        var t = new Thread(r);
        t.setDaemon(true);
        return t;
    });

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

    @Override
    public void start(Stage stage) throws Exception {

        Parent root = FXMLLoader.load(getClass().getResource("mainScreen.fxml"));
        String css = this.getClass().getResource("style.css").toExternalForm();
        Scene scene = new Scene(root);
        scene.getStylesheets().add(css);
        stage.setMinHeight(800);
        stage.setMinWidth(1200);
        stage.setScene(scene);
        stage.show();
        
    }

    @Override
    public void stop() {
        threadPool.shutdownNow();
    }
}

控制器.java

import java.io.IOException;
import javafx.fxml.FXML;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.text.Font;
import javafx.application.Platform;

public class Controller {
    
    @FXML
    private TextArea console;
    @FXML
    private TextField inputPane;
    
    public void initialize() throws IOException{

        wireInputAndOutput(inputPane, console);
        startConsoleTask();

    }

    static void wireInputAndOutput(TextField input, TextArea output) throws IOException {
        var inputConsumer = StreamWriter.redirectStandardIn(Main.threadPool);
        StreamReader.redirectStandardOut(new BufferedTextAreaAppender(output), Main.threadPool);

        input.setOnAction(e -> {
            e.consume();
            var text = input.textProperty().getValueSafe() + "\n";
            output.appendText(text);
            inputConsumer.accept(text);
            input.clear();
        });
    }
    private void startConsoleTask() {
        Main.threadPool.execute(new ConsoleTask(Platform::exit));
    }
}


编辑 2

现在的问题是当我将 ConsoleTask 放在 controller.java 中时它没有运行(这是问题 atm。)。 当我复制/粘贴 Slaw 的 main.java 时它起作用了,但我想使用 fxml 文件,所以我尝试了这个,但它不再起作用了。

测试.fxml:

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>


<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0" prefWidth="800.0" style="-fx-background-color: white;" xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="Controller">
   <children>
      <TextArea fx:id="console" editable="false" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
         <VBox.margin>
            <Insets bottom="20.0" />
         </VBox.margin>
      </TextArea>
      <TextField fx:id="inputPane" prefHeight="40.0" VBox.vgrow="ALWAYS" />
   </children>
   <padding>
      <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
   </padding>
</VBox>

ConsoleTask.java:

import java.util.NoSuchElementException;
import java.util.Scanner;

class ConsoleTask implements Runnable {

    private final static Scanner scanner = new Scanner(System.in);
    private final Runnable onExit;

    ConsoleTask(Runnable onExit) {
        this.onExit = onExit;
    }

    @Override
    public void run() {
        System.out.println("WELCOME TO CONSOLE APP!");

        boolean running = true;
        while (running && !Thread.interrupted()) {
            printOptions();

            int choice = readInt("Choose option: ", 1, 20);
            switch (choice) {
                case 1 -> doCheckIfLeapYear();
                case 2 -> doCheckIfPrime();
                case 3 -> doPrintStringBackwards();
                case 20 -> running = false;
                default -> System.out.println("Unknown option!");
            }
            
            System.out.println("\nPlease wait...");
            wait(6000);
        }
        
        onExit.run();
    }

    private void printOptions() {
        System.out.println();
        System.out.println("Options");
        System.out.println("-------");
        System.out.println("  1) Test Leap Year");
        System.out.println("  2) Test Prime");
        System.out.println("  3) Print String backwards");
        System.out.println("  20) Exit");
        System.out.println();
    }

    private int readInt(String prompt, int min, int max) {
        while (true) {
            System.out.print(prompt);

            try {
                int i = Integer.parseInt(scanner.nextLine());
                if (i >= min && i <= max) {
                    return i;
                }
            } catch (NumberFormatException | NoSuchElementException ignored) {
            }
            System.out.printf("Please enter an integer between [%,d, %,d]%n", min, max);
        }
    }

    private void doCheckIfLeapYear() {
        System.out.println();
        int year = readInt("Enter year: ", 0, 1_000_000);
        if (year % 4 == 0 || (year % 100 == 0 && year % 400 != 0)) {
            System.out.printf("The year %d is a leap year.%n", year);
        } else {
            System.out.printf("The year %d is NOT a leap year.%n", year);
        }
    }

    private void doCheckIfPrime() {
        System.out.println();
        int limit = readInt("Enter an Integer: ", 1, Integer.MAX_VALUE);

        while (limit <= 1){
            System.out.print("Wrong number! the number should be higher or equal to 2: ");
            limit = scanner.nextInt();
        }

        if(isPrime(limit)){
            System.out.println(limit+" is a prime number.");
        }else{
            System.out.println(limit+" is not a prime number.");
        }
    }
    private static boolean isPrime(int n){
        if(n <= 1){
            return false;
        }
        for(int i = 2; i <= Math.sqrt(n); i++){
            if(n % i == 0){
            return false;
            }
        }
        return true;   
    }

    private static void doPrintStringBackwards(){
        System.out.println();
        System.out.print("\nEnter a word or a sentence: ");
        String answer = scanner.nextLine();

        //StackOverflow version: (simple) 
        System.out.println("\n"+new StringBuilder(answer).reverse().toString());
        
        //Lecture version:
        /*for (int i = answer.length()-1;i>=0;i--){
            System.out.print(answer.charAt(i));
        }*/
    }
    private static void wait(int ms){
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ex) {
            Thread.currentThread().interrupt();
        }
    }
}

编辑 1

我想用这个实现的结果是:

使用 gui 中的文本字段作为 java 控制台程序的输入,但我不知道如何实现该结果。

原来的

我是 java/javaFX 的新手,我正在尝试制作一个控制台 gui 应用程序。问题是我找不到将 system.in 重定向到 JavaFX 文本字段的方法。

我已经尝试过这些解决方案,但我没有让它们工作:

thisthis

部分代码图片:

the methods

【问题讨论】:

  • 也许我错过了,但是您将标准输入重定向到TextField 的尝试在哪里?那次尝试出了什么问题?请提供minimal reproducible example
  • @Slaw 它在“问题开始的地方”是什么,但这是我最近的尝试,因为我试图将输入直接发送到方法而不是重定向它,你可能会猜到它不起作用。我也研究了输入流,但我对如何使用它感到困惑。所以现在我希望我能在这个问题上得到帮助,因为我不知道。
  • 请修剪您的代码,以便更容易找到您的问题。请按照以下指南创建minimal reproducible example
  • 但是在“问题开始”和“问题结束”之间的代码中,cmets 没有做任何类似于将标准输入重定向到文本字段的操作。您所做的是从文本字段中读取文本并将其转换为 int,然后将其传递给方法调用,这都是完全正常的 JavaFX 工作。
  • 但实际上你想在这里做什么?通常,标准输入是用户在键盘上键入控制台/终端的文本流。在这种情况下将标准输入通过管道传输到文本字段是多余的;您也可以让用户在文本字段中输入。如果您将一个程序的输出传送到您的 Java 程序(标准输入的另一个上下文),您可以从标准输入读取值并使用它(文本字段没有明显的需要)。您所说的尝试做的实际用例是什么?

标签: java javafx


【解决方案1】:

前言:鉴于您的“控制台应用程序”不涉及派生任何进程或执行任何“实际”I/O,您可能需要重新设计您的应用程序以直接使用 TextFieldTextArea,或者至少比您更直接地使用目前正在尝试做。使用System.outSystem.in 会增加一层不必要的间接性,并使一切变得更加复杂。此外,重定向标准输出和标准输入会影响整个过程,这可能是不可取的。


大概的概念

从标准输入读取字符并将字符写入标准输出将涉及对这些字符进行解码和编码。您需要使用已经为您实现此功能的类,例如BufferedReaderBufferedWriter。由于您本质上希望通过流在线程之间进行通信,因此您可以让底层流成为PipedInputStreamPipedOutputStream 的实例。

此外,由于大多数基于控制台的应用程序可以打印提示文本而无需在末尾换行,因此您必须通过读取每个单独的字符而不是逐行读取标准输出。这可能涉及对Platform#runLater(Runnable) 的大量调用,可能会压倒FX 线程,这意味着您可能应该缓冲此输出。

下面的示例显示了一个概念验证为了这。它使用 UTF-8 进行字符编码。在这个答案的末尾有一个运行示例代码的 GIF。

以下是课程的简要概述:

Class Description
Main The JavaFX application class. Creates the UI and otherwise initializes and starts the application.
StreamWriter Responsible for writing the user's input so that it can later be read by System.in. Also provides the code for redirecting System.in so that ultimately the input can come from a TextField.
StreamReader Responsible for reading the output from System.out character-by-character. Also provides the code for redirecting System.out so that ultimately it can be appended to a TextArea.
BufferedTextAreaAppender Responsible for taking the stream of characters from StreamReader and appending them to the TextArea in batches.
ConsoleTask The "console application". Uses System.in and System.out to interact with the user.

例子

请注意,我将TextArea 设置为在文本更改时自动滚动到末尾。不幸的是,这似乎在文本第一次“离开屏幕”时不起作用。但在那之后第一次它似乎工作。

主.java

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;

public class Main extends Application {

    private final ExecutorService threadPool = Executors.newFixedThreadPool(3, r -> {
        var t = new Thread(r);
        t.setDaemon(true);
        return t;
    });

    @Override
    public void start(Stage primaryStage) throws Exception {
        var textField = new TextField();
        textField.setFont(Font.font("Monospaced", 13));

        var textArea = new TextArea();
        textArea.setFont(textField.getFont());
        textArea.setEditable(false);
        textArea.setFocusTraversable(false);
        textArea.textProperty().addListener(obs -> textArea.end());

        var root = new VBox(10, textArea, textField);
        root.setPadding(new Insets(10));
        VBox.setVgrow(textArea, Priority.ALWAYS);

        primaryStage.setScene(new Scene(root, 600, 400));
        textField.requestFocus();

        primaryStage.setTitle("Console App");
        primaryStage.show();

        wireInputAndOutput(textField, textArea);
        startConsoleTask();
    }

    private void wireInputAndOutput(TextField input, TextArea output) throws IOException {
        var inputConsumer = StreamWriter.redirectStandardIn(threadPool);
        StreamReader.redirectStandardOut(new BufferedTextAreaAppender(output), threadPool);

        input.setOnAction(e -> {
            e.consume();
            var text = input.textProperty().getValueSafe() + "
";
            output.appendText(text);
            inputConsumer.accept(text);
            input.clear();
        });
    }

    private void startConsoleTask() {
        threadPool.execute(new ConsoleTask(Platform::exit));
    }

    @Override
    public void stop() {
        threadPool.shutdownNow();
    }
}

BufferedTextAreaAppender.java

import java.util.function.IntConsumer;
import javafx.application.Platform;
import javafx.scene.control.TextArea;

class BufferedTextAreaAppender implements IntConsumer {

    private final StringBuilder buffer = new StringBuilder();
    private final TextArea textArea;
    private boolean runLater = true;

    BufferedTextAreaAppender(TextArea textArea) {
        this.textArea = textArea;
    }

    @Override
    public void accept(int characterCodePoint) {
        synchronized (buffer) {
            buffer.append((char) characterCodePoint);
            if (runLater) {
                runLater = false;
                Platform.runLater(this::appendToTextArea);
            }
        }
    }

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

StreamReader.java(可能可以使用更好的名称)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executor;
import java.util.function.IntConsumer;

class StreamReader implements Runnable {

    static void redirectStandardOut(IntConsumer onNextCharacter, Executor executor) throws IOException {
        var out = new PipedOutputStream();
        System.setOut(new PrintStream(out, true, StandardCharsets.UTF_8));

        var reader = new StreamReader(new PipedInputStream(out), onNextCharacter);
        executor.execute(reader);
    }

    private final BufferedReader reader;
    private final IntConsumer onNextCharacter;

    StreamReader(InputStream stream, IntConsumer onNextCharacter) {
        this.reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
        this.onNextCharacter = onNextCharacter;
    }

    @Override
    public void run() {
        try (reader) {
            int charAsInt;
            while (!Thread.interrupted() && (charAsInt = reader.read()) != -1) {
                onNextCharacter.accept(charAsInt);
            }
        } catch (InterruptedIOException ignore) {
        } catch (IOException ioe) {
            throw new UncheckedIOException(ioe);
        }
    }
}

StreamWriter.java(可能会使用更好的名称)

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

class StreamWriter implements Runnable {

    static Consumer<CharSequence> redirectStandardIn(Executor executor) throws IOException {
        var in = new PipedInputStream();
        System.setIn(in);

        var queue = new ArrayBlockingQueue<CharSequence>(500);
        var writer = new StreamWriter(new PipedOutputStream(in), queue);
        executor.execute(writer);

        return queue::add;
    }

    private final BufferedWriter writer;
    private final BlockingQueue<? extends CharSequence> lineQueue;

    StreamWriter(OutputStream out, BlockingQueue<? extends CharSequence> lineQueue) {
        this.writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
        this.lineQueue = lineQueue;
    }

    @Override
    public void run() {
        try (writer) {
            while (!Thread.interrupted()) {
                writer.append(lineQueue.take());
                writer.flush();
            }
        } catch (InterruptedIOException | InterruptedException ignore) {
        } catch (IOException ioe) {
            throw new UncheckedIOException(ioe);
        }
    }
}

控制台任务.java

import java.nio.charset.StandardCharsets;
import java.util.NoSuchElementException;
import java.util.Scanner;

class ConsoleTask implements Runnable {

    private final Scanner scanner = new Scanner(System.in);
    private final Runnable onExit;

    ConsoleTask(Runnable onExit) {
        this.onExit = onExit;
    }

    @Override
    public void run() {
        System.out.println("WELCOME TO CONSOLE APP!");

        boolean running = true;
        while (running && !Thread.interrupted()) {
            printOptions();

            int choice = readInt("Choose option: ", 1, 3);
            switch (choice) {
                case 1 -> doCheckIfLeapYear();
                case 2 -> doCheckIfPrime();
                case 3 -> running = false;
                default -> System.out.println("Unknown option!");
            }
        }

        onExit.run();
    }

    private void printOptions() {
        System.out.println();
        System.out.println("Options");
        System.out.println("-------");
        System.out.println("  1) Test Leap Year");
        System.out.println("  2) Test Prime");
        System.out.println("  3) Exit");
        System.out.println();
    }

    private int readInt(String prompt, int min, int max) {
        while (true) {
            System.out.print(prompt);

            try {
                int i = Integer.parseInt(scanner.nextLine());
                if (i >= min && i <= max) {
                    return i;
                }
            } catch (NumberFormatException | NoSuchElementException ignored) {
            }
            System.out.printf("Please enter an integer between [%,d, %,d]%n", min, max);
        }
    }

    private void doCheckIfLeapYear() {
        System.out.println();
        int year = readInt("Enter year: ", 0, 1_000_000);
        if (year % 4 == 0 || (year % 100 == 0 && year % 400 != 0)) {
            System.out.printf("The year %d is a leap year.%n", year);
        } else {
            System.out.printf("The year %d is NOT a leap year.%n", year);
        }
    }

    private void doCheckIfPrime() {
        System.out.println();
        int number = readInt("Enter an integer: ", 1, Integer.MAX_VALUE);

        boolean isPrime = true;
        for (int i = 2; i <= (int) Math.sqrt(number); i++) {
            if (number % i == 0) {
                isPrime = false;
                break;
            }
        }

        if (isPrime) {
            System.out.printf("The number %,d is prime.%n", number);
        } else {
            System.out.printf("The number %,d is NOT prime.%n", number);
        }
    }
}

示例 GIF

【讨论】:

  • 为了将它与 controller.java 和 fxml 文件一起使用,我应该改变什么? @斯劳
  • 或者如果有一种方法可以在带有 fxml 文件的 main.java 中使用它,你能举一个例子吗? (没有机会编辑其他评论,对不起,如果这是一个愚蠢的问题,但我不得不问,这样我才能了解这一点)@Slaw
  • 在我的脑海中,基本上只有wireInputAndOutput 方法需要移动到控制器类(假设TextAreaTextField 被注入到控制器中)。然后在initialize 方法中调用它。我建议研究这个例子来尝试真正理解它。如果您了解正在发生的事情,那么根据您的需求进行调整将是相对微不足道的。
  • 检查编辑4,我试着把线输入和输出进入控制器,它没有运行。然后我也试着把启动控制台任务在控制器内部,它没有运行,所以我做错了什么? @斯劳
  • 我假设您收到的是NullPointerException?您的inputPane 字段未使用@FXML 进行注释,因此不会被注入。最重要的是,您提供的 FXML 文件中的 TextField 元素具有 fx:id="inputField"。字段的名称和fx:id 属性的值必须完全匹配。
【解决方案2】:

重构 Task.java 以将逻辑与用户输入/输出分开。我还建议不要将类命名为与 JavaFX API 中的标准类冲突的东西(尽管您显然可以通过导入和显式使用完全限定的类名来解决这个问题。)

就像是:

import java.util.Scanner;

public class LeapYearTask {
    
    private static final Scanner input = new Scanner(System.in);

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

    public static void leapYearDialog(){
        System.out.print("
Write a year: ");
        int year = input.nextInt();
        System.out.println(leapYearDialog(year));
    }

    public static String leapYearDialog(int year) { 
        
        String standardMsg ="This is leapYearDialog method:";


        if (isLeapYear(year)){
            return "
"+standardMsg+"
"+year+" is a leap year.";
        }else{
            return "
"+standardMsg+"
"+year+" is not a leap year.";
        }
    }

    public static boolean isLeapYear(int year) {
        boolean divideByFour = year % 4 ==0;
        boolean divideByHundred = year % 100 ==0;
        boolean divideByFourHundred = year % 400 ==0;
        return divideByFour && (!divideByHundred || divideByFourHundred);
    }
}

(您甚至可能想要进一步重构,将逻辑 isLeapYear() 分离到与具有文本交互的方法不同的类中。)

那么您的 JavaFX 程序只需要与逻辑部分进行交互,这是很自然的。现在不需要破解标准输入和标准输出。

public class Controller {
    
    @FXML
    private TextArea console;
    @FXML
    private TextField inputField;
    

    
    //somehow get input to work with task.leapYearDialog()
    public void runCommand(ActionEvent event) {

        int year = Integer.parseInt(inputField.getText());
        // I'm assuming you were using a background thread because this is a
        // stand-in for a long-running process.
        Task<String> leapYearTask = new Task<>() {
            @Override
            protected String call() throws Exception {
                return LeapYearTask.leapYearDialog(year);
            }
        };
        leapYearTask.setOnSucceeded(e -> console.append(leapYearTask.getValue()));
        new Thread(leapYearTask).start();
    }
}

【讨论】:

  • 好的,那么您将如何执行图片中的“方法”功能?因为这是我想在 gui 中运行的实际任务。 - 如果您需要,我也可以提供该图片中的功能的图片。
  • @UnseenSnick 不要发布文字图片。 (说真的;为什么你认为这是个好主意?)将代码发布为文本,格式正确。
  • 在那里,我将实际任务放在编辑 3 中
  • 这部分没有输出:System.out.print(" 写一年:");到文本区域,有什么原因吗?@James_D
  • @UnseenSnick 是的:没有代码可以在文本区域中显示该文本。你为什么要它在那里呢?
猜你喜欢
  • 2015-04-10
  • 2012-07-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-12
  • 1970-01-01
  • 1970-01-01
  • 2020-03-14
相关资源
最近更新 更多