65.9K
CodeProject 正在变化。 阅读更多。
Home

在跨平台 Java Swing 应用程序中设置 Mac 菜单栏和 Dock

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2020 年 2 月 16 日

CPOL

4分钟阅读

viewsIcon

12695

downloadIcon

221

一个类,可以在应用程序启动时轻松地以编程方式设置 Mac 菜单栏和 Dock 图标。

引言

Java 应用程序开发的一个吸引人的特性是“一次编写,随处运行”的概念。由于我目前使用 Java 进行应用程序开发,所以我决定看看我的应用程序在 Mac 上的外观如何。

网上有很多资源详细介绍了让 Java 应用程序在 Apple 的操作系统上运行时看起来和行为都像原生 Mac 应用程序应该采取的步骤。甚至在很多年前,可以通过在源代码中轻松配置一个应用程序,使其名称出现在菜单栏中。 可惜的是,今天的情况并非如此。

确保应用程序名称出现在菜单栏和 Dock 上的图标的一种可靠方法是打开终端并使用以下命令启动 jar

java -Xdock:name= <applicationName> -Xdock:icon= <iconPath> -jar <jar file path>

然而,这样做违背了首先制作可执行 jar 的目的,并且我想为我的用户以及我懒惰的自己保持简单。 如果我的应用程序在 Mac 上启动时,在完全加载之前使用这些参数重新启动自己会怎么样? 这真是太好了!

OSXHelper 类

我编写了一个小类,可以非常容易地确保 Java 应用程序名称出现在 Mac 菜单栏中,并且其图标出现在 Dock 中。 只需将该类添加到您的项目中并按如下方式使用它

private static final String APPLICATION_NAME = "Menu bar & dock demo";
private static final String APPLICATION_ICON = "/resources/app-icon.png";

/**
 * Launch the application.
 */
public static void main(String[] args) {
     if(OSXHelper.IS_MAC){
          // These calls must come before any AWT or Swing code is called,
          // otherwise the Mac menu bar will use the class name as the application name.
          System.setProperty("apple.laf.useScreenMenuBar", "true");
          OSXHelper.setMacMenuAboutNameAndDockIcon(args, APPLICATION_NAME, APPLICATION_ICON);
     }
     
     // Ok proceed with launch
     System.setProperty("java.net.preferIPv4Stack", "true");
	
     EventQueue.invokeLater(new Runnable() {
     public void run() {
               try {
                    Demo window = new Demo();
                    window.frame.setVisible(true);
               } catch (Exception e) {
                    e.printStackTrace()
               }
          }
     });
}

工作原理

该类包含两个静态字段和三个静态方法。 就本文而言,我将只关注 setMacMenuAboutNameAndDockIcon(),这是奇迹发生的地方。

static protected void setMacMenuAboutNameAndDockIcon(final String[] applicationArgs, 
      final String applicationName, final String applicationIcon){
   try {
      String iconPath = null;
      String[] launch = {""};
      boolean XdockSet = false;
      for(String arg : applicationArgs) if(XdockSet = arg.equals("-XdockSet")) break;

      if(!XdockSet){
         if(null != applicationIcon){
            iconPath = exportResource(new Object(){}.getClass().getEnclosingClass(), 
                  applicationIcon);
         }
         if(null != iconPath){
            launch = new String[] { "java", "-Xdock:name=" + applicationName, 
            "-Xdock:icon=" + iconPath, "-jar", 
            THIS_JAR_FILE.getAbsolutePath(), "-XdockSet" };
         } else {
            launch = new String[] { "java","-Xdock:name=" + applicationName, 
                  "-jar", THIS_JAR_FILE.getAbsolutePath(), "-XdockSet" };
         }
         String[] command = new String[launch.length + applicationArgs.length];
         System.arraycopy(launch, 0, command, 0, launch.length);
         System.arraycopy(applicationArgs, 0, command, launch.length, applicationArgs.length);
         Runtime.getRuntime().exec(command);
         Runtime.getRuntime().exit(0);
      } else { // XdocSet so remove temporary copy of icon.            
         // We land here after the Jar has been relaunched 
         // with our new command line arguments.  
         // One would think that an attempt to delete the temporary icon should be safe here, 
         // however the call to "Runtime.getRuntime().exec(commands)" above 
         // does not return until this section of code (run in another process) is completed. 
         // Therefore, the temporary icon is deleted before it can be loaded.
         // The call to invokeLater() allows the JVM to finish loading this instance before
         // the deletion of the temporary icon.
         SwingUtilities.invokeLater(new Runnable() { 
            public void run() {
               if(null != applicationIcon){
                  try {
                     // exportResource checks to see if the resource 
                     // (in this case the temporary icon) exists and will
                     // not overwrite it if present.  It will however 
                     // return the path to the existing resource.
                     String iconPath = exportResource(new Object(){}
                     .getClass().getEnclosingClass(), applicationIcon);
                     Thread.sleep(1000);
                     new File(iconPath).delete();
                  } catch (Exception e) { e.printStackTrace(); }
               }
            }
         });   
      }
   } catch (Exception e) {
      e.printStackTrace();
   }
}

在查看这段代码时; 首先要注意的是,此方法的行为就像它是递归方法一样,虽然它不直接调用自身,但它执行一种进程间递归,因此需要一个停止条件。 这是通过在方法代码的前半部分完成后附加到 jar 文件参数列表的标志来实现的。 因此,检查此标志是该过程的第一步。

第一次调用此方法时,将找不到 -XdockSet 标志(更确切地说是不应该找到。 如果您使用此类,请不要将其用作应用程序的参数。)因此执行会继续到方法的上半部分,在该部分构建并执行重新启动命令。

为了具有一定的灵活性; 设置 Dock 图标是可选的。 如果为 applicationIcon 参数提供 null,则 Java 启动命令字符串将不包含 -Xdock:icon= 参数和参数。 因此,Dock 将显示默认的 Java 咖啡杯。 但是,如果您想在 Dock 中显示应用程序的图标; 将资源从 jar 中导出是必要的,以便在重新启动期间 Java 运行时可以读取它。 对 exportResource() 的调用从可执行 jar 中提取资源到 jar 所在的文件夹,并返回提取资源的路径,在本例中为我们应用程序的图标。 然后将其包含在 Java 启动命令字符串中。 最后,我们将 -XdockSet 标志作为传递给 jar 文件的第一个参数。

然后,必须将最初传递给 jar 文件的任何参数附加到启动命令,这样我们在重新启动应用程序时就不会丢失它们。 然后,生成的启动命令用于在新进程中启动我们的应用程序,然后再退出当前进程。

第二次调用此方法时,将找到 -XdockSet 标志,因此执行会移动到方法的后半部分,在该部分进行清理。 在这种情况下,我想在 JVM 将临时图标资源加载到 Dock 之后,删除从 jar 中复制出来的临时图标资源。 代码中的注释解释了在方法完成后调用 File.delete() 的必要性。

最终评论

演示和源代码是在 Windows PC 上创建的,并在 Mac 上进行了测试。

历史

  • 2020 年 2 月 16 日:版本 1.0.0.0
© . All rights reserved.