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

How to re-run the user interface written with javafx in system tray when java jar application is running

我有一个 java 应用 运行ning java http 服务器。此 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());

}
}

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

主要方法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);
            }
        });
    }
}

注意这里

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

TrayIcon ActionListener 已更新

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如果为真,则调用 Platform.runLater。对 Platform.runLater 的调用安排了在 JavaFX 应用程序线程 上执行的操作;如果您已经在该线程上,则无需(通常)调用 Platform.runLater。当然,isFxApplicationThread 永远不会 return 为真,因为 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 调用应该导致 "loop"。当您调用 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 子class 代表 整个应用程序。理想情况下应该只有一个 class.

实例

使用SystemTray

的小例子

这是一个使用 SystemTray 来(重新)打开 JavaFX window 的小示例。 window 直到用户单击(双击,至少在 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 运行time ,然后添加 TrayIcon。不仅 JavaFX 运行time 立即启动,而且我还预先创建了场景图并存储了对它的强引用。这可能会产生很多不必要的开销和内存消耗。由于您的应用程序是一个 HTTP 服务器,可以 运行 而无需 JavaFX 运行time,因此您可以进行一些优化。

  • 一旦关闭,就不要存储对 Stage 的强引用,从而使其被垃圾回收。可能的选项是:

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

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

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

  • 延迟启动 JavaFX 运行时间。

    • 在您的 Application 子 class 中没有任何服务器 initialization/running 逻辑。特别是,不要将 main 方法放在 class 中,因为它会 .

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

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

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

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