概要

Robotで画面をキャプチャーするなどして、半透明の影をJPopupMenuに付けます。

サンプルコード

class ShadowBorder extends AbstractBorder {
  private final int xoff, yoff;
  private final transient BufferedImage screen;
  private transient BufferedImage shadow;

  public ShadowBorder(int x, int y, JComponent c, Point p) {
    super();
    this.xoff = x;
    this.yoff = y;
    BufferedImage bi = null;
    try {
      Robot robot = new Robot();
      Dimension d = c.getPreferredSize();
      bi = robot.createScreenCapture(new Rectangle(p.x, p.y, d.width + xoff, d.height + yoff));
    } catch (AWTException ex) {
      ex.printStackTrace();
    }
    screen = bi;
  }
  @Override public Insets getBorderInsets(Component c) {
    return new Insets(0, 0, xoff, yoff);
  }
  @Override public void paintBorder(Component c, Graphics g, int x, int y, int w, int h) {
    if (screen == null) {
      return;
    }
    if (shadow == null || shadow.getWidth() != w || shadow.getHeight() != h) {
      shadow = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
      Graphics2D g2 = shadow.createGraphics();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .2f));
      g2.setPaint(Color.BLACK);
      for (int i = 0; i < xoff; i++) {
        g2.fillRoundRect(xoff, xoff, w - xoff - xoff + i, h - xoff - xoff + i, 4, 4);
      }
      g2.dispose();
    }
    Graphics2D g2d = (Graphics2D) g.create();
    g2d.drawImage(screen, 0, 0, c);
    g2d.drawImage(shadow, 0, 0, c);
    g2d.setPaint(c.getBackground()); //??? 1.7.0_03
    g2d.fillRect(x, y, w - xoff, h - yoff);
    g2d.dispose();
  }
}
view all

解説

ポップアップメニューに半透明の影をつける際、フレームからはみ出すかどうかで異なる処理を行っています。

上記のサンプルコードは、フレームからはみ出す場合に使用するBorderクラスです。

  • フレーム内
    • JPopupMenu#paintComponentメソッドで半透明の影を描画
  • フレーム外
    • Robotを使って画面全体をキャプチャーし、これを利用して半透明の影をBorderとして作成
    • このためポップアップメニューがはみ出しても、影を付けることが可能

JDK 1.7.0や、1.6.0_10以上の場合は、フレーム外でもRobotを使用せず、以下のようにJPopupMenuの上位Windowの背景色を透明にすることで影をつけることができます。

class DropShadowPopupMenu extends JPopupMenu {
  private static final int OFFSET = 4;
  private transient BufferedImage shadow;
  private Border border;
  @Override public boolean isOpaque() {
    return false;
  }
  @Override public void updateUI() {
    setBorder(null);
    super.updateUI();
    border = null;
  }
  @Override protected void paintComponent(Graphics g) {
    //super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g.create();
    g2.drawImage(shadow, 0, 0, this);
    g2.setPaint(getBackground()); //??? 1.7.0_03
    g2.fillRect(0, 0, getWidth() - OFFSET, getHeight() - OFFSET);
    g2.dispose();
  }
  @Override public void show(Component c, int x, int y) {
    if (border == null) {
      Border inner = getBorder();
      Border outer = BorderFactory.createEmptyBorder(0, 0, OFFSET, OFFSET);
      border = BorderFactory.createCompoundBorder(outer, inner);
    }
    setBorder(border);
    Dimension d = getPreferredSize();
    int w = d.width;
    int h = d.height;
    if (shadow == null || shadow.getWidth() != w || shadow.getHeight() != h) {
      shadow = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
      Graphics2D g2 = shadow.createGraphics();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                          RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .2f));
      g2.setPaint(Color.BLACK);
      for (int i = 0; i < OFFSET; i++) {
        g2.fillRoundRect(
            OFFSET, OFFSET, w - OFFSET - OFFSET + i, h - OFFSET - OFFSET + i, 4, 4);
      }
      g2.dispose();
    }
    EventQueue.invokeLater(new Runnable() {
      @Override public void run() {
        Window pop = SwingUtilities.getWindowAncestor(DropShadowPopupMenu.this);
        if (pop instanceof JWindow) {
          pop.setBackground(new Color(0x0, true)); //JDK 1.7.0
        }
      }
    });
    super.show(c, x, y);
  }
}

参考リンク

コメント

  • キャプチャーが遅いのは画面全体を撮っているからで、必要なサイズだけにすれば結構速いようです。サンプルを修正してみたところ、毎回キャプチャーするようにしても特に気にならない速度で動いてます。 -- aterai
  • ソース中でisInRootPanelがおかしい気がするのですが・・・ convertPointToScreenがいらないのとreturn r.contains(pt)&&r.contains(p)にしないとフレーム内の判定がおかしいようです -- sawshun
    • ご指摘ありがとうごさいます。convertPointToScreenを削除して、MyPopupMenu#isInRootPanelは以下のように修正しました。 -- aterai
private boolean isInRootPanel(JComponent root, Point p) {
  Rectangle r = root.getBounds();
  Dimension d = this.getPreferredSize();
  //pointed out by sawshun
  return r.contains(p.x, p.y, d.width + off, d.height + off);
}
final MyPopupMenu pop = new MyPopupMenu();
pop.add(new JMenuItem("Open"));
pop.add(new JMenuItem("Save"));
pop.add(new JMenuItem("Close"));
//pop.addSeparator(); //XXX: Nimbus
JSeparator s = new JSeparator();
s.setOpaque(true);
pop.add(s);
pop.add(new JMenuItem("Exit"));
JLabel label = new JLabel(icon);
label.setComponentPopupMenu(pop);
//JDK 1.5 label.addMouseListener(new MouseAdapter() {});
//addMouseListener(new MouseAdapter() {
//  public void mouseReleased(MouseEvent e) {
//    if (e.isPopupTrigger()) {
//      Point pt = e.getPoint();
//      pop.show(e.getComponent(), pt.x, pt.y);
//    }
//    repaint();
//  }
//});
  • SynthLookAndFeel(Nimbusなど)で、JSeparatorだけでなくJMenuItemまで透明になった修正?に対応。 -- aterai
  • 1.7.0_03でなにか変更があった?のか、変な挙動をするようになったので、調査中。 -- aterai
  • exitcloseが動作するのかと思ったのですが動かないんですよね? JPopupMenuに表示させているだけでしょうか、もしそうならExitを押したときにフレームが終了するようなコードはどう書けばいいのでしょうか? -- hshs
    • 影を付けるだけのサンプルコードなので、JMenuItemは名前だけのダミーになっています。「フレームを終了するコード…」は、複数のJFrameが開いているかもしれない場合を考慮して、以下のような方法を使用するのがいいかもしれません。 -- aterai
JMenuItem mi = new JMenuItem(new AbstractAction("Exit") {
  @Override public void actionPerformed(ActionEvent e) {
    JMenuItem m = (JMenuItem) e.getSource();
    JPopupMenu popup = (JPopupMenu) m.getParent();
    JComponent invoker = (JComponent) popup.getInvoker();
    Window f = SwingUtilities.getWindowAncestor(invoker);
    if (f != null) f.dispose();
  }
});
  • 返信ありがとうございます、当方Netbeansで開発してまして、上記のコードをjPopupMenu1.add(この中);new JMenuItem以降を入れたのですが動きませんでした。よってJMenuItem m~f.dispose();までを削除し、かわりにjFrame1.setVisible(false);を入れると動作しました。 -- hshs