使 OS X 屏幕 JMenuBar 跨窗口一致工作的最佳方法是啥?

Posted

技术标签:

【中文标题】使 OS X 屏幕 JMenuBar 跨窗口一致工作的最佳方法是啥?【英文标题】:What is the best way to make an OS X screen JMenuBar work consistently across windows?使 OS X 屏幕 JMenuBar 跨窗口一致工作的最佳方法是什么? 【发布时间】:2013-04-23 17:48:52 【问题描述】:

我有一个带有 Swing 用户界面的跨平台 Java 应用程序。在 OS X 上,应用程序 uses the screen menu bar 以获得更原生的用户体验。

通常,应用程序为每个文档创建一个JFrame。屏幕菜单栏必须在所有这些窗口中保持一致。我尝试了几种方法,发现只有一种一致且高效的解决方案,它足够但并不完美。我发布此问题以防其他人有更好的方法,并希望此信息对其他人有所帮助。

一些不起作用的方法:

将同一个菜单栏附加到多个窗口

我尝试将相同的JMenuBar 添加到多个JFrame 实例,但Swing only supports a JMenuBar being attached to a single JFrame at a time,即使作为屏幕菜单栏。

我还使用 AWT MenuBar 而不是 JMenuBar 进行了测试,但同样的现象发生了。而且MenuBarJMenuBar 相比有很多限制(例如,没有图标),所以让我们继续我们想要JMenuBar 的要求。

克隆菜单栏

一种常见的解决方案是为每个新的JFrame 创建一个JMenuBar 的副本。然而,这至少存在两个问题。首先,您必须保持菜单栏同步。虽然您可以使用侦听器来执行此操作,但仅处理 OS X 平台就需要很多额外的代码。然而,第二个也是更严重的问题是性能:如果您有一个包含数百个菜单项的复杂菜单栏,那么克隆菜单栏会非常慢。我们发现这种方法会使新窗口的出现延迟几秒钟!

使用默认菜单栏

在Java for OS X v10.6 Update 1 and 10.5 Update 6:Application.setDefaultMenuBar(JMenuBar) 中向 Apple 的 Java 库添加了一个新方法。

此方法的目的是在没有JFrame 处于活动状态时提供一个菜单栏,但它也会在没有JMenuBar 自己的JFrame 处于活动状态时显示默认菜单栏。

但是,setDefaultMenuBar 功能存在几个主要问题:

    Accelerators do not work。我通过handling all key presses ourselves 在我们的应用程序中避免了这个问题,但仍然很不幸。 截至 2012 年 12 月,setDefaultMenuBar was still not available on Java7。我们显然希望避免使用已弃用或不受支持的 API。 最关键的是,打电话给setDefaultMenuBarprevents the JVM from shutting down properly。即使随后调用 setDefaultMenuBar(null) 也不会释放必要的资源。

简而言之,setDefaultMenuBar 似乎根本不是一种安全可靠的方法。

所以,问题是:实现一致屏幕JMenuBar 的最可靠、高性能和兼容(跨 OS X 版本)的方法是什么?

【问题讨论】:

保持菜单同步是什么意思?我使用的技术是每次都创建一个全新的副本,我还没有遇到这个问题,所以我很好奇我可能没有注意到什么。 @Trejkaz 对于小菜单,复制可能没问题。但是,当您有许多菜单项时,为每个新的JFrame 创建一个新副本会产生大量的内存和时间开销。正如我上面所说,树中有大约 500 个菜单项,新 JFrames 的出现会延迟几秒钟!至于同步问题,如果您的菜单结构发生变化,这很重要: A)您添加了一个新菜单项; B)您删除了一个菜单项; C)你重命名一个菜单项; D)您使用JCheckBoxMenuItemJRadioButtonMenuItem,因为它们具有关联的状态,可能需要在不同的JFrames 之间保持同步。 我想知道您是否在谈论复选框和单选按钮是的。在这些情况下,您将在两个菜单之间共享 ButtonModel,以便 Swing 本身已经使它们保持同步。我想我们还没有达到 500 个菜单项,但我也不会称其为小。 100~200?如果您已经在动作映射中拥有 Action 对象并且只是在查找它们,则不会花费太长时间。话虽如此,这个问题似乎终于得到了解决,所以无论如何都可以使用默认菜单栏。 @Trejkaz “这个问题终于得到解决”是什么意思?你的意思是Java 7现在支持setDefaultMenuBar?您有相关问题或文章的链接吗? bugs.java.com/view_bug.do?bug_id=8022667 说在 7u60 中已修复。我不知道它什么时候出来,但现在可以抢先体验了。修复的提交大约是半年前... 【参考方案1】:

我发现效果很好的解决方案是通过向应用程序的每个窗口添加WindowListener 来侦听windowActivated 事件。然后,将新激活的窗口的JMenuBar 设置为我们要显示的唯一一个菜单栏。

这是example:

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.WindowConstants;

/**
 * On OS X, with a screen menu bar, you can "hot-swap" a JMenuBar between
 * multiple JFrames when each is activated. However, there is a flash each
 * time the active window changes, where the menu bar disappears momentarily.
 * But it is a small price to pay to be able to reuse the same menu bar!
 */
public class HotSwapJMenuBarOSX 

  public static void main(final String[] args) 
    System.setProperty("apple.laf.useScreenMenuBar", "true");

    final JMenuBar menuBar = new JMenuBar();
    final JMenu file = new JMenu("File");
    menuBar.add(file);
    final JMenuItem fileNew = new JMenuItem("New");
    file.add(fileNew);

    final JFrame frame1 = new JFrame("First");
    frame1.getContentPane().add(new JButton("First"));
    frame1.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

    final JFrame frame2 = new JFrame("Second");
    frame2.getContentPane().add(new JButton("Second"));
    frame2.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

    // hot-swap the menu bar to newly activated windows
    final WindowListener listener = new WindowAdapter() 
      @Override
      public void windowActivated(WindowEvent e) 
        ((JFrame) e.getWindow()).setJMenuBar(menuBar);
      
    ;
    frame1.addWindowListener(listener);
    frame2.addWindowListener(listener);

    final int offsetX = 200, offsetY = 50;
    frame1.pack();
    frame1.setLocation(offsetX, offsetY);
    frame1.setVisible(true);
    frame2.pack();
    frame2.setLocation(frame1.getWidth() + offsetX + 10, offsetY);
    frame2.setVisible(true);
  


使用这种方法,两个框架都显示相同的菜单栏,当两个框架都消失时,JVM 会干净地退出,而无需显式调用 System.exit(int)

不幸的是,这种方法并不完美:每次活动窗口发生变化时,菜单栏都会短暂消失。有人知道更好的方法吗?

【讨论】:

虽然您的解决方案很有效,但我个人觉得它并不令人满意。最好的解决方案是为您的菜单采用 MVC 设计。创建一个“菜单模型”,集中您的菜单层次结构、您的操作、您的快捷方式和所有内容。创建显示该模型的正确视图。然后,对于每一帧,创建一个不同的视图,但基于相同的模型。当模型改变时,视图会随处改变。另请阅读The Use of Multiple JFrames, Good/Bad Practice?。 @GuillaumePolet:感谢您的建议。实际上,我的应用程序正是这样做的:github.com/imagej/imagej/blob/2dcd8e4d/core/core/src/main/java/…。实际上,它是相当痴迷的 MVC。不幸的是,最终仍然必须创建一个JMenuBar 并将其附加到每个 Swing 窗口。您能否详细说明 MVC 如何回避这个问题? @GuillaumePolet:至于使用多个JFrames 是不好的做法,该链接的上下文对其回答的问题非常具体,我认为这里不适用。无论如何,为了兼容性和可用性,我们的应用程序是在具有多窗口(即 SDI)设计的先前版本之后建模的,我们必须保留它。 (不过,我们也有一个 MDI 实现。)在更广泛的层面上,我不同意使用多个窗口是“不好的、不好的做法”。甚至 OS X 本身也带有许多多窗口应用程序:Safari、终端、TextEdit、预览等。 @trashgod:就像我说的,我们也有一个使用JInternalFrame 的MDI UI。但我们仍然需要一个具有多个窗口的 SDI 版本。但是使用无模式对话是一个非常有趣的建议;我没有意识到JDialog 继承了其父级的JMenuBar。不幸的是,这种方法会导致所有子对话框在 Windows 上都缺少任务栏条目,例如您不能再在窗口之间使用 Alt+Tab,这可能不是可接受的折衷方案。 @ctrueden:有趣;在 OS X 上,对于example,命令选项卡在应用程序而不是多个帧之间切换;我使用Action & 键绑定在无模式对话框之间切换。抱歉,我之前忽略了这一点。【参考方案2】:

您可以利用JDialog,它继承了其父级的JMenuBar。要保持对话框无模式,您可以使用

PropertyChangeEvent 在对话框和主JFrame 之间进行通信,建议here。

ActionKey Bindings 在对话框之间导航。

【讨论】:

谢谢,我比windowActivated 方法更喜欢这个解决方案,因为菜单栏保持一致,在切换活动窗口时没有闪烁或延迟。这种方法应该可以在不同的 OS X 版本中广泛使用。但是,其他平台(例如 Windows)上的对话框行为可能是不受欢迎的(例如,对话框没有任务栏条目)。因此,在某些情况下,可能需要在 OS X 上使用 JDialog 和在其他平台上使用 JFrame 的混合方法。不过,这是迄今为止我见过的最好的解决方案。

以上是关于使 OS X 屏幕 JMenuBar 跨窗口一致工作的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 FLTK 在 Windows、Mac OS X 和 Linux 中使窗口透明?

如何使窗口在触摸屏幕边缘时弹回(修补程序)

如何在 Mac OS X 中获取光标相对于窗口的位置?

OS X OpenGL 3.2 核心(黑屏)

如何使 QTabWidget 看起来透明?

第十三周课程总结