From parameter files, to raw data, to debuggin, it can be really useful to manually load text files natively in the application you’re trying to develop. That way you can ensure that the information your program is ingesting is the stuff you meant to put in.
It’s also the first step on the way to creating more complex applications like hex editors and development aids. in this article, I’ll go through the step by step process of building that editor.

In JavaFX, text can be loaded and displayed in a TextArea by parsing text using a BufferedReader as a list of strings. This can be trasnferred to a TextArea by concatenating the list and invoking setText(String) on the TextArea.
What you’ll get from this article
The user interface in a text reader needs to have three main components:
- A route through which the user can select a file. This can be drag-and-drop, or through a file chooser, but I’ll just use a file chooser for today.
- A Text area to display the contents of the file.
Additionally, we can upgrade this functionality in two ways:
- Running a background process to determine if the file has been edited outside of the application
- Enabling the user to save the file themselves through either keyboard shortcuts, or menu selection.
- Keeping track of the current file status so that if a user loads in another text file, they are prompted should changes need to be saved.
By the end of it, we will have a basic text editor capable of loading, synching and saving a text document.

That’s OK. There’s a full code example at the end of a working JavaFX Text Editor. You can jump down with this button and read the rest when you have the time.
Jump To Code!Designing the User Interface
To facilitate an interface that can open, edit, sync and save a text file, we’ll need three basic elements to our view.
A TextA
- A simple menu to open, save and close the file
- A TextArea to display the contents of the file
- A status label and progress bar to display loading progress
- A button to let the user load changes if they occur outside of the program

I’ve tried to keep it as simple as possible – really just the bare bones of what you need. You may want to improve and style it yourself if you use the program, but here’s the simple FXML markup to get you started:
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.geometry.*?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <BorderPane xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.edencoding.controllers.SimpleFileEditorController"> <top> <MenuBar> <Menu text="File"> <MenuItem text="Open" /> <MenuItem text="Save" /> <MenuItem text="Close" /> </Menu> </MenuBar> </top> <center> <AnchorPane> <TextArea editable="false" prefHeight="400.0" prefWidth="600.0" text="Load a text file using the menu" AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="5.0" AnchorPane.rightAnchor="5.0" AnchorPane.topAnchor="5.0" /> </AnchorPane> </center> <bottom> <HBox> <padding> <Insets bottom="5.0" left="5.0" right="5.0" /> </padding> <HBox alignment="CENTER_LEFT" HBox.hgrow="ALWAYS"> <Label fx:id="statusMessage" prefWidth="150.0" text="Checking for Changes..." /> <ProgressBar fx:id="progressBar" prefWidth="150.0" progress="0.0" /> </HBox> <HBox alignment="CENTER_RIGHT" HBox.hgrow="ALWAYS"> <Button fx:id="loadChangesButton" mnemonicParsing="false" text="Load Changes" /> </HBox> </HBox> </bottom> </BorderPane>
Note: I’ve exposed the Label, ProgressBar and Button using fx:id
tags, because I’ll want to control when they’re visible based on the application’s current state.
Finally, we’ll create the Controller
, which will define the actual business logic of loading a file into our View, and maintaining it in the programme’s memory. You can see I’ve already linked the FXML file to a Java file using the attribute fx:controller
. You’ll need to link yours up too.
A basic Controller doesn’t actually need any code in it. it should by default look like this:
package com.edencoding.controllers; public class SimpleFileEditorController { public void initialize(){ loadChangesButton.setVisible(false); } }
What I have added is a little code to hide the “load file changes” button, because we won’t want to see that until we’ve detected changes later on. There are actually a lot of ways to hide a button in JavaFX, with different effects on the interface. Here’s setting the visible property will suit just fine.
Next, we’ll add the Java code to control how users open a text file.
How to display a text file in a TextArea
We’ll use JavaFX events to start the sequence of loading a file into the text area in four stages:
- Set an action on the “Open” menu item using the FXML
#
operator - Use that method to let the user choose a file to load.
- Read a text file into memory
- Display the file contents

1. Set an action on the “Open” menu item using the FXML # operator
To set a method on a button action, we need to use the onAction
attribute of the MenuItem
object in FXML.
Inside the FXML file above, modify the “Open” MenuItem
so that it looks like this:
<MenuItem text="Open" onAction="#chooseFile"/>
After that, we’ll need to define the chooseFile()
method in our Controller.
2. Choosing a file to load
The controller should be looking pretty blank at the moment, so we’ll need to add a method to let our user choose a file when prompted. The prototype for this method is as so:
public void chooseFile(ActionEvent event);
Because it originates from a MenuItem
wired in from the FXML file, the method by default takes an ActionEvent
as a parameter, which will be automatically generated and passed to the method by JavaFX when the MenuItem
is clicked.
Don’t worry, we don’t have to do anything with the event except consume it once we’re finished. If you want to find out more about event propagation, and everything about JavaFX events, check out my comprehensive guide. It goes into depth about how to create them and use them effectively.
Right now we need to let our user load the file. Inside the method:
public void chooseFile(ActionEvent event) { FileChooser fileChooser = new FileChooser(); //only allow text files to be selected using chooser fileChooser.getExtensionFilters().add( new FileChooser.ExtensionFilter("Text files (*.txt)", "*.txt") ); //set initial directory somewhere user will recognise fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); //let user select file File fileToLoad = fileChooser.showOpenDialog(null); //if file has been chosen, load it using asynchronous method (define later) if(fileToLoad != null){ loadFileToTextArea(fileToLoad); } }
What we get at the end of the process is a File object that refers to the data on the hard disk – we haven’t loaded anything into the Application memory yet.
Note: because we’re compartmentalising our code nicely, once we’ve successfully selected a file, we can delegate the next action to another method – loadFileToTextArea(File file)
, which will do the heavy lifting of loading the file itself.
3. Reading a text file in Java
To read and display the text file, we’ll create a Task
that’s going to:
- Load our file in the background
- Display the contents of the file in the
TextArea
when it’s done.
Then:
- We’ll bind the
Task
to theProgressBar
, so it updates the user on the file load process. In reality, most small file loads should be fast enough that this won’t be visible, but I always think it’s good practice to build in some
I’d also recommend an obvious extension to this would be to check the file size (perhaps in bytes) and load it in chunks, but I won’t complicate the code with that here.
a. Loading our file in the background with UI updates
Loading text into the programme memory is no different in JavaFX than it would be in plain old Java, so we can do this using a BufferedReader to transcribe the text file’s contents into
private Task<String> fileLoaderTask(File fileToLoad){ //Create a task to load the file asynchronously Task<String> loadFileTask = new Task<>() { @Override protected String call() throws Exception { BufferedReader reader = new BufferedReader(new FileReader(fileToLoad)); //Use Files.lines() to calculate total lines - used for progress long lineCount; try (Stream<String> stream = Files.lines(fileToLoad.toPath())) { lineCount = stream.count(); } //Load in all lines one by one into a StringBuilder separated by "\n" - compatible with TextArea String line; StringBuilder totalFile = new StringBuilder(); long linesLoaded = 0; while((line = reader.readLine()) != null) { totalFile.append(line); totalFile.append("\n"); updateProgress(++linesLoaded, lineCount); } return totalFile.toString(); } }; //If successful, update the text area, display a success message and store the loaded file reference loadFileTask.setOnSucceeded(workerStateEvent -> { try { textArea.setText(loadFileTask.get()); statusMessage.setText("File loaded: " + fileToLoad.getName()); loadedFileReference = fileToLoad; } catch (InterruptedException | ExecutionException e) { Logger.getLogger(getClass().getName()).log(SEVERE, null, e); textArea.setText("Could not load file from:\n " + fileToLoad.getAbsolutePath()); } }); //If unsuccessful, set text area with error message and status message to failed loadFileTask.setOnFailed(workerStateEvent -> { textArea.setText("Could not load file from:\n " + fileToLoad.getAbsolutePath()); statusMessage.setText("Failed to load file"); }); return loadFileTask; }
Here, we can use the setOnFailed()
method to display helpful error messages to the user. We can also use the setOnSucceeded()
method to handle pushing the new content into the TextArea
.
4. Bind the Task to our ProgressBar and load the text to the TextArea
Now we have a Task
we can bind its progress to the progress displayed in the ProgressBar
, and set it running to load our file in the background.
We’ll do all of this in the loadFileToTextArea()
method we referenced above.
private void loadFileToTextArea(File fileToLoad) { Task<String> loadFileTask = fileLoaderTask(fileToLoad); progressBar.progressProperty().bind(loadFileTask.progressProperty()); loadFileTask.run(); }
How to update the display if the text file changes
There are a few ways to keep track of whether a text file has changed, but by far the easiest is to load the file attributes and check the date and time the file was last modified. To do this in the background we’ll add code in three stages:
- Saving the file details and scheduling checking to detect when these change
- Using JavaFX events in the ScheduledService to notify the user
- Handling whether the user wants to load the changes

1. Saving file details and scheduling checking
We’ll start by saving the time that the file was last modified (lastModifiedTime) as we load the file by adding a line of code to the setOnSucceeded()
method of the Task
we defined above:
loadFileTask.setOnSucceeded(workerStateEvent -> { try { textArea.setText(loadFileTask.get()); statusMessage.setText("File loaded: " + fileToLoad.getName()); loadedFileReference = fileToLoad; lastModifiedTime = Files.readAttributes(fileToLoad.toPath(), BasicFileAttributes.class).lastModifiedTime(); } catch (InterruptedException | ExecutionException | IOException e) { Logger.getLogger(getClass().getName()).log(SEVERE, null, e); textArea.setText("Could not load file from:\n " + fileToLoad.getAbsolutePath()); } });
Obviously make sure you define lastModifiedTime and loadedFileReference as member variables of the controller :).
private File loadedFileReference; private FileTime lastModifiedTime;
Now, we can schedule a process to check the time the file was last modified every second. To make sure we keep the UI responsive during the check-and-load cycle, we can run them through a background process that’s scheduled to run periodically.
Scheduling file checking to detect changes
Here, we’ll dig into the JavaFX concurrency package a little to schedule a Task using the ScheduledService object. This is one of a huge number of ways to schedule a Task in JavaFX.
If you’re interested in learning more about your concurrency and scheduling options, and how you can tune your efficiency against your convenience, check out my guide to JavaFX running background processes.
For now, we’ll create a ScheduledService
, defining a Task
that checks whether the file has been modified since we last loaded it.
private ScheduledService<Boolean> createFileChangesCheckingService(File file){ ScheduledService<Boolean> scheduledService = new ScheduledService<>() { @Override protected Task<Boolean> createTask() { return new Task<>() { @Override protected Boolean call() throws Exception { FileTime lastModifiedAsOfNow = Files.readAttributes(file.toPath(), BasicFileAttributes.class).lastModifiedTime(); return lastModifiedAsOfNow.compareTo(lastModifiedTime) > 0; } }; } }; scheduledService.setPeriod(Duration.seconds(10)); return scheduledService; }
We can define the event that will be fired whenever this ScheduledService
detects a file change by setting it after it’s created. This event will be fired whenever the Task
succeeds.
To get this service running, we’
2. Using JavaFX events to notify the user
Inside the setOnSucceeded()
method above, we used a WorkerStateEvent (an event triggered by the status of our task) to fire a method call to notify our user.
Now, we want to define what happens once we’ve detected a change. For a start, we’ll want to stop checking whether the file’s changed, because it already has. Then, we can notify the user and let them decide whether to load the changes.
private void scheduleFileChecking(File file){ ScheduledService<Boolean> fileChangeCheckingService = createFileChangesCheckingService(file); fileChangeCheckingService.setOnSucceeded(workerStateEvent -> { //first time task runs, this may be null. Avoid NPE. if(fileChangeCheckingService.getLastValue()==null) return; if(fileChangeCheckingService.getLastValue()){ //no need to keep checking fileChangeCheckingService.cancel(); notifyUserOfChanges(); } }); System.out.println("Starting Checking Service..."); fileChangeCheckingService.start(); }
Of course, we need to set that going, and a sensible point to start checking is when we initially load the file:
loadFileTask.setOnSucceeded(workerStateEvent -> { //beneath the try-catch block: scheduleFileChecking(loadedFileReference); });
With the ScheduledService
started, we can now notify the user and await their response!
3. Handling whether the user wants to load the changes
To notify the user of changes to the file, we’ll very simply show the button to he user:
private void notifyUserOfChanges() { loadChangesButton.setVisible(true); }
Now, we need to define the action that will be completed if the user decides to load the changes, which we’ll do in two steps:
- Add an action to the button in FXML using the # operator
- Create a connected method in the Controller to load the changes based on the file reference we’ve already saved for this file.
FXML:
<Button fx:id="loadChangesButton" onAction="#loadChanges" text="Load Changes" />
Controller:
public void loadChanges(ActionEvent event){ loadFileToTextArea(loadedFileReference); loadChangesButton.setVisible(false); }
Now, once the user selects the option to re-load the changes, the loadFileToTextArea()
will take care of loading the file, and restarting the ScheduledService
, which will check for future changes.
How to save TextArea content to a file
Compared to scheduling checks and maintaining a file, saving the contents of our file back into its original location is comparatively simple. Again, the first step is to register an action on the button in the FXML file, and then to create the method in our Controller.
FXML:
<MenuItem text="Save" onAction="#saveFile"/>
Controller:
public void saveFile(ActionEvent event) { try { FileWriter myWriter = new FileWriter(loadedFileReference); myWriter.write(textArea.getText()); myWriter.close(); lastModifiedTime = FileTime.fromMillis(System.currentTimeMillis() + 3000); System.out.println("Successfully wrote to the file."); } catch (IOException e) { Logger.getLogger(getClass().getName()).log(SEVERE, null, e); } }
And that’s it! Note I’m also resetting the lastModifiedTime attribute a few seconds into the future so we don’t accidentally pick up the file change in our scheduler. There’s undoubtedly a more elegant event-based way to do this, but for the short example here, it’s absolutely fine.
Full Code Example
In case you want the full, complete code to play with, here it is. You’ll need to adjust the file references based on how you want to store resources in your project, but here’s how I organised mine.
Project Directory │ pom.xml //I used Maven │ └───src └───main ├───java │ │ module-info.java //I used a modular project │ │ │ └───com │ └───edencoding │ │ SimpleFileEditor.java │ │ │ └───controllers │ SimpleFileEditorController.java │ └───resources │ └───com └───edencoding ├───fxml │ SimpleFileEditor.fxml │ └───img EdenCodingIcon.png
This is the code for the entry point to the application. Modify code here to change the windowing options like icon, window title and window size.
package com.edencoding; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.stage.Stage; public class SimpleFileEditor extends Application { @Override public void start(Stage primaryStage) throws Exception { FXMLLoader loader = new FXMLLoader(getClass().getResource("fxml/SimpleFileEditor.fxml")); Parent root = loader.load(); primaryStage.getIcons().add(new Image(getClass().getResourceAsStream("img/EdenCodingIcon.png"))); primaryStage.setTitle("Simple EdenCoding JavaFX File Editor"); primaryStage.setScene(new Scene(root)); primaryStage.show(); } }
This is the code for the FXML file in the application. Change code here to modify how the view looks.
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.geometry.*?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <BorderPane xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.edencoding.controllers.SimpleFileEditorController"> <top> <MenuBar> <Menu text="File"> <MenuItem text="Open" onAction="#openFile"/> <MenuItem text="Save" onAction="#saveFile"/> <MenuItem text="Close"/> </Menu> </MenuBar> </top> <center> <AnchorPane> <TextArea fx:id="textArea" prefHeight="400.0" prefWidth="600.0" text="Load a text file using the menu" AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="5.0" AnchorPane.rightAnchor="5.0" AnchorPane.topAnchor="5.0"/> </AnchorPane> </center> <bottom> <HBox> <padding> <Insets bottom="5.0" left="5.0" right="5.0"/> </padding> <HBox alignment="CENTER_LEFT" HBox.hgrow="ALWAYS"> <Label fx:id="statusMessage" prefWidth="150.0" text="Checking for Changes..."/> <ProgressBar fx:id="progressBar" prefWidth="150.0" progress="0.0"/> </HBox> <HBox alignment="CENTER_RIGHT" HBox.hgrow="ALWAYS"> <Button fx:id="loadChangesButton" onAction="#loadChanges" text="Load Changes"/> </HBox> </HBox> </bottom> </BorderPane>
This is the code for Controller of the view. Modify code here to change how the application behaves.
package com.edencoding.controllers; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.control.TextArea; import javafx.stage.FileChooser; import javafx.util.Duration; import java.io.*; import java.nio.file.Files; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.concurrent.ExecutionException; import java.util.logging.Logger; import java.util.stream.Stream; import static java.util.logging.Level.SEVERE; public class SimpleFileEditorController { private File loadedFileReference; private FileTime lastModifiedTime; public Label statusMessage; public ProgressBar progressBar; public Button loadChangesButton; public TextArea textArea; public void initialize() { loadChangesButton.setVisible(false); } public void openFile(ActionEvent event) { FileChooser fileChooser = new FileChooser(); //only allow text files to be selected using chooser fileChooser.getExtensionFilters().add( new FileChooser.ExtensionFilter("Text files (*.txt)", "*.txt") ); //set initial directory somewhere user will recognise fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); //let user select file File fileToLoad = fileChooser.showOpenDialog(null); //if file has been chosen, load it using asynchronous method (define later) if (fileToLoad != null) { loadFileToTextArea(fileToLoad); } } private void loadFileToTextArea(File fileToLoad) { Task<String> loadTask = fileLoaderTask(fileToLoad); progressBar.progressProperty().bind(loadTask.progressProperty()); loadTask.run(); } private Task<String> fileLoaderTask(File fileToLoad) { //Create a task to load the file asynchronously Task<String> loadFileTask = new Task<>() { @Override protected String call() throws Exception { BufferedReader reader = new BufferedReader(new FileReader(fileToLoad)); //Use Files.lines() to calculate total lines - used for progress long lineCount; try (Stream<String> stream = Files.lines(fileToLoad.toPath())) { lineCount = stream.count(); } //Load in all lines one by one into a StringBuilder separated by "\n" - compatible with TextArea String line; StringBuilder totalFile = new StringBuilder(); long linesLoaded = 0; while ((line = reader.readLine()) != null) { totalFile.append(line); totalFile.append("\n"); updateProgress(++linesLoaded, lineCount); } return totalFile.toString(); } }; //If successful, update the text area, display a success message and store the loaded file reference loadFileTask.setOnSucceeded(workerStateEvent -> { try { textArea.setText(loadFileTask.get()); statusMessage.setText("File loaded: " + fileToLoad.getName()); loadedFileReference = fileToLoad; lastModifiedTime = Files.readAttributes(fileToLoad.toPath(), BasicFileAttributes.class).lastModifiedTime(); } catch (InterruptedException | ExecutionException | IOException e) { Logger.getLogger(getClass().getName()).log(SEVERE, null, e); textArea.setText("Could not load file from:\n " + fileToLoad.getAbsolutePath()); } scheduleFileChecking(loadedFileReference); }); //If unsuccessful, set text area with error message and status message to failed loadFileTask.setOnFailed(workerStateEvent -> { textArea.setText("Could not load file from:\n " + fileToLoad.getAbsolutePath()); statusMessage.setText("Failed to load file"); }); return loadFileTask; } private void scheduleFileChecking(File file) { ScheduledService<Boolean> fileChangeCheckingService = createFileChangesCheckingService(file); fileChangeCheckingService.setOnSucceeded(workerStateEvent -> { if (fileChangeCheckingService.getLastValue() == null) return; if (fileChangeCheckingService.getLastValue()) { //no need to keep checking fileChangeCheckingService.cancel(); notifyUserOfChanges(); } }); System.out.println("Starting Checking Service..."); fileChangeCheckingService.start(); } private ScheduledService<Boolean> createFileChangesCheckingService(File file) { ScheduledService<Boolean> scheduledService = new ScheduledService<>() { @Override protected Task<Boolean> createTask() { return new Task<>() { @Override protected Boolean call() throws Exception { FileTime lastModifiedAsOfNow = Files.readAttributes(file.toPath(), BasicFileAttributes.class).lastModifiedTime(); return lastModifiedAsOfNow.compareTo(lastModifiedTime) > 0; } }; } }; scheduledService.setPeriod(Duration.seconds(1)); return scheduledService; } private void notifyUserOfChanges() { loadChangesButton.setVisible(true); } public void loadChanges(ActionEvent event) { loadFileToTextArea(loadedFileReference); loadChangesButton.setVisible(false); } public void saveFile(ActionEvent event) { try { FileWriter myWriter = new FileWriter(loadedFileReference); myWriter.write(textArea.getText()); myWriter.close(); lastModifiedTime = FileTime.fromMillis(System.currentTimeMillis() + 3000); System.out.println("Successfully wrote to the file."); } catch (IOException e) { Logger.getLogger(getClass().getName()).log(SEVERE, null, e); } } }
Conclusions
It’s actually surprisingly simple to create a text editor in JavaFX.
In this case, we combined the well-defined methods of loading text files in Java with the powerful event-driven mechanics of JavaFX.
At the end, we have a simple, easy to use text editor that can be used to open, sync and save text files on the hard drive.