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.
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.




August 2nd, 2007 at 6:24 am
so how did you add labels and what not to the panel?…since paintComponent doesn’t call super.
August 2nd, 2007 at 6:45 am
anoweb,
I think you’re confusing paintComponent with paint, paintComponent doesn’t paint children
August 2nd, 2007 at 7:16 am
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 :)
August 2nd, 2007 at 3:06 pm
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?
August 2nd, 2007 at 3:22 pm
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.)
August 2nd, 2007 at 3:52 pm
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?
August 2nd, 2007 at 3:55 pm
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.
August 3rd, 2007 at 4:21 am
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?
August 3rd, 2007 at 7:54 am
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?
August 3rd, 2007 at 7:55 am
Sorry, should have added if you have noticed the same behavior and if you did anything to get around it. Thanks.
August 3rd, 2007 at 9:32 am
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.
August 3rd, 2007 at 9:33 am
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 :)
August 6th, 2007 at 5:23 am
Thanks!
August 15th, 2007 at 10:02 am
Hey Romain…how did you make the nice rounded button in you dialog? can you do it with swingx painters?
August 15th, 2007 at 12:12 pm
It was achieved in the same way as for the dialog itself. And you could do it with painters, yes.
August 16th, 2007 at 1:05 pm
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).
September 28th, 2007 at 12:16 pm
Is it possible to achieve this effect without an underlying window? Basically a custom window like ITunes or WinAmp.
September 28th, 2007 at 12:22 pm
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.
September 28th, 2007 at 12:42 pm
Wow! Thanks I look forward to reading it.
October 16th, 2007 at 9:17 am
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.
January 9th, 2008 at 11:52 pm
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…
February 28th, 2008 at 8:52 pm
si alguien tendria el ejemplo me lo podrian mandar a mi correo darkx_valen666@hotmail.com desde ya gracias
June 26th, 2008 at 7:38 am
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