您的代码存在几个问题。首先是你基本观察到的:
当FXMLLoader 加载FXML 文件时,FXMLLoader 将注释@FXML 的字段注入控制器。显然,FXMLLoader 不知道任何其他对象,因此简单地用@FXML 注释任意对象中的字段并不意味着它已被初始化。
其次,您的 runnable 的 run() 方法在后台线程上执行。更改 UI must happen on the FX Application Thread (see the "Threading" section)。因此,即使 textField 已初始化,您对 textField.appendText(...) 的调用也不能保证正确运行。
最后,更一般地说,您的设计违反了“关注点分离”。您的可运行实现实际上只是从文件中读取一些文本。它不应该关心文本发生了什么,当然也不应该知道关于 UI 的任何事情。 (简而言之,将 UI 元素暴露在控制器之外总是一个糟糕的设计决策。)
这里最好的方法是给可运行的实现一个“回调”:即一个“对字符串做某事”的对象。您可以将其表示为Consumer<String>。所以:
import java.util.Scanner ;
import java.util.function.Consumer ;
import java.io.File ;
import java.io.FileNotFoundException ;
public class MyRunnable implements Runnable {
private Consumer<String> textProcessor;
public MyRunnable(Consumer<String> textProcessor) {
this.textProcessor = textProcessor ;
}
public void run() {
String firstFileName = "test.txt";
File inFile = new File(firstFileName);
Scanner in = null;
try {
in = new Scanner(inFile);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
while (in.hasNextLine()) {
textProcessor.accept(in.nextLine());
}
}
}
然后,在您的控制器中,您可以:
@FXML
private TextArea textField ;
public void Main() throws IOException {
Runnable r = new MyRunnable(s ->
Platform.runLater(() -> textField.appendText(s+"\n")));
Thread t = new Thread(r);
t.start();
}
注意Platform.runLater(...) 的使用,它会更新 FX 应用程序线程上的文本区域。
现在,根据您阅读文本行的速度,这种方法可能会因更新过多而淹没 FX 应用程序线程,从而导致 UI 无响应。 (如果您只是从本地文件中读取,肯定会出现这种情况。)有几种方法可以解决此问题。一种是简单地将所有数据读入一个字符串列表,然后在读取整个列表时对其进行处理。为此,您可以使用 Task 而不是普通的可运行文件:
public class ReadFileTask extends Task<List<String>> {
@Override
protected List<String> call {
List<String> text = new ArrayList<>();
String firstFileName = "test.txt";
File inFile = new File(firstFileName);
Scanner in = null;
try {
in = new Scanner(inFile);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
while (in.hasNextLine()) {
text.add(in.nextLine());
}
return text ;
}
}
现在在您的控制器中,您将使用它:
@FXML
private TextArea textField ;
public void Main() throws IOException {
Task<List<String>> r = new ReadFileTask();
// when task finishes, update text area:
r.setOnSucceeded(e -> {
textArea.appendText(String.join("\n", r.getValue()));
}
Thread t = new Thread(r);
t.start();
}
如果您真的想在阅读文本时不断更新文本区域,那么事情会变得有点复杂。您需要将字符串从后台线程放入某种缓冲区,然后以不会淹没 FX 应用程序线程的方式将它们读入文本区域。您可以使用BlockingQueue<String> 作为缓冲区,并在AnimationTimer 中回读。动画计时器在每帧渲染到屏幕时执行一次它的handle() 方法(因此它不会运行太频繁,与之前的Platform.runLater() 方法不同):基本策略是尽可能多地从每次运行时缓冲区,并更新文本区域。在动画计时器完成后停止它很重要,我们可以通过计算从文件中读取的行数,并在将它们全部放入文本区域时停止。
看起来像这样:
public class BackgroundFileReader extends Runnable {
public static final int UNKNOWN = -1 ;
private final AtomicInteger lineCount = new AtomicInteger(UNKNOWN);
private final BlockingQueue<String> buffer = new ArrayBlockingQueue<>(1024);
@Override
public void run() {
String firstFileName = "test.txt";
File inFile = new File(firstFileName);
Scanner in = null;
try {
in = new Scanner(inFile);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
int count = 0 ;
try {
while (in.hasNextLine()) {
buffer.put(in.nextLine());
count++ ;
}
} catch (InterruptedException exc) {
Thread.currentThread.interrupt();
}
lineCount.set(count);
}
// safe to call from any thread:
public int getTotalLineCount() {
return lineCount.get();
}
public int emptyBufferTo(List<String> target) {
return buffer.drainTo(target);
}
}
然后在控制器中,你可以这样做
@FXML
private TextArea textField ;
public void Main() throws IOException {
ReadFileTask r = new ReadFileTask();
// Read as many lines as possible from the buffer in each
// frame, updating the text area:
AnimationTimer updater = new AnimationTimer() {
private int linesRead = 0 ;
@Override
public void handle(long timestamp) {
List<String> temp = new ArrayList<>();
linesRead = linesRead + r.emptyBufferTo(temp);
if (! temp.isEmpty()) {
textField.appendText(String.join("\n", temp));
}
int totalLines = r.getTotalLineCount() ;
if (totalLines != BackgroundFileReader.UNKNOWN && linesRead >= totalLines) {
stop();
}
}
};
updater.start();
Thread t = new Thread(r);
t.start();
}