I'm using JavaFX's WebView to parse a website. The site contains a bunch of links - I need to open each of them separately, in a given order, and retrieve one information from each of them.
In order to make sure that WebView has loaded the whole site, I'm listening to changed event of WebEngine and waiting for newState == Worker.State.SUCCEEDED. The problem is that this call is asynchronous. When I'm calling webEngine.load(firstAddress);, the code immediately returns and before this page will have been loaded, my code will call another webEngine.load(secondAddress);, and so on.
I understand why it's done this way (why async is better than sync), but I'm a beginner in Java and I'm not sure what's the best solution to this problem. I somehow understand multithreading and stuff, so I've already tried a semaphore (CountDownLatch class). But the code hangs on await and I'm not sure what I'm doing wrong.
Could someone please show me how it should be done the right way? Maybe some universal pattern how to cope with scenarios like this?
A pseudocode of what I want to achieve:
WebEngine webEngine = new WebEngine();
webEngine.loadPage("http://www.something.com/list-of-cars");
webEngine.waitForThePageToLoad(); // I need an equivalent of this. In the real code, this is done asynchronously as a callback
// ... some HTML parsing or DOM traversing ...
List<String> allCarsOnTheWebsite = webEngine.getDocument()....getChildNodes()...;
// allCarsOnTheWebsite contains URLs to the pages I want to analyze
for (String url : allCarsOnTheWebsite)
{
webEngine.loadPage(url);
webEngine.waitForThePageToLoad(); // same as in line 3
String someDataImInterestedIn = webEngine.getDocument()....getChildNodes()...Value();
System.out.println(url + " : " + someDataImInterestedIn);
}
System.out.println("Done, all cars have been analyzed");
You should use listeners which get invoked when the page is loaded, instead of blocking until it's done.
Something like:
WebEngine webEngine = new WebEngine();
ChangeListener<State> initialListener = new ChangeListener<State>() {
#Override
public void changed(ObservableValue<? extends State> obs, State oldState, State newState) {
if (newState == State.SUCCEEDED) {
webEngine.getLoadWorker().stateProperty().removeListener(this);
List<String> allCarsOnTheWebsite = webEngine.getDocument()... ;
loadPagesConsecutively(allCarsOnTheWebsite, webEngine);
}
}
};
webEngine.getLoadWorker().addListener(initialListener);
webEngine.loadPage("http://www.something.com/list-of-cars");
// ...
private void loadPagesConsecutively(List<String> pages, WebEngine webEngine) {
LinkedList<String> pageStack = new LinkedList<>(pages);
ChangeListener<State> nextPageListener = new ChangeListener<State>() {
#Override
public void changed(ObservableValue<? extends State> obs, State oldState, State newState) {
if (newState == State.SUCCEEDED ) {
// process current page data
// ...
if (pageStack.isEmpty()) {
webEngine.getLoadWorker().stateProperty().removeListener(this);
} else {
// load next page:
webEngine.load(pageStack.pop());
}
}
}
};
webEngine.getLoadWorker().stateProperty().addListener(nextPageListener);
// load first page (assumes pages is not empty):
webEngine.load(pageStack.pop());
}
If you want to run all the tasks concurrently, but process them in the order they were submitted, have a look at the following example:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ProcessTaskResultsSequentially extends Application {
#Override
public void start(Stage primaryStage) {
ListView<String> results = new ListView<>();
List<Task<Integer>> taskList = new ArrayList<>();
for (int i = 1; i<= 10 ; i++) {
taskList.add(new SimpleTask(i));
}
ExecutorService exec = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t ;
});
Thread processThread = new Thread(() -> {
for (Task<Integer> task : taskList) {
try {
int result = task.get();
Platform.runLater(() -> {
results.getItems().add("Result: "+result);
});
} catch (Exception e) {
e.printStackTrace();
}
}
});
processThread.setDaemon(true);
processThread.start();
taskList.forEach(exec::submit);
primaryStage.setScene(new Scene(new BorderPane(results), 250, 400));
primaryStage.show();
}
public static class SimpleTask extends Task<Integer> {
private final int index ;
private final static Random rng = new Random();
public SimpleTask(int index) {
this.index = index ;
}
#Override
public Integer call() throws Exception {
System.out.println("Task "+index+" called");
Thread.sleep(rng.nextInt(1000)+1000);
System.out.println("Task "+index+" finished");
return index ;
}
}
public static void main(String[] args) {
launch(args);
}
}
Related
PROBLEM:
I have following code for the java.awt.Button :
Button btn = new Button("abcde");
btn.addActionListener((ActionEvent e) ->
{
String s = btn.getLabel().toUpperCase();
btn.setLabel(s);
});
I need to move the code inside btn.addActionListener to the public static void main(String[] args), something like below (in pseudo code):
Button btn = new Button("abcde");
btn.addActionListener((ActionEvent e) ->
{
notify_main()_that_button_had_been_clicked();
});
public static void main(String[] args)
{
block_until_button_clicked();
String s = UI.getButton().getLabel();
s = s.toUpperCase();
UI.getButton().setLabel(s);
}
RELEVANT INFORMATION :
I am aware that there are better solutions for GUI development, but I am restricted to using AWT for UI.
I have no power to change the above, nor can I provide details about the real code due to legal restrictions.
To remedy above, I am submitting the below MVCE. Please base your answers on it:
import java.awt.Frame;
import java.awt.Button;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class ResponsiveUI extends Frame
{
public final Button btn = new Button("abcde");
public ResponsiveUI()
{
add(btn);
btn.addActionListener((ActionEvent e) ->
{
String s = btn.getLabel().toUpperCase();
btn.setLabel(s);
});
}
public static void main(String[] args)
{
ResponsiveUI rui = new ResponsiveUI();
rui.addWindowListener(new WindowAdapter()
{
#Override
public void windowClosing(WindowEvent we)
{
System.exit(0);
}
});
rui.setSize(250, 150);
rui.setResizable(false);
rui.setVisible(true);
}
}
MY EFFORTS TO SOLVE THIS:
I have used Google extensively, and was able to find some useful links.
UI will run in the separate thread, which will make it responsive (I do not know how to join() it properly, though).
For signaling mechanism, wait() and notify() seem like the right way to go.
To set Button's text, I can use EventQueue.InvokeAndWait.
To get Button's text, I do not know what to do, but I have an ugly workaround.
Below is the modified MVCE :
import java.awt.Frame;
import java.awt.Button;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.reflect.InvocationTargetException;
public class ResponsiveUI extends Frame
{
public final Object lock = new Object(); // POINT #2 : signaling mechanism
public final Button btn = new Button("abcde");
public ResponsiveUI()
{
add(btn);
btn.addActionListener((ActionEvent e) ->
{
// POINT #2 : signal the main() thread that button is clicked
synchronized (lock)
{
lock.notify();
}
});
}
public static void main(String[] args)
{
ResponsiveUI rui = new ResponsiveUI();
// POINT #1: put UI into separate thread, so we can keep it responsive
// POINT #1: I still do not know how to properly join() (it works OK though)
Runnable r = () ->
{
rui.addWindowListener(new WindowAdapter()
{
#Override
public void windowClosing(WindowEvent we)
{
System.exit(0);
}
});
rui.setSize(250, 150);
rui.setResizable(false);
rui.setVisible(true);
};
EventQueue.invokeLater(r);
try
{
synchronized (rui.lock) // POINT #2
{ // POINT #2
rui.lock.wait(); // POINT #2 : wait for button press
final Button b = new Button(); // POINT #4 : EventQueue uses final local variables
// store text into temp button (ugly but works)
EventQueue.invokeAndWait(() -> // POINT #4
{
b.setLabel(rui.btn.getLabel());
});
// we could do all kind of things, but for illustrative purpose just transform text into upper case
EventQueue.invokeAndWait(() -> // POINT #3 :
{
rui.btn.setLabel(b.getLabel().toUpperCase());
});
}
}
catch (InterruptedException | InvocationTargetException ex)
{
System.out.println("Error : " + ex);
}
}
}
As I understand your question, you want the main thread to be notified when the [AWT] button is clicked and upon receipt of that notification, you want to change the text of that button's label.
I started with the code from your modified minimal-reproducible-example that you posted in your question.
Here is my code with explanatory notes after it.
import java.awt.Button;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class ResponsiveUI extends Frame {
private static String btnTxt;
public final Object lock = new Object();
public final Button btn = new Button("abcde");
public ResponsiveUI() {
add(btn);
btn.addActionListener((ActionEvent e) -> {
synchronized (lock) {
btnTxt = e.getActionCommand();
lock.notifyAll();
}
});
}
public static void main(String[] args) {
ResponsiveUI rui = new ResponsiveUI();
Runnable r = () -> {
rui.addWindowListener(new WindowAdapter() {
#Override
public void windowClosing(WindowEvent we) {
System.exit(0);
}
});
rui.setSize(250, 150);
rui.setResizable(false);
rui.setVisible(true);
};
EventQueue.invokeLater(r);
synchronized (rui.lock) {
try {
rui.lock.wait();
String newBtnTxt = btnTxt.toUpperCase();
EventQueue.invokeLater(() -> rui.btn.setLabel(newBtnTxt));
}
catch (InterruptedException x) {
x.printStackTrace();
}
}
}
}
In order for the GUI thread to notify the main thread, I agree with you that wait() and notifyAll() is the mechanism to use. I use notifyAll() rather than notify() to make sure that all the waiting threads are notified.
I use a shared variable btnTxt to transfer the text of the button's label between the GUI thread and the main thread.
By default, the action command of the ActionEvent is the text of the [AWT] button's label. Hence the use of method getActionCommand() in my ActionListener implementation.
All changes to the GUI must be executed on the GUI thread, so after creating the new text for the button's label in the main thread, I use method invokeLater() of class java.awt.EventQueue to actually change the button's label text.
You should be able to copy the above code as is, compile it and run it.
Change:
public final Button btn = new Button("abcde");
to:
public static final Button btn = new Button("abcde");
and move:
btn.addActionListener((ActionEvent evt) -> {
String s = btn.getLabel().toUpperCase();
btn.setLabel(s);
});
inside the main method.
The general approach is that you need to share some lock or semaphore with both the main method and your ActionListener implementation. Once you have a shared semaphore, the main method can block (wait) upon the semaphore and the ActionListener can notify the semaphore.
In pseudocode:
Lock lock = new Lock();
Button btn = new Button("abcde");
btn.addActionListener((ActionEvent e) -> {
lock.notify();
});
public static void main(String[] args) {
lock.waitUntilNotified();
String s = UI.getButton().getLabel();
s = s.toUpperCase();
UI.getButton().setLabel(s);
}
I have left the exact semaphore implementation to you since the nature of your application will dictate which mechanism will work best. For example, maybe you want to use a simple object and call the wait and notify methods on it, or you can use a blocking queue if you need to perform processing on elements that are loaded into the queue (i.e., items are queued up and the button press signals that the queued up items should now be processed).
You can find information on how to implement the wait and notify approach here:
wait and notify() Methods in Java
Guarded Blocks
Sharing the lock may be difficult since passing in the lock as a method argument is not an option in the main method. A simple approach (that would not be the first choice outside of this context) is creating the lock as a static variable:
public class Application {
public static final Lock LOCK = new Lock();
public static void main(String[] args) {
LOCK.waitUntilNotified();
String s = UI.getButton().getLabel();
s = s.toUpperCase();
UI.getButton().setLabel(s);
}
}
In some other class:
public class SomeOtherClass {
public void doSomething() {
Button btn = new Button("abcde");
btn.addActionListener((ActionEvent e) -> {
Application.LOCK.notify();
});
}
}
Bare in mind Lock is just pseudocode and should be replaced with whatever implementation of the lock that you decide to use.
Why do you need to mess with threads in first place? Threading in an AWT/Swing application has some "conventions" and you seem to be far away from them.
Why don't you use a getter to access the button and the action listener in main's layer?
First solution. Clean, and everyone is able to understand it (no public static final modifiers in an instance of a component)":
public class ResponsiveUI extends Frame {
private Button button;
public ResponsiveUI() {
super("UI");
addWindowListener(new WindowAdapter() {
#Override
public void windowClosing(WindowEvent e) {
System.exit(1);
}
});
button = new Button("something");
add(button);
}
public Button getButton() {
return button;
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
ResponsiveUI ui = new ResponsiveUI();
Button button = ui.getButton();
button.addActionListener(e -> {
button.setLabel(button.getLabel().toUpperCase());
});
ui.setSize(250, 150);
ui.setResizable(false);
ui.setVisible(true);
});
}
}
Second solution, using some sort of dependency injection and avoid having a getter for the button:
public class ResponsiveUI extends Frame {
private Button button;
public ResponsiveUI(ActionListener buttonListener) {
super("UI");
addWindowListener(new WindowAdapter() {
#Override
public void windowClosing(WindowEvent e) {
System.exit(1);
}
});
button = new Button("something");
button.addActionListener(buttonListener);
add(button);
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
ActionListener changeLabelToUppercaseListener = e -> {
Object source = e.getSource();
if (source instanceof Button) {
Button b = (Button) source;
b.setLabel(b.getLabel().toUpperCase());
}
};
ResponsiveUI ui = new ResponsiveUI(changeLabelToUppercaseListener);
ui.setSize(250, 150);
ui.setResizable(false);
ui.setVisible(true);
});
}
}
I've used a CountDownLatch to simplify all the locking system.
import java.awt.Frame;
import java.awt.Button;
import java.awt.EventQueue;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.CountDownLatch;
public class ResponsiveUI extends Frame
{
private static final CountDownLatch LOCK = new CountDownLatch(1);
public final Button btn = new Button("abcde");
public ResponsiveUI()
{
add(btn);
btn.addActionListener(ae -> LOCK.countDown());
}
public static void main(String[] args)
{
ResponsiveUI rui = new ResponsiveUI();
rui.addWindowListener(new WindowAdapter()
{
#Override
public void windowClosing(WindowEvent we)
{
System.exit(0);
}
});
rui.setSize(250, 150);
rui.setResizable(false);
rui.setVisible(true);
try {
LOCK.await();
String s = rui.btn.getLabel().toUpperCase();
EventQueue.invokeAndWait(() ->
{
rui.btn.setLabel(s);
});
}
catch (InvocationTargetException | InterruptedException ex)
{
System.out.println("Error : " + ex);
}
}
}
I'm trying make a Splash Screen, so I need that some words stays appearing. I used o Thread, but I don't know how can I make a loop for label appear and after one second it changes.
package br.com.codeking.zarsystem.splash;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class ControllerSplash {
#FXML
Label lblLoading;
#FXML
private void initialize() {
System.out.println("app start");
lblLoading.setStyle("-fx-font-size: 16px;" + "-fx-font-family: Ubuntu;"
+ " -fx-text-fill: white;");
here I've tried to repeat this step 10 times, but it don't works
while (i <= 10) {
Task<Void> sleeper = new Task<Void>() {
#Override
protected Void call() throws Exception {
try {
Thread.sleep(1500);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
};
sleeper.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
#Override
public void handle(WorkerStateEvent event) {
lblLoading.setText("to change " + i);
}
});
new Thread(sleeper).run();
i++;
}
}
}
I can't execute this in a for loop? So I don't have idea what I
have to do... I'm looking for, but nothing helps me. Can you? Thank
very much!
You can use a PauseTransition:
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(1));
pauseTransition.setOnFinished(e -> lblLoading.setText("complete"));
pauseTransition.play();
How can we update multiple controls from a single Service. Right now there is only one single updateMessage() in Service, whose value can be bound to just one control and hence update just that. How can we update values for multiple controls ?
My instance of Service Class:
//run a background thread
threadTimeChecker = new Service<Void>() {
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
while (!isDone) {
DataHelper.setCurrentDate(LocalDate.now());
if(!DataHelper.getOldDate().equals(DataHelper.getCurrentDate())) {
DataHelper.setIntIndex(DataHelper.getIntIndex()+1);
DataHelper.setOldDate(DataHelper.getCurrentDate());
DataHelper.saveData();
System.out.println("Saved!");
}
//Thread.currentThread().sleep(2000);
updateMessage(wordString.getValue());
}
return null;
}
};
}
};
threadTimeChecker.restart();
//bind string properties to labels
word.textProperty().bind(threadTimeChecker.messageProperty());
This only updates one message i.e. I can only bind one label. Is there any way I can update multiple messages from the same thread so I can bind multiple labels in my UI?
EDITED - More Information according to comments
My runnable is:
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
//loop run until the program is closed
while (!isDone) {
DataHelper.setCurrentDate(LocalDate.now());
if (!DataHelper.getOldDate().equals(DataHelper.getCurrentDate())) {
Platform.runLater(new Runnable(){
#Override
public void run() {
DataHelper.setIntIndex(DataHelper.getIntIndex()+1);
}
});
DataHelper.setOldDate(DataHelper.getCurrentDate());
DataHelper.saveData();
}
Thread.currentThread().sleep(5000);
}
return null;
}
};
}
I run a thread to change DataHelper.IntIndex which invokes a listener that changes the 'String Property' as per the index like:
//listener to detect change in index and assign strings of word,meaning, and sentence, accordingly
DataHelper.intIndexProperty().addListener(
(v, oldValue, newValue) -> {
wordString.setValue(DataHelper.getListOfWords().get((int) newValue).get("word"));
meaningString.setValue(DataHelper.getListOfWords().get((int) newValue).get("meaning"));
sentenceString.setValue(DataHelper.getListOfWords().get((int) newValue).get("sentence"));
System.out.print("kjbmmj");
}
);
And I have used these 'String Properties' to bind to three different labels correspondingly like:
//bind string properties to labels
word.textProperty().bind(wordString);
meaning.textProperty().bind(meaningString);
sentence.textProperty().bind(sentenceString);
Now what I want to do is to use more JavaFX inclined updateMessage to achieve the same.
Instead of updating multiple messages, we just need to update a single instance of DataHelper. DataHelper has contents which will update multiple labels. For instance, let us consider we have the following labels which we want to update after each call of service :
wordLabel
meaningLabel
sentenceLabel
To keep things simple, let us consider that you've a class DataHelper which has three properties word, meaning and sentence.
private class DataHelper {
public DataHelper(String word, String meaning, String sentence) {
this.word.setValue(word);
this.meaning.setValue(meaning);
this.sentence.setValue(sentence);
}
StringProperty word = new SimpleStringProperty();
StringProperty meaning = new SimpleStringProperty();
StringProperty sentence = new SimpleStringProperty();
// setters and getters
}
We call the service for some background task and whenever the service is done with the background task it can return us the updated DataHelper.
Service<DataHelper> service = new Service<DataHelper>() {
#Override
protected Task<DataHelper> createTask() {
return new Task<DataHelper>() {
#Override
protected DataHelper call() throws Exception {
i.incrementAndGet(); // Don't worry about i here
return new DataHelper("Word " + i, "Meaning " + i, "Sentence " + i);
}
};
}
};
Now, every time we call the server we get an updated DataHelper which we want to show on the label(s).
To approach this, we declare a variable dataHelper and bind its properties to the textProperty() of various labels :
DataHelper dataHelper = new DataHelper("Word", "Meaning", "Sentence");
wordLabel.textProperty().bind(dataHelper.wordProperty());
meaningLabel.textProperty().bind(dataHelper.meaningProperty());
sentenceLabel.textProperty().bind(dataHelper.sentenceProperty());
Now you must be wondering, how will we update dataHelper, right? Well, that's the easy part. This can be can taken care in setOnSucceeded() of Service, where getValue() will return us a new instance of DataHelper with updated values.
service.setOnSucceeded(event -> {
dataHelper.setWord(service.getValue().getWord());
dataHelper.setMeaning(service.getValue().getMeaning());
dataHelper.setSentence(service.getValue().getSentence());
});
Complete MCVE :
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.concurrent.atomic.AtomicInteger;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
// Properties
DataHelper dataHelper = new DataHelper("Word", "Meaning", "Sentence");
AtomicInteger i = new AtomicInteger(0);
// UI elements
Label wordLabel = new Label();
Label meaningLabel = new Label();
Label sentenceLabel = new Label();
Button startService = new Button("Start");
Service<DataHelper> service = new Service<DataHelper>() {
#Override
protected Task<DataHelper> createTask() {
return new Task<DataHelper>() {
#Override
protected DataHelper call() throws Exception {
i.incrementAndGet();
return new DataHelper("Word " + i, "Meaning " + i, "Sentence " + i);
}
};
}
};
startService.setOnAction(e -> {
if(service.getState().equals(Worker.State.READY) || service.getState().equals(Worker.State.SUCCEEDED)) {
service.restart();
}
});
service.setOnSucceeded(event -> {
dataHelper.setWord(service.getValue().getWord());
dataHelper.setMeaning(service.getValue().getMeaning());
dataHelper.setSentence(service.getValue().getSentence());
});
wordLabel.textProperty().bind(dataHelper.wordProperty());
meaningLabel.textProperty().bind(dataHelper.meaningProperty());
sentenceLabel.textProperty().bind(dataHelper.sentenceProperty());
VBox box = new VBox(10, wordLabel, meaningLabel, sentenceLabel, startService);
box.setAlignment(Pos.CENTER);
Scene scene = new Scene(box, 200, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
private class DataHelper {
StringProperty word = new SimpleStringProperty();
StringProperty meaning = new SimpleStringProperty();
StringProperty sentence = new SimpleStringProperty();
public DataHelper(String word, String meaning, String sentence) {
this.word.setValue(word);
this.meaning.setValue(meaning);
this.sentence.setValue(sentence);
}
public String getMeaning() {
return meaning.get();
}
public StringProperty meaningProperty() {
return meaning;
}
public void setMeaning(String meaning) {
this.meaning.set(meaning);
}
public String getSentence() {
return sentence.get();
}
public StringProperty sentenceProperty() {
return sentence;
}
public void setSentence(String sentence) {
this.sentence.set(sentence);
}
public String getWord() {
return word.get();
}
public StringProperty wordProperty() {
return word;
}
public void setWord(String word) {
this.word.set(word);
}
}
public static void main(String[] args) {
launch(args);
}
}
The initial situation is like this:
Now, the tree is potentially huge (I have a tree with 22 million nodes for instance). What happens here is that I use a jooq/h2 backend to store all the nodes and execute queries to find children.
Which means, in the image above, the node is marked as expandable but its children are not populated yet. It is done on demand. And after expansion I get this:
The problem I have is that of course, expansion can take time; and what I'd like to do is to add a visual clue to the graphic of the TreeItem to show that it is loading...
And I can't do it.
OK, first of all, a general view of the architecture:
it is, in short, "MVP with passive views";
what JavaFX calls a "controller", which is in my code implemented by *Display classes, are the passive views, whose only role is to capture UI events and forward them to the *Presenter; such classes are "GUI implementation specific";
the *Presenter reacts to those events by ordering the *View class to update the *Display;
when tasks require a certain amount of time to complete, too much for the GUI to remain interactive, a BackgroundTaskRunner is used by the *Presenter to:
instruct the *View to modify the UI to acknowledge the task (on the GUI thread);
perform the task (on a background thread);
instruct the *View to modify the UI when the task completes (on the GUI thread);
if the task fails, instruct the *View to modify the UI accordingly (on the GUI thread).
With JavaFX:
the UI is a *Display class; it is defined by, and loaded from, an FXML file;
the (implementation of) the *View class has visibility over all GUI elements defined in the *Display class.
The *View class is in fact an interface; this allows me to be able to make a webapp version of this program (planned).
Now, the context of this code...
The *Presenter, *View and *Display all relate to the "Parse tree" tab visible in the above images.
Given the architecture above, the problem lies with the implementation of the *View class, and with the *Display class.
The *Display class has an init() method which initializes all relevant JavaFX components, if need be. In this case, the TreeView, called parseTree, is initialized as such:
#Override
public void init()
{
parseTree.setCellFactory(param -> new ParseTreeNodeCell(this));
}
ParseTreeNodeCell is defined as such:
public final class ParseTreeNodeCell
extends TreeCell<ParseTreeNode>
{
// FAILED attempt at showing a progress indicator...
private final ProgressIndicator indicator = new ProgressIndicator();
private final Text text = new Text();
private final HBox hBox = new HBox(text, indicator);
public ParseTreeNodeCell(final TreeTabDisplay display)
{
// FIXME: not sure about the following line...
indicator.setMaxHeight(heightProperty().doubleValue());
// ... But this I want: by default, not visible
indicator.setVisible(false);
// The whole tree is readonly
setEditable(false);
// Some non relevant code snipped away
}
public void showIndicator()
{
indicator.setVisible(true);
}
public void hideIndicator()
{
indicator.setVisible(false);
}
// What to do when a TreeItem is actually attached...
#Override
protected void updateItem(final ParseTreeNode item, final boolean empty)
{
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
return;
}
final String msg = String.format("%s (%s)",
item.getRuleInfo().getName(),
item.isSuccess() ? "SUCCESS" : "FAILURE");
text.setText(msg);
setGraphic(hBox);
// HACK. PUKE. UGLY.
((ParseTreeItem) getTreeItem()).setCell(this);
}
}
ParseTreeItem is this:
public final class ParseTreeItem
extends TreeItem<ParseTreeNode>
{
private final boolean leaf;
private ParseTreeNodeCell cell;
public ParseTreeItem(final TreeTabDisplay display,
final ParseTreeNode value)
{
super(value);
leaf = !value.hasChildren();
// If the item is expanded, we load children.
// If it is collapsed, we unload them.
expandedProperty().addListener(new ChangeListener<Boolean>()
{
#Override
public void changed(
final ObservableValue<? extends Boolean> observable,
final Boolean oldValue, final Boolean newValue)
{
if (oldValue == newValue)
return;
if (!newValue) {
getChildren().clear();
return;
}
display.needChildren(ParseTreeItem.this);
}
});
}
#Override
public boolean isLeaf()
{
return leaf;
}
public void setCell(final ParseTreeNodeCell cell)
{
this.cell = cell;
}
public void showIndicator()
{
cell.showIndicator();
}
public void hideIndicator()
{
cell.hideIndicator();
}
}
Now, always in the *Display class, the needChildren() method is defined as such:
ParseTreeItem currentItem;
// ...
public void needChildren(final ParseTreeItem parseTreeItem)
{
// Keep a reference to the current item so that the *View can act on it
currentItem = parseTreeItem;
presenter.needChildren(currentItem.getValue());
}
The presenter does this:
public void needChildren(final ParseTreeNode value)
{
taskRunner.computeOrFail(
view::waitForChildren, () -> {
// FOR TESTING
TimeUnit.SECONDS.sleep(1L);
return getNodeChildren(value.getId());
},
view::setTreeChildren,
throwable -> mainView.showError("Tree expand error",
"Unable to extend parse tree node", throwable)
);
}
(see here; for the taskRunner)
The corresponding methods in the view member above (JavaFX implementation) do this:
#Override
public void waitForChildren()
{
// Supposedly shows the indicator in the TreeItemGraphic...
// Except that it does not.
display.currentItem.showIndicator();
}
#Override
public void setTreeChildren(final List<ParseTreeNode> children)
{
final List<ParseTreeItem> items = children.stream()
.map(node -> new ParseTreeItem(display, node))
.collect(Collectors.toList());
// This works fine
display.currentItem.getChildren().setAll(items);
// But this does not...
display.currentItem.hideIndicator();
}
Even though I define methods on the TreeItem to show the progress indicator, it doesn't show at all :/
In fact, my problem is twofold, and all related to ParseTreeItem:
in ParseTreeNodeCell, I need to cast to ParseTreeItem to set the cell;
even when I do this, well, it doesn't work at all, I can't see the indicator show up at all.
Not only that, but for some reason I need to check (in ParseTreeNodeCell) that I actually have a value, since otherwise I get an NPE. And I can't find a way to get the matching cell from a tree item...
So, all in all, I do many things badly and none correctly.
How do I manage to get the graphic of a TreeItem change in that situation, as long as the loading is still in progress?
EDIT
Solution found, inspited by the code written by #James_D; see my own answer for how I really did it.
First, I admit I didn't go through all your code carefully.
I think the approach to have here is to use a TreeItem subclass that exposes an observable property describing the "loaded status" of the children. Then have the tree cells observe the loaded status of the current tree item, and display a progress bar accordingly.
Here's a SSCCE:
(Updated: apparently if I only observe the treeItem and not the item, the tree fails to remove the disclosure graphics from empty cells... Fixed by using the itemProperty to manage the text.)
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class LazyTreeCellLoadingExample extends Application {
// Executor for background tasks:
private static final ExecutorService exec = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t ;
});
#Override
public void start(Stage primaryStage) {
TreeView<Long> tree = new TreeView<>();
tree.setRoot(new LazyTreeItem(1L));
// cell factory that displays progress bar when item is loading children:
tree.setCellFactory(tv -> {
// the cell:
TreeCell<Long> cell = new TreeCell<>();
// progress bar to display when needed:
ProgressBar progressBar = new ProgressBar();
// listener to observe *current* tree item's child loading status:
ChangeListener<LazyTreeItem.ChildrenLoadedStatus> listener = (obs, oldStatus, newStatus) -> {
if (newStatus == LazyTreeItem.ChildrenLoadedStatus.LOADING) {
cell.setGraphic(progressBar);
} else {
cell.setGraphic(null);
}
};
// listener for tree item property
// ensures that listener above is attached to current tree item:
cell.treeItemProperty().addListener((obs, oldItem, newItem) -> {
// if we were displaying an item, (and no longer are...),
// stop observing its child loading status:
if (oldItem != null) {
((LazyTreeItem) oldItem).childrenLoadedStatusProperty().removeListener(listener);
}
// if there is a new item the cell is displaying:
if (newItem != null) {
// update graphic to display progress bar if needed:
LazyTreeItem lazyTreeItem = (LazyTreeItem) newItem;
if (lazyTreeItem.getChildrenLoadedStatus() == LazyTreeItem.ChildrenLoadedStatus.LOADING) {
cell.setGraphic(progressBar);
} else {
cell.setGraphic(null);
}
// observe loaded status of current item in case it changes
// while we are still displaying this item:
lazyTreeItem.childrenLoadedStatusProperty().addListener(listener);
}
});
// change text if item changes:
cell.itemProperty().addListener((obs, oldItem, newItem) -> {
if (newItem == null) {
cell.setText(null);
cell.setGraphic(null);
} else {
cell.setText(newItem.toString());
}
});
return cell ;
});
Button debugButton = new Button("Debug");
debugButton.setOnAction(evt -> {
dumpData(tree.getRoot(), 0);
});
primaryStage.setScene(new Scene(new BorderPane(tree, null, null, debugButton, null), 400, 250));
primaryStage.show();
}
private void dumpData(TreeItem<Long> node, int depth) {
for (int i=0; i<depth; i++) System.out.print(" ");
System.out.println(node.getValue());
for (TreeItem<Long> child : node.getChildren()) dumpData(child, depth+1);
}
// TreeItem subclass that lazily loads children in background
// Exposes an observable property specifying current load status of children
public static class LazyTreeItem extends TreeItem<Long> {
// possible load statuses:
enum ChildrenLoadedStatus { NOT_LOADED, LOADING, LOADED }
// observable property for current load status:
private final ObjectProperty<ChildrenLoadedStatus> childrenLoadedStatus = new SimpleObjectProperty<>(ChildrenLoadedStatus.NOT_LOADED);
public LazyTreeItem(Long value) {
super(value);
}
// getChildren() method loads children lazily:
#Override
public ObservableList<TreeItem<Long>> getChildren() {
if (getChildrenLoadedStatus() == ChildrenLoadedStatus.NOT_LOADED) {
lazilyLoadChildren();
}
return super.getChildren() ;
}
// load child nodes in background, updating status accordingly:
private void lazilyLoadChildren() {
// change current status to "loading":
setChildrenLoadedStatus(ChildrenLoadedStatus.LOADING);
long value = getValue();
// background task to load children:
Task<List<LazyTreeItem>> loadTask = new Task<List<LazyTreeItem>>() {
#Override
protected List<LazyTreeItem> call() throws Exception {
List<LazyTreeItem> children = new ArrayList<>();
for (int i=0; i<10; i++) {
children.add(new LazyTreeItem(10*value + i));
}
// for testing (loading is so lazy it falls asleep)
Thread.sleep(3000);
return children;
}
};
// when loading is complete:
// 1. set actual child nodes to loaded nodes
// 2. update status to "loaded"
loadTask.setOnSucceeded(event -> {
super.getChildren().setAll(loadTask.getValue());
setChildrenLoadedStatus(ChildrenLoadedStatus.LOADED);
});
// execute task in backgroun
exec.submit(loadTask);
}
// is leaf is true only if we *know* there are no children
// i.e. we've done the loading and still found nothing
#Override
public boolean isLeaf() {
return getChildrenLoadedStatus() == ChildrenLoadedStatus.LOADED && super.getChildren().size()==0 ;
}
// normal property accessor methods:
public final ObjectProperty<ChildrenLoadedStatus> childrenLoadedStatusProperty() {
return this.childrenLoadedStatus;
}
public final LazyTreeCellLoadingExample.LazyTreeItem.ChildrenLoadedStatus getChildrenLoadedStatus() {
return this.childrenLoadedStatusProperty().get();
}
public final void setChildrenLoadedStatus(
final LazyTreeCellLoadingExample.LazyTreeItem.ChildrenLoadedStatus childrenLoadedStatus) {
this.childrenLoadedStatusProperty().set(childrenLoadedStatus);
}
}
public static void main(String[] args) {
launch(args);
}
}
Update
After quite a bit of discussion, I came up with a second solution. This is basically similar to the previous solution in the way it manages the progress bar: there is a TreeItem subclass that exposes a BooleanProperty which is true if and only if the item is currently loading its children. The TreeCell observes this property on the TreeItem it is currently displaying - taking care to register a listener with the treeItemProperty so that the listener for the loadingProperty is always registered with the current item.
The difference is in the way the children are loaded, and - in the case of this solution - unloaded. In the previous solution, child nodes were loaded when first requested, and then retained. In this solution, child nodes are loaded when the node is expanded, and then removed when the node is collapsed. This is handled with a simple listener on the expandedProperty.
The first solution behaves slightly more as expected from a user perspective, in that if you collapse a node which is the head of a subtree, and then expand it again, the expanded state of the subtree is retained. In the second solution, collapsing a node has the effect of collapsing all descendent nodes (because they are actually removed).
The second solution is more robust for memory usage. This is actually unlikely to be an issue outside of some extreme use cases. TreeItem objects are purely model - i.e. they store only data, no UI. Thus they probably use no more than a few hundred bytes of memory each. In order to consume significant amounts of memory, the user would have to navigate through hundreds of thousands of nodes, which would probably take days. (That said, I'm typing this into Google Chrome, which I think I've had running for over a month with at least 8-10 hours' active use per day, so such use cases are certainly not impossible.)
Here's the second solution. One note: I don't make any effort to handle a quick expand and collapse of a node (collapsing while the data are still loading). The TreeItem subclass should really keep track of any current Task (or use a Service) and call cancel() if a task is running and the user collapses the node. I didn't want to confuse the logic more than necessary to demonstrate the basic idea.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class LazyTreeCellLoadingExample2 extends Application {
private static final ExecutorService EXEC = Executors.newCachedThreadPool((Runnable r) -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t ;
});
#Override
public void start(Stage primaryStage) {
TreeView<Integer> tree = new TreeView<>();
tree.setRoot(new LazyTreeItem(1));
tree.setCellFactory(tv -> createTreeCell()) ;
primaryStage.setScene(new Scene(new BorderPane(tree), 450, 600));
primaryStage.show();
}
private TreeCell<Integer> createTreeCell() {
ProgressBar progressBar = new ProgressBar();
TreeCell<Integer> cell = new TreeCell<>();
ChangeListener<Boolean> loadingChangeListener =
(ObservableValue<? extends Boolean> obs, Boolean wasLoading, Boolean isNowLoading) -> {
if (isNowLoading) {
cell.setGraphic(progressBar);
} else {
cell.setGraphic(null);
}
};
cell.treeItemProperty().addListener(
(ObservableValue<? extends TreeItem<Integer>> obs,
TreeItem<Integer> oldItem,
TreeItem<Integer> newItem) -> {
if (oldItem != null) {
LazyTreeItem oldLazyTreeItem = (LazyTreeItem) oldItem ;
oldLazyTreeItem.loadingProperty().removeListener(loadingChangeListener);
}
if (newItem != null) {
LazyTreeItem newLazyTreeItem = (LazyTreeItem) newItem ;
newLazyTreeItem.loadingProperty().addListener(loadingChangeListener);
if (newLazyTreeItem.isLoading()) {
cell.setGraphic(progressBar);
} else {
cell.setGraphic(null);
}
}
});
cell.itemProperty().addListener(
(ObservableValue<? extends Integer> obs, Integer oldItem, Integer newItem) -> {
if (newItem == null) {
cell.setText(null);
cell.setGraphic(null);
} else {
cell.setText(newItem.toString());
}
});
return cell ;
}
public static class LazyTreeItem extends TreeItem<Integer> {
private final BooleanProperty loading = new SimpleBooleanProperty(false);
private boolean leaf = false ;
public final BooleanProperty loadingProperty() {
return this.loading;
}
public final boolean isLoading() {
return this.loadingProperty().get();
}
public final void setLoading(final boolean loading) {
this.loadingProperty().set(loading);
}
public LazyTreeItem(Integer value) {
super(value);
expandedProperty().addListener((ObservableValue<? extends Boolean>obs, Boolean wasExpanded, Boolean isNowExpanded) -> {
if (isNowExpanded) {
loadChildrenLazily();
} else {
clearChildren();
}
});
}
#Override
public boolean isLeaf() {
return leaf ;
}
private void loadChildrenLazily() {
setLoading(true);
int value = getValue();
Task<List<TreeItem<Integer>>> loadTask = new Task<List<TreeItem<Integer>>>() {
#Override
protected List<TreeItem<Integer>> call() throws Exception {
List<TreeItem<Integer>> children = new ArrayList<>();
for (int i=0; i<10; i++) {
children.add(new LazyTreeItem(value * 10 + i));
}
Thread.sleep(3000);
return children ;
}
};
loadTask.setOnSucceeded(event -> {
List<TreeItem<Integer>> children = loadTask.getValue();
leaf = children.size() == 0 ;
getChildren().setAll(children);
setLoading(false);
});
EXEC.submit(loadTask);
}
private void clearChildren() {
getChildren().clear();
}
}
public static void main(String[] args) {
launch(args);
}
}
The "double listener" in for the cell is because we really need to observe a "property of a property". Specifically, the cell has a treeItem property, and the tree item has a loadingProperty. It's the loading property belonging to the current tree item that we're really interested in. Of course, there are two ways this can change: the tree item changes in the cell, or the loading property changes in the tree item. The EasyBind framework includes API specifically for observing "properties of properties". If you use EasyBind, you can replace the (30 or so lines of) code
ChangeListener<Boolean> loadingChangeListener = ... ;
cell.treeItemProperty().addListener(...);
with
ObservableValue<Boolean> loading = EasyBind.select(cell.treeItemProperty())
// ugly cast still here:
.selectObject(treeItem -> ((LazyTreeItem)treeItem).loadingProperty());
loading.addListener((obs, wasLoading, isNowLoading) -> {
if (isNowLoading != null && isNowLoading.booleanValue()) {
cell.setGraphic(progressBar);
} else {
cell.setGraphic(null);
}
});
Problem SOLVED!
Here is how I solved it...
The first thing that I learned with this question is that a TreeCell is in fact a "moving object": it moves from TreeItem to TreeItem; here, the TreeItem is a ParseTreeItem which has a dedicated property, loadingProperty(), and in the cell's treeItemProperty() I make use of this property to update the graphics.
As #James_D suggested in his code, I use EasyBind; the code is basically a ripoff of his, except for the fact that I don't use the cell's text but only a graphic, which is a HorizontalBox.
In addition I also have to listen to the cell's selectedProperty() since when it is selected, I need to update the info box and the text area:
public final class ParseTreeNodeCell
extends TreeCell<ParseTreeNode>
{
private final Text text = new Text();
private final ProgressBar progressBar = new ProgressBar();
private final HBox hBox = new HBox(text);
public ParseTreeNodeCell(final TreeTabDisplay display)
{
setEditable(false);
selectedProperty().addListener(new ChangeListener<Boolean>()
{
#SuppressWarnings("AutoUnboxing")
#Override
public void changed(
final ObservableValue<? extends Boolean> observable,
final Boolean oldValue, final Boolean newValue)
{
if (!newValue)
return;
final ParseTreeNode node = getItem();
if (node != null)
display.parseTreeNodeShowEvent(node);
}
});
final ObservableValue<Boolean> loading
= EasyBind.select(treeItemProperty())
.selectObject(item -> ((ParseTreeItem) item).loadingProperty());
loading.addListener(new ChangeListener<Boolean>()
{
#Override
public void changed(
final ObservableValue<? extends Boolean> observable,
final Boolean oldValue, final Boolean newValue)
{
final ObservableList<Node> children = hBox.getChildren();
if (newValue == null || !newValue.booleanValue()) {
children.remove(progressBar);
return;
}
if (!children.contains(progressBar))
children.add(progressBar);
}
});
itemProperty().addListener(new ChangeListener<ParseTreeNode>()
{
#Override
public void changed(
final ObservableValue<? extends ParseTreeNode> observable,
final ParseTreeNode oldValue, final ParseTreeNode newValue)
{
if (newValue == null) {
setGraphic(null);
return;
}
text.setText(String.format("%s (%s)",
newValue.getRuleInfo().getName(),
newValue.isSuccess() ? "SUCCESS" : "FAILURE"));
setGraphic(hBox);
}
});
}
}
Now, the code of ParseTreeItem which extends TreeItem<ParseTreeNode>, is as such:
public final class ParseTreeItem
extends TreeItem<ParseTreeNode>
{
private final BooleanProperty loadingProperty
= new SimpleBooleanProperty(false);
private final boolean leaf;
public ParseTreeItem(final TreeTabDisplay display,
final ParseTreeNode value)
{
super(value);
leaf = !value.hasChildren();
expandedProperty().addListener(new ChangeListener<Boolean>()
{
#Override
public void changed(
final ObservableValue<? extends Boolean> observable,
final Boolean oldValue, final Boolean newValue)
{
if (!newValue) {
getChildren().clear();
return;
}
display.needChildren(ParseTreeItem.this);
}
});
}
public BooleanProperty loadingProperty()
{
return loadingProperty;
}
#Override
public boolean isLeaf()
{
return leaf;
}
}
Again, this is nearly a ripoff, except for the fact that I don't want the item to hold the logic to:
fetch the children,
updating its own loading property.
The "secret" is in display.needChildren(ParseTreeItem.this); this is what it does:
public void needChildren(final ParseTreeItem parseTreeItem)
{
currentItem = parseTreeItem;
presenter.needChildren(currentItem.getValue());
}
And, in turn, the code in presenter does:
public void needChildren(final ParseTreeNode value)
{
taskRunner.computeOrFail(
view::waitForChildren,
() -> getNodeChildren(value.getId()),
view::setTreeChildren,
throwable -> mainView.showError("Tree expand error",
"Unable to extend parse tree node", throwable)
);
}
And is where the view comes in; it is the view which updates currentItem, since it has direct access to the display and its fields:
#Override
public void waitForChildren()
{
display.currentItem.loadingProperty().setValue(true);
}
#Override
public void setTreeChildren(final List<ParseTreeNode> children)
{
final List<ParseTreeItem> items = children.stream()
.map(node -> new ParseTreeItem(display, node))
.collect(Collectors.toList());
display.currentItem.getChildren().setAll(items);
display.currentItem.loadingProperty().setValue(false);
}
As you can see, when waitingForChildren() and setTreeChildren() update the loadingProperty(); and in turn the ParseTreeNodeCell will then update the graphics accordingly.
I'm new to Javafx and developing an IDE using this. The problem i'm facing with JavaFX is that i have to use Platform.runLater() to reflect changes in GUI from other threads. As my IDE i'm developing use multiple threads to keep up to date information and using Platform.runLater() makes application unresponsive. And sometime background processes has to print output of millions of line which i think cause problem when multiple threads try to do same. I tried to put a counter so that if output is larger than 250000 lines it will print output after 250000 lines else in other case it will print immediately after completion of the thread, even in this case if two or more thread try to execute Platform.runLater() (also there are other threads which creates tree with checkbox items and reflect realtime values) application hangs but everything in background is keep running normally and even application doesn't throw any exception. In normal java swing app i didn't face any similar problem. So i'm seeking guidance to tackle these problems. Can somebody gave me PRO tips to solve similar problems? :)
Edit On The request of #jewelsea
I tried to keep the sample code as simple as possible
FxUI.java
public class FxUI extends Application {
public static TextArea outputArea;
#Override
public void start(Stage primaryStage) {
outputArea= new TextArea();
Button btn = new Button();
btn.setText("Start Appending Text To Text Area");
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
Thread r=new Thread( new Runnable() {
#Override
public void run() {
for (int i = 0; i < 10; i++) {
Thread t= new Thread(new simpleThread(i));
t.start();
try {
Thread.sleep(1000);
System.out.println("Thread Awake");
} catch (InterruptedException ex) {
Logger.getLogger(FxUI.class.getName()).log(Level.SEVERE, null, ex);
}
} }
});
r.start();
}
});
VBox root = new VBox(30);
outputArea.setWrapText(true);
outputArea.setPrefHeight(400);
root.getChildren().add(outputArea);
root.getChildren().add(btn);
Scene scene = new Scene(root, 500, 500);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
simpleThread.java
public class simpleThread implements Runnable {
int threadnumber;
public simpleThread(int j) {
threadnumber = j;
}
#Override
public void run() {
String output = "";
String content;
int length;
final String finalcontent2;
final int finallength2;
for (long i = 0L; i <= 10000; i++) {
final String finalcontent;
final int finallength;
if (i % 1000 == 0) {
output += "\nThread number = " + threadnumber + " \t Loop Counter=" + i;
content = FxUI.outputArea.getText() + "\n" + output;
length = content.length();
finallength = length;
finalcontent = "" + content;
Platform.runLater(new Runnable() {
#Override
public void run() {
System.out.println("appending output");
FxUI.outputArea.setText(finalcontent);
FxUI.outputArea.positionCaret(finallength);
}
});
} else {
output += "\nThread number = " + threadnumber + " \t Loop Counter=" + i;
}
System.out.println("Thread number = " + threadnumber + " \t Loop Counter=" + i);
}
}
}
I think the first real issue here is that your code is simply massively inefficient. Building up a string in a loop is a really bad thing to do: you create a new object and copy all the characters every time. Additionally, each time you update the text area, you are copying the entire existing text, creating another String by concatenating the additional content, and then replacing all the existing content with the new content. The string concatenation is going to run in quadratic time (as you are increasing the length of the strings each time) and you're going to cause mayhem for Java's string interning process.
Also, note that you shouldn't read the state of a node in the scene graph anywhere except on the FX application thread, so your line
content = FxUI.outputArea.getText() + "\n" + output;
is not thread-safe.
In general, to build up a string in a loop, you should use a StringBuilder to build up the string contents. If you're using a TextArea, it has an appendText(...) method which is all you need to update it.
Update following discussion in comments:
Having made those general comments, making those improvements doesn't really get you to a state where the performance is acceptable. My observation there is that the TextArea is slow to respond to user input even after the threads have completed. The issue is (I guess) that you have a large amount of data which is actually associated with a "live" part of the scene graph.
A better option here is probably to use a virtualized control such as a ListView to display the data. These only have cells for the visible portion and reuse them as the user scrolls. Here is an example. I added selection and copy-to-clipboard functionality as that is the main thing you would miss going from a TextArea to a ListView. (Note that if you have a huge number of things selected, the String.join() method is very slow to run. You might have to create a background task for that and a blocking dialog to show its progress if that's important.)
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class BigListBackgroundThreadDemo extends Application {
private static final int NUM_ITERATIONS = 10_000 ;
private static final int NUM_THREADS_PER_CALL = 5 ;
#Override
public void start(Stage primaryStage) {
ListView<String> data = new ListView<>();
data.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
Button startButton = new Button("Start");
Button selectAllButton = new Button("Select All");
Button selectNoneButton = new Button("Clear Selection");
Button copyToClipboardButton = new Button("Copy to clipboard");
copyToClipboardButton.disableProperty().bind(Bindings.isEmpty(data.getSelectionModel().getSelectedItems()));
AtomicInteger threadCount = new AtomicInteger();
ExecutorService exec = Executors.newFixedThreadPool(5, r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t ;
});
startButton.setOnAction(event -> {
exec.submit(() -> {
for (int i=0; i < NUM_THREADS_PER_CALL; i++) {
exec.submit(createTask(threadCount, data));
try {
Thread.sleep(500);
} catch (InterruptedException exc) {
throw new Error("Unexpected interruption", exc);
}
}
});
});
selectAllButton.setOnAction(event -> {
data.getSelectionModel().selectAll();
data.requestFocus();
});
selectNoneButton.setOnAction(event -> {
data.getSelectionModel().clearSelection();
data.requestFocus();
});
copyToClipboardButton.setOnAction(event -> {
ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(String.join("\n", data.getSelectionModel().getSelectedItems()));
Clipboard.getSystemClipboard().setContent(clipboardContent);
});
HBox controls = new HBox(5, startButton, selectAllButton, selectNoneButton, copyToClipboardButton);
controls.setAlignment(Pos.CENTER);
controls.setPadding(new Insets(5));
BorderPane root = new BorderPane(data, null, null, controls, null);
Scene scene = new Scene(root, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private Task<Void> createTask(AtomicInteger threadCount, ListView<String> target) {
return new Task<Void>() {
#Override
public Void call() throws Exception {
int count = threadCount.incrementAndGet();
AtomicBoolean pending = new AtomicBoolean(false);
BlockingQueue<String> messages = new LinkedBlockingQueue<>();
for (int i=0; i < NUM_ITERATIONS; i++) {
messages.add("Thread number: "+count + "\tLoop counter: "+i);
if (pending.compareAndSet(false, true)) {
Platform.runLater(() -> {
pending.set(false);
messages.drainTo(target.getItems());
target.scrollTo(target.getItems().size()-1);
});
}
}
return null ;
}
};
}
public static void main(String[] args) {
launch(args);
}
}
In JavaFX you have to do your background process in a Service which run a Task . By doing this you'll won't freez your GUI thread
Quick example, if you want a String as a return value of your process.
The service :
public class MyService extends Service<String> {
#Override
protected Task<String> createTask() {
return new Task<String>() {
#Override
protected String call() throws Exception {
//Do your heavy stuff
return "";
}
};
}
}
Place you want to use your service :
final MyService service = new MyService();
service.setOnSucceeded(e -> {
//your service finish with no problems
service.getValue(); //get the return value of your service
});
service.setOnFailed(e -> {
//your service failed
});
service.restart();
You have other method like setOnFailed, for the different status. So implement what you need.
You can also monitor this service, But I let you read the doc for this. It's quit simple.
You should also read JavaFX concurency