Search code examples
javaswingborderjbutton

Button corners don't get painted over by borders, they are moved outwards instead


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!
Button screenshot
Magnified, it looks like this:

It doesn't look like much in the screenshots, but it looks very peculiar in real life!


Solution

  • I'm confused, why not modify your custom BasicButtonUI to paint a area with rounded corners, making use of something like RoundRectangle2D for example.

    enter image description here

    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.

    A more "metro" style

    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.

    enter image description here

    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: