JavaFX splash screen message and progress not updating - java

I have a JavaFX application that checks for the presence of a database at startup. If the database is not found, a "seed" database must be created before the program can proceed.
Since creating the seed database can be lengthy, I want to display a splash screen and show progress updates as the process proceeds. Only after the seed database is completely written should the program proceed.
Here is the class for the splash screen:
public final class SplashWindow {
private final Label message;
private final ProgressBar progress;
private final Stage splashStage;
public SplashWindow() {
Image img = new Image(IMAGE_PREFIX + "splash_image.png");
double imgWidth = img.getWidth();
ImageView splashImage = new ImageView(img);
splashImage.setFitWidth(imgWidth);
splashImage.setPreserveRatio(true);
message = new Label("Saving seed database...");
message.setPrefWidth(imgWidth);
message.setAlignment(Pos.CENTER);
progress = new ProgressBar();
progress.setPrefWidth(imgWidth);
Pane splashVBox = new VBox(3);
splashVBox.setPadding(new Insets(5));
splashVBox.getChildren().addAll(splashImage, progress, message);
splashStage = new Stage(StageStyle.UTILITY);
splashStage.setScene(new Scene(splashVBox));
}
public void bindMessageProperty(ReadOnlyStringProperty sp) {
message.textProperty().bind(sp);
}
public void bindProgressProperty(ReadOnlyDoubleProperty dp) {
progress.progressProperty().bind(dp);
}
public void show() {
splashStage.show();
}
public void shutdown() {
message.textProperty().unbind();
progress.progressProperty().unbind();
splashStage.hide();
}
}
When run, the splash screen shows correctly with the image, progress bar and text message area.
The SplashWindow class is called by the following method:
private void saveSeedDatabase(ObservableList<RefModel> docList) {
SplashWindow splash = new SplashWindow();
Task<Integer> saveTask = new Task<Integer>() {
#Override
protected Integer call() throws InterruptedException {
updateMessage("Saving references...");
int docsSaved = 0;
for (RefModel rm : docList) {
if (isCancelled()) {
updateMessage("Cancelled");
break;
}
updateMessage("Saving: " + rm.getTitle());
saveNewReference(rm);
docsSaved++;
updateProgress(docsSaved, docList.size());
}
updateMessage("Saved " + docsSaved + " references to database");
return docsSaved;
}
};
saveTask.setOnSucceeded((WorkerStateEvent t) -> {
splash.shutdown();
});
splash.bindMessageProperty(saveTask.messageProperty());
splash.bindProgressProperty(saveTask.progressProperty());
splash.show();
new Thread(saveTask).start();
try {
saveTask.get();
} catch (InterruptedException | ExecutionException ex) {
// Do nothing
}
}
When run, the splash screen is displayed but never shows the update messages and progress. It shows a wait cursor when the mouse if over the splash screen.
If the try/catch block at the end of the method is commented out, the main program attempts to proceed without waiting for the database to be written. In this case, the splash screen is hidden by the main program window, but does display the update messages as it works. From debugging this, it looks like everything is running on the correct thread -- the database stuff is on a worker thread, the SplashWindow stuff is on the FX event thread.
It seems clear that the call to saveTask.get() is blocking the UI thread, but I am not sure why.
#JewelSea has written a nice alternative that doesn't fit my program architecture very well. It would not be impossible to alter my program to work with his solution.
However, I don't understand why the get() call blocks the UI. What am I doing wrong.

According to the JavaDocs:
(get()) Waits if necessary for the computation to complete, and then retrieves its result.
Retrieving the computed value without blocking the GUI Thread would be done with getValue(). However: This method only returns a result, when the Task has successfully finished its work. That is why you should do this aysnc in the onSucceeded block.

Related

Use ScheduledExecutorService to update JavaFX elements

Currently I am making a program that reminds me when to water my plants, while also putting the weather into account. I would like to display the current temperature and humidity, and I have made code that does that well enough already. However, this code only works when manually running the method via a button press, and throws Exception in thread "pool-3-thread-1" java.lang.IllegalStateException: Not on FX application thread; currentThread = pool-3-thread-1 when I attempt to run it in a ScheduledExecutorService. From my understanding JavaFX does not allow other threads to edit JavaFX components without Platform.runLater, however I can't seem to find anything about Platform.runLater being combined with ScheduledExecutorService.
Here is my update method:
public void update() {
final Runnable updater = new Runnable() {
public void run() {
humidityLabel.setText("Humidity: " + Double.toString(Weather.getHumidity()) + "%");
humidityDialArm.setRotate(Weather.getHumidity() * 1.8);
tempLabel.setText("Temperature: " + Double.toString(Weather.getTemperature()) + "°F");
temperatureDialArm.setRotate(Weather.getTemperature()*1.5);
icon = Weather.getIcon();
conditionLabel.setText(Weather.getCondition());
}
};
final ScheduledFuture<?> updaterHandle = scheduler.scheduleAtFixedRate(updater, 10, 10, TimeUnit.MINUTES);
}
And here is my main method:
public static void main(String[] args) {
App app = new App();
launch();
app.update();
}
I found a similar problem here, however I haven't been able to find a way to get Platform.runLater to work well with the ScheduledExecutorService. I also found this on GitHub, however I can't tell what the fix for this problem was other than it was fixable. I also tried putting a while loop at main that would just constantly update it, but that just caused the program to hang and eventually crash. Even if it did work, that would also make it not runnable for long periods of time as the API I am using limits the amount of GET requests per day.
Use ScheduledService
The javafx.concurrent.ScheduledService class provides a way to repeatedly do an action and easily communicate with the FX thread. Here is an example:
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.scene.image.Image;
public class WeatherService extends ScheduledService<WeatherService.Result> {
#Override protected Task<Result> createTask() {
return new Task<>() {
#Override protected Result call() throws Exception {
// this is invoked on the background thread
return new Result(
Weather.getTemperature(),
Weather.getHumidity(),
Weather.getCondition(),
Weather.getIcon()
);
}
};
}
public record Result(double temperature, double humidity, String condition, Image icon) {}
}
Then where you use the service you'd add an on-succeeded handler to handle updating the UI:
service.setOnSucceeded(e -> {
var result = service.getValue();
// update UI (this handler is invoked on the UI thread)
});
To have it execute every 10 minutes, with an initial delay of 10 minutes, to match what you're doing with the ScheduledExecutorService, you would do:
service.setDelay(javafx.util.Duration.minutes(10));
service.setPeriod(javafx.util.Duration.minutes(10));
// you can define the thread pool used with 'service.setExecutor(theExecutor)'
When first configuring the service. You also need to maintain a strong reference to the service; if it gets garbage collected, then the task will not be rescheduled.
Use Platform#runLater(Runnable)
If you have to use ScheduledExecutorService for some reason, then you should run the code that updates the UI in a runLater call. Here's an example:
public void update() {
final Runnable updater =
() -> {
// get information on background thread
double humidity = Weather.getHumidity();
double temperature = Weather.getTemperature();
Image icon = Weather.getIcon();
String condition = Weather.getCondition();
// update UI on FX thread
Platform.runLater(
() -> {
humidityLabel.setText("Humidity: " + humidity + "%");
humidityDialArm.setRotate(humidity * 1.8);
tempLabel.setText("Temperature: " + temperature + "°F");
temperatureDialArm.setRotate(temperature * 1.5);
iconView.setImage(icon);
conditionLabel.setText(condition);
});
};
scheduler.scheduleAtFixedRate(updater, 10, 10, TimeUnit.MINUTES);
}

JavaFX show AnchorPane from Thread

I am trying to create the following concept: start a thread whenever a specific screen gets launched. The thread should receive a message which is called a "tag", which is not working yet so I got it hardcoded.
Then show an AnchorPane based on the validation of the tag: either the showError or showValid function. However, the application first runs the function and then shows the AnchorPane and the updated ListView.
I want to start the following thread whenever a specific screen launches.
public class RFIDThread extends Thread{
private static final Logger logger = Logger.getLogger(RFIDApplication.class);
/**
* The incoming data stream from the LLRP reader connection
*/
private DataInputStream inStream = null;
/**
* The socket for the connection to the LLRP Reader
*/
private Socket socket = null;
/**
* A queue to store incoming LLRP Messages
*/
private LinkedBlockingQueue<LLRPMessage> queue = null;
private String[] found_tags = new String[5];
private JSONArray valid_tags;
private TagsListController controller;
/**
* Thread for constant reading of the stream
*
* #param socket
* #param controller
* #param tags
* #param orderNumber
* #throws java.io.IOException
*/
public RFIDThread(Socket socket, TagsListController controller, JSONArray tags, String orderNumber) throws IOException {
this.socket = socket;
this.controller = controller;
this.queue = new LinkedBlockingQueue<LLRPMessage>();
try {
this.inStream = new DataInputStream(socket.getInputStream());
} catch (IOException e) {
logger.error("Cannot get input stream", e);
}
valid_tags = tags;
found_tags[0] = "aga9jrjahr";
found_tags[1] = "agahs4suj";
found_tags[2] = "a79gtvaTGBQG";
found_tags[3] = "at3anit08av9agq4";
//found_tags[4] = "4a05355d0000000000017cc0";
//start();
}
#Override
public void run()
{
super.run();
if (socket.isConnected()) {
for (String found_tag : found_tags) {
Integer index = valid_tags.indexOf(found_tag);
if (index > 0) {
Platform.runLater(() -> {
controller.showValid(found_tag);
});
} else {
Platform.runLater(() -> {
controller.showError(found_tag);
});
}
}
}
}
}
The thread should run functions: showError or showValid based on the tag it receives. Currently I have some hardcoded tags set-up which are all invalid so it should run the showError() function. This function: adds the tag to a ListView, sets the tag as text of a label, display the AnchorPane, sleep 1 second, hide the AnchorPane and then sleep 1 second. After this, the next tag must be processed.
/**
* Display red screen
* #param tag
*/
public void showError(String tag) {
this.found_tags_list.getItems().add(tag);
this.errorTag.setText(tag);
System.out.println(errorTag.getText());
this.errorPane.setVisible(true);
pause(1000);
this.validPane.setVisible(false);
pause(1000);
}
You didn't post the code for your pause() method, so I'm going to assume it does something like Thread.sleep(...) and handles the interrupted exception appropriately. I.e. I'm going to assume you have something like:
public void pause(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException exc) {
Thread.currentThread().interrupt();
}
}
The showError() method is being (explicitly) executed on the FX Application Thread. That thread is also responsible for rendering the UI. Consequently, the UI can't be redrawn while the showError() method is executing (because a single thread can't do two things at once: that's basically the definition of "thread").
So it's always an error to block the FX Application Thread, because it makes the UI unresponsive and prevents it from being drawn.
If you are already on the FX Application Thread, and want to schedule some code to execute in the future, you can do that with a PauseTransition. So instead of
this.errorPane.setVisible(true);
pause(1000);
this.validPane.setVisible(false);
you can do
this.errorPane.setVisible(true);
PauseTransition pause = new PauseTransition(Duration.millis(1000));
pause.setOnFinished(e -> this.validPane.setVisible(false));
pause.play();
The second pause in that method makes less sense. It simply pauses the FX Application Thread, and then the method exits, so there is nothing it is waiting for anyway.
If the idea is to make the background thread pause at that point, you should call pause() on the background thread. (Calling it on the FX Application Thread, obviously, won't make the background thread pause anyway.)
So I think your code should look like:
public class RFIDThread extends Thread {
// ...
#Override
public void run() {
super.run();
if (socket.isConnected()) {
for (String found_tag : found_tags) {
Integer index = valid_tags.indexOf(found_tag);
if (index > 0) {
Platform.runLater(() -> controller.showValid(found_tag));
} else {
Platform.runLater(() -> controller.showError(found_tag));
}
pause(2000);
}
}
}
}
Note that I'm guessing here the intention is for your background thread to pause for (approximately) one second after the pane you show is hidden again, which would mean it needs to pause for two seconds in total.
In the controller, you do
public void showError(String tag) {
this.found_tags_list.getItems().add(tag);
this.errorTag.setText(tag);
System.out.println(errorTag.getText());
this.errorPane.setVisible(true);
PauseTransition pause = new PauseTransition(Duration.millis(1000));
pause.setOnFinished(e -> this.validPane.setVisible(false));
pause.play();
}

Java Swing redraw delay

I'm writing a little private application at the moment which utilizes the Wolfram Alpha Java Bindings. The application's GUI is realized using Swing. My Wolfram Alpha Plugin has a function looking like this:
public void fire(String value) {
// This sets the field state to loading
_state = loading;
// This tells the controller to redraw the window
// When reading the state of the plugin it sets an
// animated gif at a JPanel. This should be displayed
// before the application is blocked by the Wolfram Alpha
// Request.
// updateInput does nothing more then call repaint after some
// other function calls. No special threading included.
getController().updateInput("");
// My wolfram request follows:
WAQuery query = _engine.createQuery();
query.setInput(value);
try {
// For educational purposes, print out the URL we are about to send:
System.out.println("Query URL:");
System.out.println(_engine.toURL(query));
System.out.println("");
// This sends the URL to the Wolfram|Alpha server, gets the XML result
// and parses it into an object hierarchy held by the WAQueryResult object.
WAQueryResult queryResult = _engine.performQuery(query);
if (queryResult.isError()) {
System.out.println("Query error");
System.out.println(" error code: " + queryResult.getErrorCode());
System.out.println(" error message: " + queryResult.getErrorMessage());
} else if (!queryResult.isSuccess()) {
System.out.println("Query was not understood; no results available.");
} else {
// Got a result.
System.out.println("Successful query. Pods follow:\n");
for (WAPod pod : queryResult.getPods()) {
if (!pod.isError()) {
System.out.println(pod.getTitle());
System.out.println("------------");
for (WASubpod subpod : pod.getSubpods()) {
for (Object element : subpod.getContents()) {
if (element instanceof WAPlainText) {
System.out.println(((WAPlainText) element).getText());
System.out.println("");
}
}
}
System.out.println("");
}
}
// We ignored many other types of Wolfram|Alpha output, such as warnings, assumptions, etc.
// These can be obtained by methods of WAQueryResult or objects deeper in the hierarchy.
}
} catch (WAException e) {
e.printStackTrace();
}
}
The Problem is that the request is started BEFORE the window is repainted. I have a request delay of about one second and THEN the loading animation is displayed. This animation should be displayed before the request is started.
What I tried so far:
Thread.sleep() in front of the Wolfram Query. But that would block the repaint too?! - same unwanted behavior
Put the request into a runnable - same unwanted behavior
Put the request into a class extended by Thread - same unwanted behavior
Combine both with Thread.sleep() - same unwanted behavior
What am I overseeing here?
I have a request delay of about one second and THEN the loading animation is displayed. This animation should be displayed before the request is started.
Never use Thread.sleep() in Swing application that sometime hangs the whole application as you stated it already in your post. You should use Swing Timer in that case.
Please have a look at How to Use Swing Timers
Sample code:
Timer timer = new Timer(1000, new ActionListener() {
#Override
public void actionPerformed(ActionEvent arg0) {
// start loading animation now
}
});
timer.setRepeats(false);
timer.start()
What I suggest is:
Use background threading, specifically a SwingWorker'
Do the long running code in a SwingWorker's doInBackground method.
Start your animation display before executing your SwingWorker.
Give the SwingWorker a PropertyChangeListener, and when the SwingWorker's state is done, then stop the animation.
Be sure that the animation does not tie up the Swing event thread either. Use a Swing Timer for it.
For example,
// start animation -- be sure to use a Swing Timer or
// other way to prevent tying up the Swing event thread
SwingWorker<Void, Void> myWorker = new SwingWorker<Void, Void>() {
protected Void doInBackground() throws Exception {
fire(someValue);
return null;
};
};
myWorker.addPropertyChangeListener(new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent pcEvt) {
if (pcEvt.getNewValue().equals(SwingWorker.StateValue.DONE)) {
// .... stop the animation here
}
}
});
myWorker.execute();

JProgressBar updating on one thread, not on another

I created a popup window to show progress of something, and it is working fine with my downloader, everything is being updated.
private void downloadFile(String link, String directory, String name) throws IOException {
task = new Downloader(link, directory, name);
task.start();
}
and in the Downloader class:
public void run() {
try {
while ((length = is.read(b)) != -1) {
downloaded += length;
bout.write(b, 0, length);
int percent = (int) ((downloaded * 100) / fileLength);
window.modify1("Downloading " + name + ".jar");
window.modify2((int) downloaded, "Progress: " + percent + "% [" + String.valueOf(downloaded).subSequence(0, String.valueOf(downloaded).length() - 1) + "kb/" + String.valueOf(fileLength).subSequence(0, String.valueOf(fileLength).length() - 1) + "kb]");
}
is.close();
bout.close();
window.exit();
Creator c = new Creator(directory, name);
c.create();
this.join();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
However, when I attempt to do almost the same in another thread, it does not work. Nothing in the popup window is being shown until the thread finishes.
LauncherThread t = new LauncherThread();
t.start();
try {
t.join();
} catch (InterruptedException e2) {
e2.printStackTrace();
}
and in the LauncherThread class:
public void run() {
window.modify1("Fetching data...");
window.modify2(0, "Progress: 0% [0/10]");
Main.trust();
window.modify2(1, "Progress: 10% [1/10]");
Main.bukkitVersions = Finder.findBukkitVersions();
window.modify2(2, "Progress: 20% [2/10]");
Main.spigotVersions = Finder.findSpigotVersions();
window.modify2(3, "Progress: 30% [3/10]");
Main.vanillaVersion = Finder.findVanillaVersion();
window.modify2(4, "Progress: 40% [4/10]");
Main.bukkitLinks = new String[3];
Main.bukkitLinks[0] = Finder.findDownloadLink("bukkit", "rb");
window.modify2(5, "Progress: 50% [5/10]");
Main.bukkitLinks[1] = Finder.findDownloadLink("bukkit", "beta");
window.modify2(6, "Progress: 60% [6/10]");
Main.bukkitLinks[2] = Finder.findDownloadLink("bukkit", "dev");
window.modify2(7, "Progress: 70% [7/10]");
Main.spigotLinks = new String[2];
Main.spigotLinks[0] = Finder.findDownloadLink("spigot", "lastStable");
window.modify2(8, "Progress: 80% [8/10]");
Main.spigotLinks[1] = Finder.findDownloadLink("spigot", "lastBuild");
window.modify2(9, "Progress: 90% [9/10]");
Main.vanillaLink = Finder.findDownloadLink("vanilla", null);
window.modify2(10, "Progress: 100% [10/10]");
window.exit();
}
I'm still quite new to Java so I apologize for my ignorance.
Edit: I encased the t.start() method inside a SwingUtilities.invokeLater(), and it works now. But the new problem is that the main class is no longer waiting for the LauncherThread to finish.
I went with the easy (and possibly bad) way. the initialize method is called in the LauncherThread after it finishes.
/**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
new Main();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* Create the application.
*/
public Main() {
launch();
}
/**
* Run launcher.
*/
private void launch() {
LauncherThread t = new LauncherThread(this);
t.start();
}
I pass the Main instance to the LauncherThread class through its constructor, so I can call the initialize method from there without having to use static access.
main.initialize();
You are in the process of running into serious problems and I'm afraid your current solution doesn't solve the problem but just hides it.
The root problem is that Swing (as most comparable libs) isn't thread safe. So there must only one thread that accesses Swing components. That thread isn't arbitrary, but a special thread named EDT (Event Dispatch Thread).
The consequences of that are:
The initialization of your UI must be wrapped in a call to SwingUtilities.invokeLater or SwingUtilities.invokeAndWait
If you do work in a separate thread that code must not touch your Progressbar directly. Instead it must dispatch updates to the progress bar through the two methods mentioned above.
If you don't do this right now and your code seams to work, it does not mean it will work tomorrow, or on a different machine, or when the weather changes or the government in china changes.
If you want, one thread to wait on another you should look into stuff like CountDownLatch just make sure you don't block the EDT, because when it is blocked nothing will get painted in the UI, so the application seems to be dead for the user. Again executing some code through invokeLater might solve that problem.
When i have to update Swing components from another Thread, then i'm doing it in
SwingUtilites.invokeLater(new Runnable() {
//set new values
});
from Javadoc ot method:
Causes doRun.run() to be executed asynchronously on the AWT event dispatching thread. This will happen after all pending AWT events have been processed. This method should be used when an application thread needs to update the GUI.
One more thing,.
You might consider doing your application via SwingWorker, those object are friendly with EDT. But remember, that they are executed in limited ThreadPool. I tried once to launch 50+ swing workers to update components of panel, however it worked only for first 10 of them. Probably could be changed to have them all running, but I've changed my approach.

JavaFx Progress Indicator freezes when I run another thread

So I have been trying to implement a progress indicator with no luck. I am not sure I understand managing threads with JavaFx very well, despite having read a bit about the Platform.RunLater and Tasks. So here is my use case.
My program allows users to connect to a database and look at some of the schemas and other objects in the database. Sometimes connecting to a large database and pulling up all its tables and info takes a while, so I would like to show a progress indicator. I am not trying to update the progress at all I would just like to make the progress indicator visible at a value of -1 while the process is running to pull everything from the database. Ideally I will have a progress indicator loaded in from an FXML file invisible. When I start the process of pulling info from the database I would like to make it visible.
When trying to make my progress visible it never showed up, so I decide to start out having it visible and making it invisible, just to see what happens. The progress indicator rotated nicely when I opened the program up, but as soon as I try to connect to the database it stopped rotating and just froze. I assume this is what happens when I try to make it visible too which is why it was never showing up.
The following is my current code, I would appreciate any detailed help with explanations so I can understand what is going on. Thanks
from the method that is doing most of the work.
//make progress indicator visible
pi.setVisible(true);
// separate non-FX thread
ExtractorThread t = new ExtractorThread();
t.setCp(cp);
t.start();
//Wait until the thread is done
try{
t.join();
}
catch(Exception e){
e.printStackTrace();
}
//Retrieve the dbextractor from the thread
DbExtractor dbe = t.getDbe();
//move on to the next page in the application
this.caster.goToDataSource(c, cp, dbe);
The ExtractorThread which does the work.
private class ExtractorThread extends Thread{
private ConnectionProperties cp;
private DbExtractor dbe;
public void run() {
dbe = new DbExtractor(cp);
try {
dbe.extract();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public DbExtractor getDbe() {
return dbe;
}
public void setCp(ConnectionProperties cp) {
this.cp = cp;
}
}
If I am supposed to use the Platform.RunLater I am not sure where to use it or why. Any help would be appreciated, thanks!
Use the javafx.concurrent API. Extend Task instead of Thread:
private class ExtractorThread extends Task<DbExtractor>{
private ConnectionProperties cp;
public DbExtractor call() throws Exception {
dbe = new DbExtractor(cp);
dbe.extract();
return dbe;
}
public void setCp(ConnectionProperties cp) {
this.cp = cp;
}
}
Then do:
//make progress indicator visible
pi.setVisible(true);
// separate non-FX thread
final ExtractorThread t = new ExtractorThread();
t.setCp(cp);
t.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
public void handle(WorkerStateEvent event) {
DbExtractor dbExtractor = t.getValue();
this.caster.goToDataSource(c, cp, dbe);
}
});
t.setOnFailed(...); // similarly, to handle exceptions
new Thread(t).start();
I don't code JavaFX, and so I can't give you chapter and verse, but this line:
t.join();
will block the calling code until the background thread is through. Don't do this. Instead use some type of listener to get notified when the background thread finishes. If this were Swing, I'd use a PropertyChangeListener added to a SwingWorker to notify me when the background thread was done. I think that you can still use a PropertyChangeListener to do a similar thing with with JavaFX, but I cannot tell you if this would represent the canonical solution.
Also, don't extend Thread but instead implement Runnable. This won't fix your problem but is basic Java common sense.

Categories

Resources