简体   繁体   English

JavaFX:从第二类访问FXML元素

[英]JavaFX: Accessing FXML elements from a second class

I am writing a program that is attempting to do the following: 我正在编写一个试图执行以下操作的程序:

  1. Take two text files and write their contents to two separate TextAreas 取两个文本文件并将其内容写入两个单独的TextAreas
  2. Write to these separate areas simultaneously using multithreading, specifically the Runnable interface 使用多线程(特别是Runnable接口)同时写入这些单独的区域

I have created a "MyRunnable" class: 我创建了一个“ 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");
        }
    }
}

My controller class has one method 我的控制器类只有一种方法

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

IntelliJ tells me that textField is never assigned, and when I run my main program, and click the button that calls Main() I get a null pointer exception on the following line. IntelliJ告诉我从来没有分配textField,并且当我运行主程序并单击调用Main()的按钮时,在下一行得到一个空指针异常。

textField.appendText("Hello");

How do I accomplish what I want to accomplish? 如何完成我想完成的工作?

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

There are a couple of issues with your code. 您的代码有两个问题。 First the one you have basically observed: 首先,您已经基本观察到的一个:

Fields annotated @FXML are injected by the FXMLLoader into the controller when the FXMLLoader loads the FXML file. 注释字段@FXML被注入FXMLLoader到当控制器FXMLLoader加载FXML文件。 Obviously, the FXMLLoader has no knowledge of any other objects, so simply annotating a field in an arbitrary object with @FXML will not mean it is initialized. 显然, FXMLLoader不了解任何其他对象,因此仅使用@FXML注释任意对象中的字段并不意味着它已初始化。

Secondly, your runnable's run() method is executed on a background thread. 其次,您的runnable的run()方法在后台线程上执行。 Changes to the UI must happen on the FX Application Thread (see the "Threading" section) . 对UI的更改必须在FX Application线程上进行(请参阅“线程”部分) So your calls to textField.appendText(...) are not guaranteed to behave correctly, even if textField were initialized. 因此,即使已初始化textField ,也不能保证您对textField.appendText(...)的调用行为正确。

Finally, and more generally, your design violates "separation of concerns". 最后,更普遍地说,您的设计违反了“关注点分离”。 Your runnable implementation is really just reading some text from a file. 您可运行的实现实际上只是从文件中读取一些文本。 It should not be concerned with what is happening to that text, and it certainly shouldn't know anything about the UI. 它不应该关心该文本发生了什么,当然也不应该对UI有所了解。 (Put simply, it is always a bad design decision to expose UI elements outside of the controller.) (简单地说,将UI元素暴露在控制器外部始终是一个错误的设计决定。)

The best approach here is to give the runnable implementation a "callback": ie an object that "does something with the string". 这里最好的方法是给可运行的实现一个“回调”:即一个“用字符串做某事”的对象。 You can represent this as a Consumer<String> . 您可以将其表示为Consumer<String> So: 所以:

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());
        }
    }
}

Then, in your controller, you can do: 然后,在您的控制器中,您可以执行以下操作:

@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();
}

Note the use of Platform.runLater(...) , which updates the text area on the FX Application Thread. 注意使用Platform.runLater(...) ,它会更新FX Application线程上的文本区域。

Now, depending on how fast you are able to read the lines of text, this approach might swamp the FX Application Thread with too many updates, making the UI unresponsive. 现在,根据您阅读文本行的速度,此方法可能会使FX Application Thread的更新过多,从而使UI无响应。 (If you are simply reading from a local file, this will certainly be the case.) There are a couple of approaches to fixing this. (如果只是从本地文件读取,肯定会是这种情况。)有两种方法可以解决此问题。 One is simply to read all the data into a list of strings, and then process the entire list when it's been read. 一种是简单地将所有数据读入字符串列表,然后在读取整个列表时对其进行处理。 For this, you can use a Task instead of a plain runnable: 为此,您可以使用Task代替普通的runnable:

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 ;
    }
}

Now in your controller you would use this with: 现在,在您的控制器中,您可以将其用于:

@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();
}

If you really want to update the text area continually as the text is read, then things get a bit more complex. 如果您真的想在阅读文本时不断更新文本区域,那么事情会变得有些复杂。 You need to put the strings into a buffer of some kind from the background thread, and then read them into the text area in a way that won't flood the FX Application Thread. 您需要将字符串放入来自后台线程的某种缓冲区中,然后以不会淹没FX Application Thread的方式将它们读入文本区域。 You can use a BlockingQueue<String> for the buffer, and read back in an AnimationTimer . 您可以将BlockingQueue<String>用于缓冲区,然后在AnimationTimer读回。 The animation timer executes its handle() method once every time a frame is rendered to the screen (so it won't run too often, unlike the Platform.runLater() approach from earlier): the basic strategy is to grab as much as possible from the buffer every time it runs, and update the text area. 动画计时器每当将一帧渲染到屏幕时都执行一次handle()方法(因此,它不会运行得太频繁,这与之前的Platform.runLater()方法不同):基本策略是抓取尽可能多的内容。每次运行时都可以从缓冲区中获取,并更新文本区域。 It's important to stop the animation timer when it's done, which we can do by counting the lines read from the file, and stopping when we have put them all in the text area. 重要的是要在完成动画时停止动画计时器,我们可以通过对从文件中读取的行进行计数来停止动画计时器,并在将它们全部放入文本区域后停止动画计时器。

This looks like this: 看起来像这样:

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);
    }
}

And then in the controller, you can do 然后在控制器中,您可以执行

@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();
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM