Skip to main content

Nimbus Look and Feel - Round buttons

Posted by Anthra on March 13, 2014 at 11:56 AM PDT

Nimbus is a cross-platform look and feel introduced in the Java SE 6 Update 10 (6u10) release. Based on Synth Look and Feel you can modify global properties. Properties can be simple (for example colors or dimensions) or more complex (for example Painters).

If you want to create you own painter you will probably extends AbstractRegionPainter which is used to paint a certain Region (a region define an area of a component, more information can be found there).
In nimbus most of the painters are sadly package-private and/or final. To extend them you need to copy the code and create a new class. That's the solution used here to create round button. The ButtonPainter class of nimbus is responsible for painting the background of button. Let's copy its code to a new class named RoundedCornerButtonPainter.

Getting round border is pretty easy. The original code defines round rectangle with fixed arc dimensions. We will compute arc dimension using width and height or the original rounded rectangles.

private RoundRectangle2D decodeRoundRect1() {
roundRect.setRoundRect(decodeX(0.2857143f), //x
   decodeY(0.42857143f), //y
   decodeX(2.7142859f) - decodeX(0.2857143f), //width
   decodeY(2.857143f) - decodeY(0.42857143f), //height
   12.0f, 12.0f); //rounding
return roundRect;
}
will become:
protected float getRounding(float width, float height){
return Math.min(width, height);
}

protected RoundRectangle2D decodeRoundRect1() {
roundRect.setRoundRect(decodeX(0.2857143f), //x
   decodeY(0.42857143f), //y
   decodeX(2.7142859f) - decodeX(0.2857143f), //width
   decodeY(2.857143f) - decodeY(0.42857143f), //height
   getRounding(decodeX(2.7142859f) - decodeX(0.2857143f),decodeY(2.857143f) - decodeY(0.42857143f)),
   getRounding(decodeX(2.7142859f) - decodeX(0.2857143f),decodeY(2.857143f) - decodeY(0.42857143f))); //rounding
return roundRect;
}

To apply that painter on buttons you need to be able to build one. The only constructor is using AbstractRegionPainter.PaintContext which cannot be created outside painter (protected inner class of AbstractRegionPainter). We will create a new constructor that will build a PaintContext:

public RoundedCornerButtonPainter(int state) {
super();
this.state = state;
this.ctx = new PaintContext(new Insets(7, 7, 7, 7), new Dimension(104, 33), false, null, Double.POSITIVE_INFINITY, 2.0);
}

To apply the painter you can use it in the look and feel UI defaults if you want to apply the change to all of your buttons or you can use client properties of a component. You need to construct one painter for each state.
To apply to all buttons:
UIManager.getLookAndFeel().getDefaults().put("Button[Default].backgroundPainter", 
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DEFAULT));
UIManager.getLookAndFeel().getDefaults().put("Button[Default+Focused].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DEFAULT_FOCUSED));
UIManager.getLookAndFeel().getDefaults().put("Button[Default+MouseOver].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_DEFAULT));
UIManager.getLookAndFeel().getDefaults().put("Button[Default+Focused+MouseOver].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_DEFAULT_FOCUSED));
UIManager.getLookAndFeel().getDefaults().put("Button[Default+Pressed].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_DEFAULT));
UIManager.getLookAndFeel().getDefaults().put("Button[Default+Focused+Pressed].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_DEFAULT_FOCUSED));
UIManager.getLookAndFeel().getDefaults().put("Button[Disabled].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DISABLED));
UIManager.getLookAndFeel().getDefaults().put("Button[Enabled].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_ENABLED));
UIManager.getLookAndFeel().getDefaults().put("Button[Focused].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_FOCUSED));
UIManager.getLookAndFeel().getDefaults().put("Button[MouseOver].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER));
UIManager.getLookAndFeel().getDefaults().put("Button[Focused+MouseOver].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_FOCUSED));
UIManager.getLookAndFeel().getDefaults().put("Button[Pressed].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED));
UIManager.getLookAndFeel().getDefaults().put("Button[Focused+Pressed].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_FOCUSED));

To apply to a specific button:

UIDefaults overrides = new UIDefaults();
overrides.put("Button[Default].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DEFAULT));
overrides.put("Button[Default+Focused].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DEFAULT_FOCUSED));
overrides.put("Button[Default+MouseOver].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_DEFAULT));
overrides.put("Button[Default+Focused+MouseOver].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_DEFAULT_FOCUSED));
overrides.put("Button[Default+Pressed].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_DEFAULT));
overrides.put("Button[Default+Focused+Pressed].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_DEFAULT_FOCUSED));
overrides.put("Button[Disabled].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DISABLED));
overrides.put("Button[Enabled].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_ENABLED));
overrides.put("Button[Focused].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_FOCUSED));
overrides.put("Button[MouseOver].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER));
overrides.put("Button[Focused+MouseOver].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_FOCUSED));
overrides.put("Button[Pressed].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED));
overrides.put("Button[Focused+Pressed].backgroundPainter",
new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_FOCUSED));
c.putClientProperty("Nimbus.Overrides", overrides);
c.putClientProperty("Nimbus.Overrides.InheritDefaults", false);

Round buttons

Nimbus look and feel is a vector based look and feel, it would be a pity to use pixel based graphics. We can easily extend our painter to draw a specified shape inside.

The shape will be automatically centered, scaled and drawn according to these new member variables:
Shape shape: The shape to fill
boolean respectRatio: If true the original ratio of the shape will be kept
double shapeSpaceRatio: the ratio of the space used inside the component by the shape ( for example if 0.5 the shape will be twice smaller than the button dimension)

Constructors will be add to set these new variables and call the RoundedCornerBorderPainter constructors. The doPaint method has been overridden to add a call to our paintShape method. To color used to fill the shape is defined inside the getExtendedCacheKeys method and will used the foreground property of the component (if set).

public ShapeRoundedCornerButtonPainter(int state, Shape shape, boolean respectRatio, double shapeSpaceRatio) {
super(state);
this.shape = shape;
this.respectRatio = respectRatio;
this.shapeSpaceRatio = shapeSpaceRatio;
}

public ShapeRoundedCornerButtonPainter(JComponent c, int state, Shape shape, boolean respectRatio, double shapeSpaceRatio) {
super(c, state);
this.shape = shape;
this.respectRatio = respectRatio;
this.shapeSpaceRatio = shapeSpaceRatio;
}

private void paintShape(Graphics2D g){
shapeBounds = decodeShapeBounds();
g.setPaint((Color)componentColors[5]);
if(respectRatio)
{
double shapeRatio = shape.getBounds().getWidth()/shape.getBounds().getHeight();
double boundsRatio = shapeBounds.getWidth()/shapeBounds.getHeight();
if(shapeRatio {
affineTransform.setToScale(shapeSpaceRatio*shapeBounds.getHeight()/shape.getBounds().getHeight(),
shapeSpaceRatio*shapeBounds.getHeight()/shape.getBounds().getHeight());
}
else
{
affineTransform.setToScale(shapeSpaceRatio*shapeBounds.getWidth()/shape.getBounds().getWidth(),
shapeSpaceRatio*shapeBounds.getWidth()/shape.getBounds().getWidth());
}
}
else
{
affineTransform.setToScale(shapeSpaceRatio*shapeBounds.getWidth()/shape.getBounds().getWidth(),
                               shapeSpaceRatio*shapeBounds.getHeight()/shape.getBounds().getHeight());
}
Shape rescaled = affineTransform.createTransformedShape(shape);
affineTransform.setToTranslation(
        shapeBounds.getWidth()/2+shapeBounds.getX()-rescaled.getBounds().getWidth()/2-rescaled.getBounds().getX(),
shapeBounds.getHeight()/2+shapeBounds.getY()-rescaled.getBounds().getHeight()/2-rescaled.getBounds().getY());
rescaled = affineTransform.createTransformedShape(rescaled);
g.fill(rescaled);
}

private Rectangle2D decodeShapeBounds(){
Rectangle2D bounds = new Rectangle2D.Float(decodeX(0.2857143f), //x
   decodeY(0.2857143f), //y
   decodeX(2.7142859f) - decodeX(0.2857143f), //width
   decodeY(2.7142859f) - decodeY(0.2857143f));
return bounds;
}

I've added two or three shapes into the demo.

Round buttons with shape

You can also convert a String or a single char into shape. That way the text/character will take all the place available (according to shapeSpaceRatio).

public Shape convert(String s) {
Font font = getFont();
GlyphVector v = font.createGlyphVector(getFontMetrics(font).getFontRenderContext(), s);
return v.getOutline();
}

Round buttons with text shape

Like always I attached the Netbeans project containing all sources.

AttachmentSize
thumb2.png3.86 KB
simpleRoundButtons.png4.63 KB
roundButtonsWithShapes.png7.89 KB
roundButtonsShapeFromText.png3 KB
NimbusRoundButtonNetBeansProject.zip68.49 KB
Related Topics >>

Comments

I forgot to upload the picture. How do i see my comments not ...

I forgot to upload the picture. How do i see my comments not yet approved?
sorry for my english

Hello blocked, Sorry for the delay in the comment ...

Hello blocked,

Sorry for the delay in the comment approbation. I've never had trouble with this. After your report I tried to stress the button (clicking as hell and mouse in-out) but I cannot reproduce your issue on my side. I'm on Win7 64b with Java 1.7.0_51.

What is your configuration?

Windows XP 32 bits with java version:1.7.0_51. I will come ...

Windows XP 32 bits with java version:1.7.0_51. I will come back after I test on a Win 7 OS.
Also, do you resized the frame ? For a frame bigger the display problems are bigger.

Hi, I have an question. I run this code and after ...

Hi,
I have an question. I run this code and after pressing the button repeatedly or i did mouse over the button looks like this. With the standard button of Nimbus the button looks good. On your computer the effect is the same?
(I deleted any code related to the other buttons.). The other buttons have higher display problems.