Rounded Corners and Shadow for Dialogs (Extreme GUI Makeover)

At JavaOne 2007, the Extreme GUI Makeover demo showed a semi-translucent dialog over a blurry background. This effect was explained in a previous entry on this blog. However, I received a few requests asking how the dialog itself was implemented.

This dialog is shown in the screenshot below. Not only is it translucent, but it also sports rounded corners and a subtle drop shadow. Implementing such a dialog is fairly easy and I will describe you what techniques you can use.

Extreme GUI Makeover

Before we delve into the code, remember that this “dialog” is in fact a simple JPanel (more specifically a JXPanel from SwingX) added to the frame’s glass pane. To draw the rounded corners and the translucent background we first need to make our component non-opaque:

public class DetailPanel extends JPanel {
    public DetailPanel() {
        setOpaque(false);
    }
}

The next step is simply to override the paintComponent() method:

@Override
protected void paintComponent(Graphics g) {
    int x = 34;
    int y = 34;
    int w = getWidth() - 68;
    int h = getHeight() - 68;
    int arc = 30;

    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);

    g2.setColor(new Color(0, 0, 0, 220));
    g2.fillRoundRect(x, y, w, h, arc, arc);

    g2.setStroke(new BasicStroke(3f));
    g2.setColor(Color.WHITE);
    g2.drawRoundRect(x, y, w, h, arc, arc); 

    g2.dispose();
}

This implementation fills a black, translucent rounded rectangle to paint the background. Then it proceeds with drawing a white, opaque rounded rectangle using the exact same dimension, location and arcs. The border effect is rendered by setting a thick stroke on the graphics surface, 3 pixels in this case. As you can see you can create good-looking custom components with just a few lines of code.

Rendering the drop shadow is a bit more complicated though. SwingX provides a DropShadowBorder that works in most situation but that fits only rectangular components. Because we are using a rounded rectangle component, we need another approach. Hopefully, SwingX contains a very handy class named ShadowRenderer. As its name suggests, it can be used to render shadows. Just pass an image and you get a shadow for it.

Shadow rendering is performed in the setBounds() methods, called whenever the component changes dimension and/or location. The idea is to create an image buffer in which we draw a rounded rectangle of the same dimension as the one we used at painting time. This image is then passed to the shadow renderer to create the shadow we want to paint behind the component:

@Override
public void setBounds(int x, int y, int width, int height) {
    super.setBounds(x, y, width, height);

    int w = getWidth() - 68;
    int h = getHeight() - 68;
    int arc = 30;
    int shadowSize = 20;

    shadow = GraphicsUtilities.createCompatibleTranslucentImage(w, h);
    Graphics2D g2 = shadow.createGraphics();
    g2.setColor(Color.WHITE);
    g2.fillRoundRect(0, 0, w, h, arc, arc);
    g2.dispose();

    ShadowRenderer renderer = new ShadowRenderer(shadowSize, 0.5f, Color.BLACK);
    shadow = renderer.createShadow(shadow);
}

Once again, very easy. Unfortunately, this technique does not work very well. Because our component uses a translucent background, we can see the shadow through. The result of the blending produces an almost opaque effect which defeats the purpose of having a semi-translucent dialog in the first place. This is why tools like Photoshop do not draw shadows behind the layers.

The solution is to get rid of the part of the shadow that lies within the area in which the component paints its background. This could be achieved at painting time but I chose to do it when generating the shadow. To the previous code snippet, just add the following:

g2 = shadow.createGraphics();
// The color does not matter, red is used for debugging
g2.setColor(Color.RED);
g2.setComposite(AlphaComposite.Clear);
g2.fillRoundRect(shadowSize, shadowSize, w, h, arc, arc);
g2.dispose();

When the “clear” alpha composite is set on the graphics surface, everything we draw erases the existing pixels. We thus cut a hole into our shadow. The final step is simply to paint the shadow in paintComponent(), just before the filling and drawing of the rounded rectangles occurs:

if (shadow != null) {
    int xOffset = (shadow.getWidth()  - w) / 2;
    int yOffset = (shadow.getHeight() - h) / 2;
    g2.drawImage(shadow, x - xOffset, y - yOffset, null);
}

This code is not the most efficient nor elegant way to achieve this effect but it is clear, easy to implement and fast enough. This technique can be used in a variety of situations too. For instance, you could add a drop shadow to an icon by generating the shadow, then “clearing” with the icon itself. Note that the “clearing” step is required only when you are or might be drawing a translucent element on top of the shadow.

28 Responses to “Rounded Corners and Shadow for Dialogs (Extreme GUI Makeover)”

  1. anoweb says:

    so how did you add labels and what not to the panel?…since paintComponent doesn’t call super.

  2. Magnus says:

    anoweb,
    I think you’re confusing paintComponent with paint, paintComponent doesn’t paint children

  3. anoweb says:

    oh duh….the panel x,y is set to 34…so the label I added was being added “outside” of the black area of the panel :)

  4. Andrew says:

    I don’t get how you add components to the panel. If I just want to add a random JPanel to the DetailPanel as the content to display, then the dialog is not surrounding it. If I change the x,y of the dialog to 0,0, then it covers the shadow. I have tried making the shadow larger, but it gets clipped because of the super.setBounds is the size of the panel, not the panel + shadow. It seems like you want to make the shadow just grow outside the bounds of the the rest of the panel, but I don’t understand how to do that in a swing legal way. Any help?

  5. Romain Guy says:

    Andrew, in our case we just used an EmptyBorder to offset the content inward:

    int bpad = 55;
    setBorder(new EmptyBorder(bpad, bpad, bpad, bpad));

    You could also setup an inner panel with a padding (with GridBagLayout for instance.)

  6. Andrew says:

    Well, i sorta figured it out by doing:
    super.setBounds(x, y, width + mShadowSize * 2, height + mShadowSize * 2);

    Then changing all the getWidth/getHeight to be getWidth() – mShadowSize * 2 (same for height).

    And set x and y to mShadowSize. This seems to do a decent job. I sorta like your inner panel idea though. Sorta a wrapper around the actual content panel?

  7. Romain Guy says:

    Yes indeed. That’s what SwingX’s JXTaskPane does for instance. You create an inner JPanel and override methods like add()/remove()/etc. to be forwarded to that JPanel. Very much like for JFrame.add()/etc.

  8. Jeff says:

    Romain,

    It seems that you often override paintComponent in Swing containers rather than using the Painter api from swinglabs. Why? Is there a performance benefit?

  9. Steve says:

    Romain,

    Very cool effect. I was playing around with something very similar and I have found that I can’t keep the focus on the glass pane even with using key and mouse listeners when the user hits Tab. The focus will go to something on the content pane, which breaks the dialog behavior. Any ideas?

  10. Steve says:

    Sorry, should have added if you have noticed the same behavior and if you did anything to get around it. Thanks.

  11. Romain Guy says:

    Steve, my book explains how to deal with it. If you look at the glass pane examples on filthyrichclients.org, you’ll find the answer. Basically, you need to disable the focus traversal ability. There are APIs for that in JComponent.

  12. Romain Guy says:

    Jeff, there’s virtually no performance gain. But I demonstrated the use of SwingX’s painters last year, shortly after Richard and I came up with them, and it’s always better to show how to use Java 2D instead. Painters are very easy :)

  13. Andrew says:

    Hey Romain…how did you make the nice rounded button in you dialog? can you do it with swingx painters?

  14. Romain Guy says:

    It was achieved in the same way as for the dialog itself. And you could do it with painters, yes.

  15. Andrew says:

    Any hints on how to do it with a painter? I tried just setting a roundedrect shape painter, but it doesn’t change the shape of the button. Everything else seems to work (colors, gloss, etc).

  16. Jon says:

    Is it possible to achieve this effect without an underlying window? Basically a custom window like ITunes or WinAmp.

  17. Romain Guy says:

    Sure, with JNA. I actually implemented such a window in the same Extreme GUI Makeover demo. Check out this entry: http://www.curious-creature.org/2007/04/10/translucent-swing-windows-on-mac-os-x/

    I’ll write a new blog entry about this tonight.

  18. Jon says:

    Wow! Thanks I look forward to reading it.

  19. For anyone else having problem getting labels to show (using standard Swing JPanel rather than JXPanel), get rid of the g2.dispose() and it will work.

  20. Deekshit M says:

    Hi Romain,
    There us a small problem with this code. I added JCombobox to the DetailPanel. Now I clicked the drop down button. The list is not visible. The list is drawn behind the panel. The textfield stays on the panel properly. This is clearly visible when the Combobox is added at the top right corner. Any solution…

  21. sdark not says:

    si alguien tendria el ejemplo me lo podrian mandar a mi correo darkx_valen666@hotmail.com desde ya gracias

  22. BenSept says:

    Hi,
    I’ve got the same problem than Deekshit. I tried combo.setLightWeightPopupEnabled(false) in vein.
    I can see the combopopup right when it goes out of the frame (when the combo is located in the bottom of the frame for exemple). I think the problem is due to the JPopupMenu, which is a JComponent and not a JPanel.
    I tried to show my own popupmenu : there is the same problem if it extends JComponent ; there’s no problem if it extends JPanel.
    Any suggestion Romain and others ?
    Is the sole solution to make my own ComboBoxUI using my own ComboPopup extends JPanel ?
    Thx

  23. hardy says:

    hello guys…i already tried to do this dialog…the problem is..all the component(ex : JLabel) that i attach to it is not display when run…i already comment out g2.disposed()..any solution to this??

  24. mirlinho says:

    Hi Romain,
    thank you very much, is a most interesting.

  25. Piyush says:

    Hi Romain,

    I want to Thank You for this wonderful stuff.

    Thanks!

  26. Joy Pappy says:

    Hi Romain,

    Sorry but I could not get the shadow work for me. I am pasting the code here hope its ok??

    /*
    * To change this template, choose Tools | Templates
    * and open the template in the editor.
    */

    package javaex.shadow1;

    import java.awt.AlphaComposite;
    import java.awt.BasicStroke;
    import java.awt.Color;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.RenderingHints;
    import java.awt.image.BufferedImage;
    import javaex.GraphicsUtilities;
    import javaex.ShadowRenderer;
    import javax.swing.JPanel;

    /**
    *
    * @author JoyP
    */
    public class DetailPanel extends JPanel{

    public DetailPanel(){
    setOpaque(false);
    }

    @Override
    public void paintComponent(Graphics g){
    int x = 34;
    int y = 34;
    int w = getWidth() – 68;
    int h = getHeight() – 68;
    int arc = 30;

    Graphics2D g2 = (Graphics2D) g.create();

    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
    RenderingHints.VALUE_ANTIALIAS_ON);

    g2.setColor(Color.BLACK);
    g2.fillRoundRect(x, y, w, h, arc, arc);

    g2.setStroke(new BasicStroke(3f));
    g2.setColor(Color.WHITE);
    g2.drawRoundRect(x, y, w, h, arc, arc);

    g2.dispose();

    System.out.println(“w : “+w+”, h : “+h);

    }//end method: paintComponent

    @Override
    public void setBounds(int x, int y, int width, int height) {
    super.setBounds(x, y, width, height);

    int w = getWidth() – 68;
    int h = getHeight() – 68;
    int arc = 30;
    int shadowSize = 20;

    shadow = GraphicsUtilities.createCompatibleTranslucentImage(w, h);
    Graphics2D g2 = shadow.createGraphics();
    // The color does not matter, red is used for debugging
    g2.setColor(Color.RED);
    g2.setComposite(AlphaComposite.Clear);
    g2.fillRoundRect(shadowSize, shadowSize, w, h, arc, arc);
    g2.dispose();

    ShadowRenderer renderer = new ShadowRenderer(shadowSize, 0.5f, Color.BLACK);
    shadow = renderer.createShadow(shadow);

    }

    private BufferedImage shadow = null;

    }

    Thanks!

  27. Joy Pappy says:

    Hi Romain,

    I made a mistake in the setBounds method, thats why was not able to get the shadow rendered. Sorry for the trouble.

    Finally got it working. Looks good. Hats off to you!!

    Regards/Joy