如何在 java jar 应用程序运行时在系统托盘中重新运行用 javafx 编写的用户界面



我有一个运行java http服务器的java应用程序。此 Java 应用程序应连续运行。我不想在程序第一次运行时打开javafx gui。

正如我所说,应用程序应该连续运行。用户应该能够通过单击系统托盘图标随时打开用户界面。 或者应该能够关闭界面中的十字按钮。

我使用Platform.setImplicitExit (false)来不阻止 java 应用程序按下界面上的十字按钮。

如果用户想再次看到屏幕,我想通过按系统托盘重新渲染屏幕。

我想在不关闭 java 程序的情况下显示和隐藏用户界面。 什么是最佳实践 我在等你的帮助。

相关代码如下。

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());
}
}

主类

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();
}

主类中的方法

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);
}
});
}
}

注意这里

Application.launch(Gui.class, "");

托盘图标操作侦听器已更新

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, "");
}
}
}
});

一些观察

首先,在更新的侦听器中:

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, "");
}
}
}
});

您检查Platform.isFxApplicationThread如果为 true,则调用Platform.runLater。对Platform.runLater的调用调度要在JavaFX 应用程序线程上执行的操作;如果您已经在该线程上,则无需(通常)调用Platform.runLater。当然,isFxApplicationThread永远不会返回 true,因为SystemTray是 AWT 的一部分,并且会在 AWT 相关线程上调用侦听器。这意味着将始终调用else分支,这是一个问题,因为您不能在单个 JVM 实例中多次调用Application.launch;这样做会导致抛出IllegalStateException

此外,在您的start方法中:

@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 */
}

Platform.runLater调用应该会导致"循环"。当您调用start时,您可以通过Platform.runLater调用安排Runnable稍后运行。在这个Runnable里面,你称之为new Gui().start(new Stage()).它的作用是再次调用start(在Gui的新实例上),它将再次调用Platform.runLater,这将再次调用new Gui().start(new Stage()),这将再次调用start,哪个...你明白了。

请注意,Application.launch(Gui.class)将创建Gui的实例,并使用主Stage调用start但如上所述,launch只能调用一次。从概念上讲,Application子类表示整个应用程序。理想情况下,该类应该只有一个实例。


使用SystemTray的小示例

下面是一个使用SystemTray打开 JavaFX 窗口的小示例。在用户单击(双击,至少在 Windows 上)托盘图标之前,不会显示该窗口。

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);
}
}

关于示例

在我的示例中,我保留了对Stage的强烈引用,当调用添加到TrayIconActionListener时,我显示了该。请注意,在ActionListener中我如何使用Platform.runLater.对于您添加到SystemTray相关对象的每个侦听器(例如java.awt.MenuItem),将任何将与 JavaFX 对象交互的代码包装在Platform.runLater调用中。

现在,我的示例首先启动 JavaFX 运行时,然后添加TrayIcon。JavaFX 运行时不仅会立即启动,而且我还预先创建了场景图并存储了对它的强引用。这可能是大量不必要的开销和内存消耗。由于您的应用程序是一个可以在没有 JavaFX 运行时的情况下运行的 HTTP 服务器,因此您可以进行一些优化。

  • 关闭后,不要存储对Stage的强引用,允许对其进行垃圾回收。可能的选项包括:

    关闭
    • Stage时立即移除参照。这将要求您每次都重新创建场景图。

    • 关闭Stage一段时间后任意删除引用。这将通过某种计时器完成(例如PauseTransitionTimeline),当Stage在经过的时间之前重新打开时重置。

    当用户请求GUI时,您将在必要时(重新)创建场景图并使用模型(重新)初始化它。在处理场景图时,不要忘记任何必要的清理,例如删除观察模型的侦听器;任何位置的任何强引用都会将对象保留在内存中,从而导致内存泄漏。

  • 懒洋洋地启动 JavaFX 运行时。

    • Application子类中没有任何服务器初始化/运行逻辑。特别是,不要将main方法放在该类中,因为它会间接启动 JavaFX 运行时。

    • 在第一个请求显示 GUI 时,使用Application.launch.

      注意:我认为对launch的调用必须放在单独的线程上。在 JavaFX 运行时退出之前,调用launch的线程不会返回。由于 AWT 线程上调用了TrayIcon侦听器,这将导致该线程被阻塞,这并不好。

    • 在后续请求中,只需显示窗口,必要时重新创建它。如何做到这一点取决于您的架构。一种选择是使您的Application类成为一种通过Application.launch设置的懒惰单例;您将获得一个引用并调用一个方法来显示窗口(在 FX 线程上)。

这两个选项都将使 JavaFX 运行时在启动后保持活动状态,直到整个应用程序退出。从技术上讲,您可以独立退出 JavaFX 运行时,但如前所述,多次调用Application.launch是一个错误;如果这样做,您将无法再次显示 GUI,直到重新启动整个应用程序。如果确实要允许应用程序的 JavaFX 端退出并重新启动,则可以使用单独的进程。但是,使用单独的过程可能并非易事。

最新更新