Issue
I have a java application running java http server. This java application should run continuously. I don't want to open javafx gui when the program is run for the first time.
As I said, the application should run continuously. The user should be able to open the user interface at any time by clicking on the system tray icon. Or should be able to close the cross-button in the interface.
I used Platform.setImplicitExit (false)
to not stop the java application from pressing the cross-button on the interface.
If the user wants to see the screen again, I want to re-render the screen by pressing the system tray.
I want to show and hide the user interface without closing the java program. What is best practice I'm waiting for your help.
Related codes are below.
public class Gui extends Application {
@Override
public void start(Stage stage) throws Exception {
Platform.setImplicitExit(false);
Platform.runLater(new Runnable() {
@Override
public void run() {
try {
new Gui().start(new Stage());
} catch (Exception e) {
e.printStackTrace();
}
}
});
Scene scene = new Scene(new StackPane());
LoginManager loginManager = new LoginManager(scene);
loginManager.showLoginScreen();
stage.setScene(scene);
stage.show();
// stage.setOnCloseRequest(e -> Platform.exit());
}
}
Main class
public static void main(String[] args) throws IOException, Exception, FileNotFoundException {
ServerSocket ss = null;
try {
ss = new ServerSocket(9090);
if (ss != null) {
ss.close();
}
} catch (BindException e) {
System.out.println("Sikke Node Server is already running.");
System.exit(0);
}
launchh();
}
Method in main Class
private static void createAndShowGUI() {
if (SystemTray.isSupported()) {
final PopupMenu popup = new PopupMenu();
final TrayIcon trayIcon = new TrayIcon(createImage("/sikke24.gif", "Sikke Node "), "Sikke Node Server",
popup);
trayIcon.setImageAutoSize(true);
final SystemTray tray = SystemTray.getSystemTray();
final int port = Integer.parseInt(_System.getConfig("rpcport").get(0));
// Create a popup menu components
MenuItem aboutItem = new MenuItem("About");
Menu displayMenu = new Menu("Display");
MenuItem infoItem = new MenuItem("Info");
MenuItem noneItem = new MenuItem("None");
MenuItem exitItem = new MenuItem("Exit Sikke Node Server");
// Add components to popup menu
popup.add(aboutItem);
popup.addSeparator();
popup.add(displayMenu);
displayMenu.add(infoItem);
displayMenu.add(noneItem);
popup.add(exitItem);
trayIcon.setPopupMenu(popup);
try {
tray.add(trayIcon);
} catch (AWTException e) {
System.out.println("Sikke Node Icon could not be added.");
return;
}
trayIcon.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
/*
* JOptionPane.showMessageDialog(null,
* "Server started successfully. The server works on port number:" + port);
*/
Application.launch(Gui.class, "");
}
});
aboutItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(null,
"Server started successfully. The server works on port number:" + port);
}
});
ActionListener listener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
MenuItem item = (MenuItem) e.getSource();
System.out.println(item.getLabel());
if ("Error".equals(item.getLabel())) {
trayIcon.displayMessage("Sikke Node Server", "This is an error message",
TrayIcon.MessageType.ERROR);
} else if ("Warning".equals(item.getLabel())) {
trayIcon.displayMessage("Sikke Node Server", "This is a warning message",
TrayIcon.MessageType.WARNING);
} else if ("Info".equals(item.getLabel())) {
// GUI runs
trayIcon.displayMessage("Sikke Node Server", "This is an info message",
TrayIcon.MessageType.INFO);
} else if ("None".equals(item.getLabel())) {
trayIcon.displayMessage("Sikke Node Server", "This is an ordinary message",
TrayIcon.MessageType.NONE);
}
}
};
trayIcon.displayMessage("Sikke Node Server", "Sikke Node Server started successfully on port : " + port,
TrayIcon.MessageType.INFO);
infoItem.addActionListener(listener);
noneItem.addActionListener(listener);
exitItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
tray.remove(trayIcon);
System.exit(0);
}
});
}
}
Watch out here
Application.launch(Gui.class, "");
TrayIcon ActionListener updated
trayIcon.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 1) {
if (Platform.isFxApplicationThread()) {
Platform.runLater(new Runnable() {
@Override
public void run() {
try {
new Gui().start(new Stage());
} catch (Exception e) {
e.printStackTrace();
}
}
});
} else {
Application.launch(Gui.class, "");
}
}
}
});
Solution
Some Observations
First off, in your updated listener:
trayIcon.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 1) {
if (Platform.isFxApplicationThread()) {
Platform.runLater(new Runnable() {
@Override public void run() { /* OMITTED FOR BREVITY */ }
});
} else {
Application.launch(Gui.class, "");
}
}
}
});
You check Platform.isFxApplicationThread
and, if true, then call Platform.runLater
. The call to Platform.runLater
schedules the action to execute on the JavaFX Application Thread; if you're already on that thread there's no need (typically) to call Platform.runLater
. Of course, isFxApplicationThread
will never return true because SystemTray
is part of AWT and will invoke the listener on the AWT related thread. This means the else
branch will always be called, which is a problem since you cannot call Application.launch
more than once in a single JVM instance; doing so results in an IllegalStateException
being thrown.
Also, in your start
method:
@Override
public void start(Stage stage) throws Exception {
Platform.setImplicitExit(false);
Platform.runLater(new Runnable() {
@Override
public void run() {
try {
new Gui().start(new Stage());
} catch (Exception e) {
e.printStackTrace();
}
}
});
/* SOME CODE OMITTED FOR BREVITY */
}
That Platform.runLater
call should be causing a "loop". When you call start
you schedule the Runnable
to run at some later time via the Platform.runLater
call. Inside this Runnable
you call new Gui().start(new Stage())
. What that does is call start
again (on a new instance of Gui
), which will call Platform.runLater
again, which will call new Gui().start(new Stage())
again, which will call start
again, which... you get the idea.
Note that Application.launch(Gui.class)
will create an instance of Gui
and call start
with the primary Stage
for you. But as mentioned above, launch
can only be called once. Conceptually, the Application
subclass represents the entire application. There should ideally only ever be one instance of that class.
Small Example Using SystemTray
Here is a small example using SystemTray
to (re)open a JavaFX window. The window is not displayed until the user clicks (double-click, at least on Windows) the tray icon.
import java.awt.AWTException;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.image.BufferedImage;
import java.util.function.Predicate;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
public class Main extends Application {
private Stage primaryStage;
private boolean iconAdded;
@Override
public void start(Stage primaryStage) throws AWTException {
if (SystemTray.isSupported()) {
installSystemTray();
Platform.setImplicitExit(false);
StackPane root = new StackPane(new Label("Hello, World!"));
primaryStage.setScene(new Scene(root, 500, 300));
primaryStage.setTitle("JavaFX Application");
primaryStage.setOnCloseRequest(this::promptUserForDesiredAction);
this.primaryStage = primaryStage;
} else {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText(null);
alert.setContentText("SystemTray is not supported. Will exit application.");
alert.showAndWait();
Platform.exit();
}
}
@Override
public void stop() {
if (iconAdded) {
SystemTray tray = SystemTray.getSystemTray();
for (TrayIcon icon : tray.getTrayIcons()) {
tray.remove(icon);
}
}
}
private void promptUserForDesiredAction(WindowEvent event) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.initOwner((Window) event.getSource());
alert.setTitle("Choose Action");
alert.setHeaderText(null);
alert.setContentText("Would you like to exit or hide the application?");
// Use custom ButtonTypes to give more meaningful options
// than, for instance, OK and CANCEL
ButtonType exit = new ButtonType("Exit");
ButtonType hide = new ButtonType("Hide");
alert.getDialogPane().getButtonTypes().setAll(exit, hide);
alert.showAndWait().filter(Predicate.isEqual(exit)).ifPresent(unused -> Platform.exit());
}
private void installSystemTray() throws AWTException {
TrayIcon icon = new TrayIcon(createSystemTrayIconImage(), "Show JavaFX Application");
// On Windows 10, this listener is invoked on a double-click
icon.addActionListener(e -> Platform.runLater(() -> {
if (primaryStage.isShowing()) {
primaryStage.requestFocus();
} else {
primaryStage.show();
}
}));
SystemTray.getSystemTray().add(icon);
iconAdded = true;
}
// Creates a simple red circle as the TrayIcon image. This is here
// to avoid needing an image resource for the example.
private BufferedImage createSystemTrayIconImage() {
Circle circle = new Circle(6.0, Color.FIREBRICK);
Scene scene = new Scene(new Group(circle), Color.TRANSPARENT);
return SwingFXUtils.fromFXImage(circle.snapshot(null, null), null);
}
}
About Example
In my example I keep a strong reference to the Stage
which I show when the ActionListener
added to the TrayIcon
is invoked. Notice how in the ActionListener
I use Platform.runLater
. For every listener you add to the SystemTray
related objects (e.g. java.awt.MenuItem
), wrap any code that will interact with JavaFX objects in a Platform.runLater
call.
Now, my example launches the JavaFX runtime first and then adds the TrayIcon
. Not only is the JavaFX runtime launched immediately but I also pre-create the scene-graph and store a strong reference to it. This can be a lot of unnecessary overhead and memory consumption. As your application is an HTTP server that can run without the JavaFX runtime there are some optimizations you can make.
Don't store a strong reference to the
Stage
once closed, allowing it to be garbage collected. Possible options are:Immediately remove the reference when the
Stage
is closed. This will require you to recreate the scene-graph every time.Remove the reference some arbitrary time after the
Stage
has been closed. This would be done with some kind of timer (e.g.PauseTransition
orTimeline
) that resets when theStage
has been reopened before the time elapsed.
When the user requests the GUI you would, when necessary, (re)create the scene-graph and (re)initialize it with your model. Don't forget any necessary cleanup, such as removing listeners observing your model, when disposing the scene-graph; any strong references anywhere will keep the object(s) in memory, leading to a memory leak.
Lazily launch the JavaFX runtime.
Don't have any server initialization/running logic in your
Application
subclass. Particularly, don't put yourmain
method in that class as it will indirectly launch the JavaFX runtime.On the first request to show the GUI use
Application.launch
.Note: I believe the call to
launch
must be put on a separate thread. The thread that callslaunch
does not return until the JavaFX runtime exits. Since theTrayIcon
listeners are called on the AWT thread this will lead to that thread being blocked, which would not be good.On subsequent requests just display the window, recreating it if necessary. How you go about this is dependent on your architecture. One option is to make your
Application
class a sort of lazy singleton that gets set viaApplication.launch
; you'd get a reference and call a method to show the window (on the FX thread).
Both options will keep the JavaFX runtime alive, once launched, until the entire application exits. You could technically exit the JavaFX runtime independently but, as mentioned before, calling Application.launch
more than once is an error; if you do this you won't be able to show the GUI again until the entire application is restarted. If you really want to allow the JavaFX side of the application to exit and be relaunched you can use a separate process. However, using a separate process is likely non-trivial.
Answered By - Slaw
Answer Checked By - Pedro (JavaFixing Volunteer)