0

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, "");
                    }
                }
            }
        });
Baltazar
  • 65
  • 1
  • 11
  • 2
    My question was who gave minus points by who and by what reason.Is it because you don't know the answer? – Baltazar Feb 01 '19 at 13:37
  • Don't call `Application.launch` more than once in a single JVM; the application is already launched, you just need to display the window. Keep a reference to the `Stage` and call `Platform.runLater(stage::show)` inside the appropriate `ActionListener`. – Slaw Feb 01 '19 at 17:35
  • Also, why have `new Gui().start(new Stage())`? You're already in an instance of `Gui` and already have access to a `Stage`, both provided by the JavaFX runtime. Besides, that code will form a "loop" that constantly creates more `Gui` objects and calls `start` on them. – Slaw Feb 01 '19 at 17:37
  • I am not opening the GUI directly when the java application is running. The user who wants to open the user interface should click the tray icon. I changed the actionListener part of the line icon. But once again I wanted to open the GUI for the first time. Then the tray icon will be locked when you want to close the interface and open it again. I update the action listener part of the tray icon above. Where am I doing wrong. – Baltazar Feb 04 '19 at 07:27

1 Answers1

4

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 or Timeline) that resets when the Stage 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 your main 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 calls launch does not return until the JavaFX runtime exits. Since the TrayIcon 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 via Application.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.

Slaw
  • 37,820
  • 8
  • 53
  • 80