Summary

JMenuBarに追加したトップレベルのJMenuに下線または上線を追加して、選択状態を強調表示します。

Source Code Examples

class MenuHighlightLayerUI extends LayerUI<JMenuBar> {
  private static final Color SELECTION_COLOR = new Color(0x00_AA_FF);
  private static final int SZ = 3;
  private final Rectangle rect = new Rectangle();

  @Override public void installUI(JComponent c) {
    super.installUI(c);
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(
          AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
    }
  }

  @Override public void uninstallUI(JComponent c) {
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(0);
    }
    super.uninstallUI(c);
  }

  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    if (c instanceof JLayer && !rect.isEmpty()) {
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setPaint(SELECTION_COLOR);
      g2.fillRect(rect.x, rect.y + rect.height - SZ, rect.width, SZ);
      g2.dispose();
    }
  }

  @Override protected void processMouseEvent(
      MouseEvent e, JLayer<? extends JMenuBar> l) {
    super.processMouseEvent(e, l);
    if (e.getID() == MouseEvent.MOUSE_EXITED) {
      rect.setSize(0, 0);
    }
  }

  @Override protected void processMouseMotionEvent(
      MouseEvent e, JLayer<? extends JMenuBar> l) {
    super.processMouseMotionEvent(e, l);
    Component c = e.getComponent();
    if (c instanceof JMenu) {
      rect.setBounds(c.getBounds());
    } else {
      rect.setSize(0, 0);
    }
  }
}
View in GitHub: Java, Kotlin

Description

  • JLayerでラップしたJMenuBarを使用して選択されたJMenuに下線を描画
    • LayerUI#processMouseMotionEvent(...)などをオーバーライドしてJMenu領域内に下線を上書きで表示する
    • LookAndFeelに依存しない
    • JMenuBarJLayerでラップしているためJRootPane#setJMenuBar(JMenuBar)が使用不可
      • このため、このサンプルではJPanel#add(menuBar, BorderLayout.SOUTH)JPanelの下部に配置している
  • JMenuBarの上余白を追加し、その領域に選択されたJMenuの上線を描画
    • JMenuBar#paintComponent(...)をオーバーライドして選択JMenu領域内ではなく、その直上のJMenuBar上余白にハイライトを描画する
    • 下線を描画する場合はJMenuBarに下余白を追加し、トップレベルJMenuが開くJPopupMenuの位置をJMenuの下辺ではなくJMenuBarの下辺になるよう調整する必要がある
    • JMenuBarMouseMotionListenerを追加してもJMenu上でのマウスイベントを取得できないので、MenuSelectionManagerChangeListenerを追加してJMenuの選択状態の変化を取得している
class SelectionIndicatorMenuBar extends JMenuBar {
  private static final Color SELECTION_COLOR = new Color(0x00_AA_FF);
  private static final int SZ = 3;
  private final Rectangle rect = new Rectangle();
  private transient ChangeListener listener;

  @Override public void updateUI() {
    MenuSelectionManager manager = MenuSelectionManager.defaultManager();
    manager.removeChangeListener(listener);
    super.updateUI();
    Border inside = BorderFactory.createEmptyBorder(SZ + 1, 0, 0, 0);
    Border outside = UIManager.getBorder("MenuBar.border");
    Border border = BorderFactory.createCompoundBorder(outside, inside);
    setBorder(border);
    listener = this::updateTopLevelMenuBorder;
    manager.addChangeListener(listener);
  }

  @Override protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    if (!rect.isEmpty()) {
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setPaint(SELECTION_COLOR);
      g2.fillRect(rect.x, rect.y - SZ, rect.width, SZ);
      g2.dispose();
    }
  }

  private void updateTopLevelMenuBorder(ChangeEvent e) {
    Object o = e.getSource();
    rect.setSize(0, 0);
    MenuElement[] p = ((MenuSelectionManager) o).getSelectedPath();
    if (p != null && p.length > 1 && Objects.equals(this, p[0].getComponent())) {
      updateMenuIndicator(p[1].getComponent());
    }
    repaint();
  }

  private void updateMenuIndicator(Component c) {
    if (c instanceof JMenu && ((JMenu) c).isTopLevelMenu()) {
      JMenu menu = (JMenu) c;
      ButtonModel m = menu.getModel();
      if (m.isArmed() || m.isPressed() || m.isSelected()) {
        rect.setBounds(menu.getBounds());
      }
    }
  }
}
  • 選択JMenuMatteBorderを設定して下線を描画
    • このサンプルではJInternalFrame#setJMenuBar(...)JMenuBarを配置し、その子JMenuの選択状態変化をMenuSelectionManagerに追加したChangeListenerで取得
    • MetalLookAndFeelMotifLookAndFeelは有効で、WindowsLookAndFeelNimbusLookAndFeelでは無効
    • BasicMenuUI#paintBackground(...)をオーバーライドする方法もあるが、各JMenusetUI(...)BasicMenuUIを設定する手間を省くためJMenuBarを継承してJMenuMatteBorderを設定している
class SelectionHighlightMenuBar extends JMenuBar {
  private static final Color ALPHA_ZERO = new Color(0x0, true);
  private static final Color SELECTION_COLOR = new Color(0x00_AA_FF);
  private static final int SZ = 3;
  private transient ChangeListener listener;

  @Override public void updateUI() {
    MenuSelectionManager manager = MenuSelectionManager.defaultManager();
    manager.removeChangeListener(listener);
    super.updateUI();
    listener = e -> updateTopLevelMenuHighlight();
    manager.addChangeListener(listener);
    EventQueue.invokeLater(this::updateTopLevelMenuHighlight);
  }

  private void updateTopLevelMenuHighlight() {
    for (MenuElement me : getSubElements()) {
      updateMenuBorder(me.getComponent());
    }
  }

  private void updateMenuBorder(Component c) {
    if (c instanceof JMenu) {
      JMenu menu = (JMenu) c;
      if (menu.isTopLevelMenu() && menu.getParent().equals(this)) {
        ButtonModel model = menu.getModel();
        boolean b = model.isArmed() || model.isPressed() || model.isSelected();
        Color color = b ? SELECTION_COLOR : ALPHA_ZERO;
        Border inside = UIManager.getBorder("Menu.border");
        Border outside = BorderFactory.createMatteBorder(0, 0, SZ, 0, color);
        menu.setBorder(BorderFactory.createCompoundBorder(outside, inside));
      }
    }
  }
}

Reference

Comment