简体   繁体   中英

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
  2. Write to these separate areas simultaneously using multithreading, specifically the Runnable interface

I have created a "MyRunnable" class:

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.

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

Secondly, your runnable's run() method is executed on a background thread. Changes to the UI must happen on the FX Application Thread (see the "Threading" section) . So your calls to textField.appendText(...) are not guaranteed to behave correctly, even if textField were initialized.

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. (Put simply, it is always a bad design decision to expose UI elements outside of the controller.)

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

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. (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:

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. You can use a BlockingQueue<String> for the buffer, and read back in an 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. 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();
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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