如何使OS X屏幕上的JMenuBar在多个窗口中保持一致?

3
我有一个跨平台的Java应用程序,具有Swing用户界面。在OS X上,该应用程序使用屏幕菜单栏以获得更本地化的用户体验。
一般情况下,该应用程序为每个文档创建一个JFrame。屏幕菜单栏必须在所有这些窗口中保持一致。我尝试了几种方法,发现只有一种一致且高效的解决方案,虽然不完美但已足够。我发布这个问题是希望其他人有更好的方法,并希望这些信息能帮助其他人。
一些行不通的方法:

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

我尝试将相同的JMenuBar添加到多个JFrame实例,但Swing仅支持将JMenuBar附加到单个JFrame中, 即使作为屏幕菜单栏也是如此。
我还尝试过使用AWT的MenuBar而不是JMenuBar,但是出现了相同的现象。与JMenuBar相比,MenuBar有许多限制(例如没有图标),因此让我们继续满足需要一个JMenuBar的要求。
克隆菜单栏 一种常见的解决方案是为每个新的JFrame创建一个JMenuBar的副本。然而,这种方法至少存在两个问题。首先,您必须保持菜单栏同步。虽然可以使用监听器来完成,但这是处理OS X平台所需的大量额外代码。但是,第二个更严重的问题是性能:如果您有一个具有数百个菜单项的复杂菜单栏,则克隆菜单栏非常慢。我们发现这种方法会使新窗口的出现延迟数秒!
使用默认菜单栏

苹果的Java库中添加了一个新的方法Java for OS X v10.6 Update 1 and 10.5 Update 6Application.setDefaultMenuBar(JMenuBar)

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

然而,setDefaultMenuBar功能存在几个主要问题:

  1. 加速键无法使用。我们在应用程序中通过自己处理所有按键操作来避免这个问题,但这仍然很不幸。
  2. 截至2012年12月,setDefaultMenuBar仍然不可用于Java7。我们显然要避免使用已弃用或不受支持的API。
  3. 最关键的是,调用setDefaultMenuBar会阻止JVM正常关闭。即使随后调用setDefaultMenuBar(null)也不能释放必要的资源。

简而言之,setDefaultMenuBar似乎并不是一个安全且健壮的选择。

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

你说的“保持菜单同步”是什么意思?我使用的技术是每次创建一个全新的副本,目前还没有遇到这个问题,所以我很好奇可能是我没有注意到的地方。 - Hakanai
@Trejkaz 对于小型菜单,复制可以接受。但是当您拥有许多菜单项时,为每个新的 JFrame 制作一个新副本会产生实质性的内存和时间开销。如上所述,在树中大约有500个菜单项时,新 JFrame 的外观会被延迟数秒!至于同步问题,如果您的菜单结构发生更改,则会有影响:A)添加新菜单项;B)删除菜单项;C)重命名菜单项;D)使用 JCheckBoxMenuItemJRadioButtonMenuItem,因为这些具有关联状态,可能需要在不同的 JFrame 之间保持同步。 - ctrueden
我想知道你是否在谈论复选框和单选按钮。在这些情况下,您将共享ButtonModel两个菜单之间,以便Swing本身已经将它们保持同步。我猜我们还没有达到500个菜单项,但我也不会称其为小型。100〜200?如果您已经在操作映射中拥有Action对象并且只是查找它们,则不需要太长时间。话虽如此,似乎这个问题最终已经得到解决,因此可能仍然可以使用默认菜单栏。 - Hakanai
@Trejkaz,“这个问题终于被解决”是什么意思?你是说Java 7现在支持setDefaultMenuBar了吗?你有相关问题或文章的链接吗? - ctrueden
@Trejkaz 谢谢!我很想听听任何尝试这个修复程序的人的评论。特别是,我想知道上面提到的问题(加速器不工作和JVM关机阻塞)是否已经通过这个修复程序得到解决。 - ctrueden
显示剩余2条评论
2个回答

3
我发现的解决方案相当有效,它是通过为应用程序的每个窗口添加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) 的情况下干净地退出。
不幸的是,这种方法并不完美:每次活动窗口更改时,菜单栏会短暂消失。有人知道更好的方法吗?

1
虽然你的解决方案在某种程度上可以工作,但我个人觉得它并不令人满意。最好的解决方案是为菜单选择MVC设计模式。创建一个“菜单模型”,将菜单层次结构、操作、快捷键等集中起来。创建适当的视图来显示该模型。然后,为每个框架创建一个不同的视图,但基于相同的模型。当模型发生变化时,视图将在所有地方进行相应的变化。还可以阅读《使用多个JFrames,好还是坏的实践?》(https://dev59.com/Wmkw5IYBdhLWcg3w8O6o#9554657)。 - Guillaume Polet
@GuillaumePolet:感谢您的建议。实际上,我的应用程序正是这样做的:https://github.com/imagej/imagej/blob/2dcd8e4d/core/core/src/main/java/imagej/menu/ShadowMenu.java#L63。它非常强调MVC模式。不幸的是,最终仍然必须创建一个`JMenuBar`并将其附加到每个Swing窗口。您能详细说明一下MVC如何避开这个问题吗? - ctrueden
@GuillaumePolet:至于使用多个JFrame是否是不良实践,该链接的上下文非常特定于它所回答的问题,我认为在这里不适用。无论如何,为了兼容性和可用性,我们的应用程序是基于以前具有多窗口(即SDI)设计的版本进行建模的,我们必须保留它。(我们也有MDI实现。)从更广泛的层面上说,我不同意使用多个窗口是“不好的,不好的实践”;甚至OS X本身都带有许多多窗口应用程序:Safari、Terminal、TextEdit、Preview等。 - ctrueden
@trashgod:就像我说的,我们也有使用JInternalFrame的MDI UI。但是我们仍然需要一个具有多个窗口的SDI版本。但是使用非模态对话框是一个非常有趣的建议;我没有意识到JDialog会继承其父级的JMenuBar。不幸的是,这种方法会导致所有子对话框在Windows上缺少任务栏条目,因此您不能再使用Alt+Tab在窗口之间切换,这可能是无法接受的权衡。 - ctrueden
@trashgod:越想使用JDialog,我就越喜欢它...但只在OS X上。我将尝试更新我们的UI,以在OS X上使用JDialog窗口,但在其他平台上仍然使用JFrame窗口。我认为这将提供跨平台的用户期望的行为。如果您发布有关使用非模态JDialog的答案,我会接受它,因为我认为它比我的答案中的windowActivated hack更好。顺便说一下,如果您不知道:您可以使用Cmd+backtick在OS X上在应用程序的窗口之间循环。 :-) - ctrueden
显示剩余2条评论

2
您可以利用JDialog,它继承了其父窗口的JMenuBar。为了保持对话框无模态,您可以使用
  • PropertyChangeEvent在对话框和主要的JFrame之间进行通信,如这里所建议的。

  • Action键绑定用于在对话框之间导航。


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接