【问题标题】:JavaFX: Accessing FXML elements from a second classJavaFX:从第二类访问 FXML 元素
【发布时间】:2017-04-07 17:21:22
【问题描述】:

我正在编写一个尝试执行以下操作的程序:

  1. 获取两个文本文件并将其内容写入两个单独的 TextAreas
  2. 使用多线程,特别是 Runnable 接口同时写入这些单独的区域

我创建了一个“MyRunnable”类:

public class MyRunnable implements Runnable {

    @FXML
    private TextArea textField;

    public MyRunnable() throws IOException {
    }

    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()) {
            textField.appendText("Hello");
            textField.appendText("\n");
        }
    }
}

我的控制器类有一个方法

public void Main() throws IOException {
    Runnable r = new MyRunnable();
    Thread t = new Thread(r);
    t.start();
}

IntelliJ 告诉我从未分配过 textField,当我运行我的主程序并单击调用 Main() 的按钮时,我在下一行得到一个空指针异常。

textField.appendText("Hello");

我如何完成我想要完成的事情?

【问题讨论】:

  • 啊,这很简单,修复。我不敢相信我没有想到这一点。

标签: java multithreading intellij-idea javafx fxml


【解决方案1】:

您能否将 textField 移动到您的控制器类并将其作为参数传递给您的可运行对象?

【讨论】:

  • 他/她可以这样做,但这是一个非常糟糕的设计。在控制器之外公开 UI 元素会使应用程序的可维护性大大降低(假设您决定将文本区域替换为例如列表视图)。
  • @James_D 我在想runnable 将是一个私有的内部或匿名类。现在我想起来了,内部类已经能够看到textField 字段。所以它甚至不需要成为 runnable 的参数。
  • 是的:在这种情况下,您不会在设计方面违反任何规定。 (当然,如果它是一个非静态私有内部类,它无论如何都可以访问封闭类中定义的文本区域。)
  • 是的,我也有同样的想法。 :-)
  • 我开始写一个例子,但开始怀疑最初的前提是否会被误导。听起来 OP 想要从后台线程同时更新文本字段,这是不可能的。最好的办法是将文件加载到后台的缓冲区中,然后使用 Platform.runLater() 对 UI 更新进行排队。
【解决方案2】:

您的代码存在几个问题。首先是你基本观察到的:

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&lt;String&gt; 作为缓冲区,并在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();
}

【讨论】:

  • 什么是消费者 textProcessor;?
  • @user2778506 这是我定义的字段,类型为Consumer。这是“消耗”字符串的东西。我弄错了方法名称,虽然它应该是 accept(...): 现在已修复。
猜你喜欢
  • 1970-01-01
  • 2017-12-02
  • 2015-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-22
  • 1970-01-01
  • 2012-12-15
相关资源
最近更新 更多