Summary

JFrameがデフォルトWindow装飾を使用する場合、JButtonなどを配置したJWindowをタイトルバー内のアイコン化ボタン位置に連動して表示します。

Source Code Examples

class ExtraBarPositionHandler extends WindowAdapter implements ComponentListener {
  private final JWindow extraBar;

  protected ExtraBarPositionHandler(JWindow extraBar) {
    this.extraBar = extraBar;
  }

  @Override public void windowActivated(WindowEvent e) {
    extraBar.setBackground(UIManager.getColor("activeCaption"));
  }

  @Override public void windowDeactivated(WindowEvent e) {
    extraBar.setBackground(UIManager.getColor("inactiveCaption"));
  }

  @Override public void windowOpened(WindowEvent e) {
    setLocationRelativeTo(e.getWindow());
    extraBar.setVisible(true);
  }

  @Override public void windowClosed(WindowEvent e) {
    extraBar.setVisible(false);
  }

  @Override public void windowIconified(WindowEvent e) {
    extraBar.setVisible(false);
  }

  @Override public void windowDeiconified(WindowEvent e) {
    setLocationRelativeTo(e.getWindow());
    extraBar.setVisible(true);
  }

  @Override public void componentResized(ComponentEvent e) {
    // System.out.println("componentResized");
    // ???: setLocationRelativeTo(e.getComponent());
    EventQueue.invokeLater(() -> setLocationRelativeTo(e.getComponent()));
  }

  @Override public void componentMoved(ComponentEvent e) {
    setLocationRelativeTo(e.getComponent());
  }

  @Override public void componentShown(ComponentEvent e) {
    setLocationRelativeTo(e.getComponent());
    extraBar.setVisible(true);
  }

  @Override public void componentHidden(ComponentEvent e) {
    extraBar.setVisible(false);
  }

  private void setLocationRelativeTo(Component p) {
    EventQueue.invokeLater(() -> updateExtraBarLocation(p));
  }

  private void updateExtraBarLocation(Component p) {
    JRootPane root = SwingUtilities.getRootPane(p);
    Icon iconifyIcon = UIManager.getIcon("InternalFrame.iconifyIcon");
    Insets zeroIns = new Insets(0, 0, 0, 0);
    Border bdr = root.getBorder();
    Insets ins = bdr == null ? zeroIns : bdr.getBorderInsets(root);
    if (p instanceof Frame &&
        ((Frame) p).getExtendedState() == Frame.MAXIMIZED_BOTH) {
      ins = zeroIns;
    }
    JButton iconifyIconButton = SwingUtils
        .descendants(root)
        .filter(JButton.class::isInstance)
        .map(JButton.class::cast)
        .filter(b -> Objects.equals(b.getIcon(), iconifyIcon))
        .findFirst()
        .orElse(null);
    Point pt = iconifyIconButton == null
        ? new Point()
        : iconifyIconButton.getLocation();
    SwingUtilities.convertPointToScreen(pt, root);
    int x = pt.x - extraBar.getWidth();
    int y = p.getY() + ins.top + 1;
    extraBar.setLocation(x, y);
  }
}
View in GitHub: Java, Kotlin

Description

  • JFrame.setDefaultLookAndFeelDecorated(true)を指定してJFrameがデフォルトのWindow装飾を使用するよう設定
    • このためMetalLookAndFeel以外には未対応で、LookAndFeelの切り替えにも対応していない
  • デフォルトのWindow装飾を使用する場合、アイコン化JButtonUIManager.getIcon("InternalFrame.iconifyIcon")アイコンが適用されるので、JRootPane内でこれを使用するJButtonを探し、その位置を取得する
  • アイコン化JButtonの位置をスクリーン相対にSwingUtilities.convertPointToScreen(pt, root)で変換して、Dialog.ModalExclusionType.APPLICATION_EXCLUDEな子JWindowをアイコン化JButtonのとなりに配置する
  • WindowAdapterを継承、かつComponentListenerを実装するイベントリスナーを作成して、常に親JFrameの移動やリサイズに子JWindowの位置が追従するよう設定
    • JFrameのリサイズで子JWindowのサイズ以下になると、JButtonがタイトルバーの外に表示されるので、これを防ぐためにJFrame#setMinimumSize(...)で最小サイズを設定
    • JFrameがアイコン化される場合は子JWindowを非表示に変更する
    • WindowAdapter#windowActivated(...)が実行されたら子JWindowの背景色をUIManager.getColor("activeCaption")WindowAdapter#windowDeactivated(...)が実行されたらUIManager.getColor("inactiveCaption")に切り替える
    • 最大化解除時に子JWindowの位置が正しく更新されない場合がある?ため、ComponentListener#componentResized(...)が実行されたらEventQueue.invokeLater(...)を二重に掛けて子JWindowの位置更新を実行している
    • JFrameが最大化される場合はそのJRootPaneの余白が無くなるので、子JWindowy座標をその分修正する必要がある

Reference

Comment