I am trying to make a custom button, (which extends BasicButtonUI
), have rounded corners. I found out that I should use a rounded border which covered the square corners of the button.
I am adding the border to my button like this:
b.setBorder(new RoundedBorder(20));
And I am drawing the border like this:
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Constants.windowColor);
g2d.setStroke(new BasicStroke(5f)); // Width of 5 pixels.
g2d.setPaintMode();
g2d.drawRoundRect(x, y, width-1, height-1, radius, radius);
}
But, as you can see in the screenshot below, the corners of the button are "moved" outwards. They aren't even in line with the edges of the button!
Magnified, it looks like this:
It doesn't look like much in the screenshots, but it looks very peculiar in real life!
I'm confused, why not modify your custom BasicButtonUI
to paint a area with rounded corners, making use of something like RoundRectangle2D
for example.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.LinearGradientPaint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.RoundRectangle2D;
import javax.swing.AbstractButton;
import javax.swing.ButtonModel;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicButtonUI;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
setLayout(new GridBagLayout());
setBackground(Color.RED);
setBorder(new EmptyBorder(23, 32, 32, 32));
JButton circleButton = new JButton("Hello");
circleButton.setUI(new CircleButtonUI());
circleButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("I've been triggered");
}
});
add(circleButton);
}
}
public class CircleButtonUI extends BasicButtonUI {
private Shape buttonShape;
@Override
public void installUI(JComponent c) {
super.installUI(c);
c.setOpaque(false);
// This is because on some platforms, the background
// is undefined or otherwise causes the button not
// to render
c.setBackground(Color.LIGHT_GRAY.brighter());
}
@Override
public boolean contains(JComponent c, int x, int y) {
if (buttonShape == null) {
return c.contains(x, y);
}
return buttonShape.contains(x, y);
}
@Override
public void paint(Graphics g, JComponent c) {
int width = c.getWidth() - 2;
int height = c.getHeight() - 2;
buttonShape = new RoundRectangle2D.Double(1, 1, width, height, 20, 20);
AbstractButton b = (AbstractButton) c;
paintContent(g, b);
super.paint(g, c);
}
protected void paintContent(Graphics g, AbstractButton b) {
if (buttonShape == null) {
return;
}
Graphics2D g2d = (Graphics2D) g.create();
// paint the interior of the button
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
ButtonModel model = b.getModel();
Color highlight = b.getBackground();
if (model.isArmed() && model.isPressed()) {
highlight = highlight.darker();
}
Color darklight = highlight.darker();
LinearGradientPaint lgp = new LinearGradientPaint(
buttonShape.getBounds().getLocation(),
new Point((int) buttonShape.getBounds().getMaxX(), (int) buttonShape.getBounds().getMaxY()),
new float[]{0, 1f},
new Color[]{highlight, darklight});
g2d.setPaint(lgp);
g2d.fill(buttonShape);
// draw the perimeter of the button
g2d.setColor(b.getBackground().darker().darker().darker());
g2d.draw(buttonShape);
g2d.dispose();
}
@Override
protected void paintFocus(Graphics g, AbstractButton b, Rectangle viewRect, Rectangle textRect, Rectangle iconRect) {
// Paint focus highlight, if you want to
}
public Dimension getMinimumSize(JComponent c) {
Dimension size = super.getMinimumSize(c);
return new Dimension(size.width + 8, size.height + 16);
}
public Dimension getPreferredSize(JComponent c) {
Dimension size = super.getPreferredSize(c);
return new Dimension(size.width + 8, size.height + 16);
}
public Dimension getMaximumSize(JComponent c) {
Dimension size = super.getPreferredSize(c);
return new Dimension(size.width + 8, size.height + 16);
}
}
}
Please note that this just an example, you'd need to do some more work to get a fully functional implementation.
So, going back to Swing: Create a UWP ("Metro")–like button, I implemented a "style" concept, so you can more easily modify the style of the button as well as implemented the "rounded" edge effect and painting the focus.
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.RoundRectangle2D;
import java.util.WeakHashMap;
import javax.swing.AbstractButton;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicButtonUI;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
setLayout(new GridBagLayout());
setBorder(new EmptyBorder(32, 32, 32, 32));
add(makeButton(MetroLookAndFeel.Style.DEFAULT));
add(makeButton(MetroLookAndFeel.Style.PRIMARY));
add(makeButton(MetroLookAndFeel.Style.SECONDARY));
add(makeButton(MetroLookAndFeel.Style.SUCCESS));
add(makeButton(MetroLookAndFeel.Style.ALERT));
add(makeButton(MetroLookAndFeel.Style.WARNING));
add(makeButton(MetroLookAndFeel.Style.INFO));
add(makeButton(MetroLookAndFeel.Style.DARK));
add(makeButton(MetroLookAndFeel.Style.LIGHT));
add(makeButton(MetroLookAndFeel.Style.LINK));
}
protected JButton makeButton(MetroLookAndFeel.Style style) {
JButton btn = new JButton("Test");
btn.setUI(new MetroLookAndFeel(style));
return btn;
}
}
public class MetroLookAndFeel extends BasicButtonUI {
public static class Style {
public static final Style DEFAULT = new Style(Color.BLACK, new Color(235, 235, 235));
public static final Style PRIMARY = new Style(Color.WHITE, new Color(3, 102, 214));
public static final Style SECONDARY = new Style(Color.WHITE, new Color(96, 125, 139));
public static final Style SUCCESS = new Style(Color.WHITE, new Color(96, 169, 23));
public static final Style ALERT = new Style(Color.WHITE, new Color(206, 53, 44));
public static final Style WARNING = new Style(Color.WHITE, new Color(255, 148, 71));
public static final Style INFO = new Style(Color.WHITE, new Color(94, 189, 236));
public static final Style DARK = new Style(Color.WHITE, new Color(80, 80, 80));
public static final Style LIGHT = new Style(Color.BLACK, new Color(248, 248, 248));
public static final Style LINK = new Style(Color.BLUE, new Color(0, 0, 0, 0));
private Color foreground;
private Color background;
private Style(Color foreground, Color background) {
this.foreground = foreground;
this.background = background;
}
public Color getForeground() {
return foreground;
}
public Color getBackground() {
return background;
}
}
private static final Border EMPTY_BORDER = new EmptyBorder(10, 14, 10, 14);
private WeakHashMap<AbstractButton, RoundRectangle2D> buttonShapeCache;
private WeakHashMap<AbstractButton, RoundRectangle2D> focusShapeCache;
private Style style;
public MetroLookAndFeel() {
this(Style.DEFAULT);
}
public MetroLookAndFeel(Style style) {
this.style = style;
focusShapeCache = new WeakHashMap<>(8);
buttonShapeCache = new WeakHashMap<>(8);
}
public Style getStyle() {
return style;
}
@Override
protected void installDefaults(AbstractButton b) {
super.installDefaults(b);
// Maybe pass the font sizing as a hint with style?
Font f = new Font("Segoe UI", Font.PLAIN, b.getFont().getSize());
b.setFont(f);
b.setOpaque(false);
b.setContentAreaFilled(false);
b.setBorder(EMPTY_BORDER);
b.setBackground(getStyle().getBackground());
b.setForeground(getStyle().getForeground());
}
protected RoundRectangle2D shapeForButton(AbstractButton button) {
RoundRectangle2D shape = buttonShapeCache.get(button);
if (shape != null) {
return shape;
}
shape = new RoundRectangle2D.Double(1, 1, button.getWidth() - 2, button.getHeight() - 2, 16, 16);
buttonShapeCache.put(button, shape);
return shape;
}
protected RoundRectangle2D focusShapeForButton(AbstractButton button) {
RoundRectangle2D shape = focusShapeCache.get(button);
if (shape != null) {
return shape;
}
shape = new RoundRectangle2D.Double(2, 2, button.getWidth() - 4, button.getHeight() - 4, 16, 16);
focusShapeCache.put(button, shape);
return shape;
}
@Override
public void paint(Graphics g, JComponent c) {
int width = c.getWidth() - 2;
int height = c.getHeight() - 2;
AbstractButton b = (AbstractButton) c;
Graphics2D g2d = (Graphics2D) g.create();
// paint the interior of the button
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
paintContent(g2d, b);
super.paint(g2d, c);
g2d.dispose();
}
protected void paintContent(Graphics2D g2d, AbstractButton button) {
Graphics2D otherG2d = (Graphics2D) g2d.create();
otherG2d.setColor(button.getBackground());
otherG2d.fill(shapeForButton(button));
if (!button.hasFocus() || !button.isFocusPainted()) {
otherG2d.setColor(button.getBackground().darker());
otherG2d.draw(shapeForButton(button));
}
otherG2d.dispose();
}
@Override
protected void paintButtonPressed(Graphics g, AbstractButton b) {
System.out.println("Pressed");
super.paintButtonPressed(g, b);
}
@Override
protected void paintFocus(Graphics g, AbstractButton button, Rectangle viewRect, Rectangle textRect, Rectangle iconRect) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.BLUE);
g2d.setStroke(new BasicStroke(2, BasicStroke.JOIN_ROUND, BasicStroke.CAP_ROUND));
g2d.draw(focusShapeForButton(button));
g2d.dispose();
}
}
}
I would be able to just tell Swing that I needed a button with rounded corners. Is this possible, or do I have to do complex (at least for me!) painting?
The short answer is no. Swing provides a plug'n'play style for it's UI components, via the Look and Feel API, this means that it's possible for a button to appear differently on different platforms.
Border
also doesn't "fill", it just paints the outline of the border itself, see How to Use Borders for more details.
This leaves you with two options. Either create a custom JButton
, making use of things like Border
and overriding paintComponent
or creating a custom Look and Feel delegate, as demonstrated above.
There are arguments for and against both, but when you need to realise is, JButton
is actually quite a complex component and requires some effort to design for.
I would also recommend taking the time to look over: