Skip to content using Screen Reader
Shopping Cart Shopping Cart
Back to Articles

How do you Make those Buttons?

The state of Nebraska has a rectangular shape, with a rounded northeast corner and a notched southwest corner. That suggested the buttons in our Game Book, which have rounded northwest and southeast corners.

Controls Dialog from the Game Book


The Java Swing and Java2D APIs make it easy to add custom-shaped buttons to your user interface. Architecturally, you just:

  • Subclass JButton
  • Add code to set the shape
  • Override the paintComponent() and paintBorder() methods to render the shape


But there are a few tricks.

Make the Fill Shape

Our overridden paintComponent() method will use a fill shape. Here we use the Java2D API Shape.add() functionality to compose the Nebraska shape.

We start with a RoundedRectangle. Note that arcLength and arcHeight are divided by 2 when they get into the RoundedRectangle object.

We then add a normal Rectangle at the top, offset to the right, to add the square northeast corner. We add a second Rectangle at the bottom and left to add the square southwest corner.

Make the Border

We use the same technique to create the draw shape used for the border.

Make the Shape Respond to the Text

We put the setShape() functionality in its own method for needs just such as this. Here we use the method setButtonText() to make the shape responsive to the text and font.

Make the Shape Resizable

We make the NebraskaButton its own ComponentListener so it can resize and reshape itself per its container and that container's LayoutManager.

Make the Shape Detectable

Override the contains() method so that containers can detect our new shape and not just the bounding rectangle.

Paint the Component

Here's where we override the paintComponent() method.

We use Java2D RenderingHints to avoid the jaggies when we render rounded shapes.

We use the ButtonModel to get the button's state so that our code can determine what color to paint the background.

Note that we use the isArmed() method rather than the isPressed() method. This is because we want our button to NOT trigger when the mouse moves out of the bounding area defined in 5. above. Had we used isPressed(), our button would have remained pressed when the mouse moved outside its bounds. This could confuse the user as to whether or not the button actually was pressed.

Paint the Border

We've chosen the Art Deco style for our application. So here we want our button to have a thick black border when it does not have focus. And to have a thick white border when it does have focus. Our overridden paintBorder() method does this.

Make the Button Responsive to the Enter Key

Java is curious in that buttons are triggered by the Space Bar, but not by the Enter key; unless a button is set to be the "default" button for a frame. This seems to me to be counter-intuitive. Therefore, here we make our button a KeyListener of itself; and trigger a doClick() when the Enter key is pressed.

Last Words

We add a self-test that demonstrates the effects of focus traversal, mouse, and key input.

Code


import java.awt.geom.*;
import java.awt.*;
import java.awt.event.*;

/**
 * This is example code. Feel free to copy it.
 *
 * @author John Bannick, 7-128 Software
 * @version 1.0.0
 */

public class NebraskaButton extends JButton
  implements ComponentListener, KeyListener{

  protected static final int    DEFAULT_WIDTH  = 150;
  protected static final int    DEFAULT_HEIGHT = 50;

  protected static final Insets INSETS_MARGIN = new Insets(2,5,2,5);

  protected static final int    BORDER_WIDTH  = 5;

  protected double           m_dWidthFill     = 0d;
  protected double           m_dHeightFill    = 0d;

  protected Shape            m_shape          = null;
  protected Area             m_areaFill       = null;
  protected Area             m_areaDraw       = null;

  protected RoundRectangle2D m_rrect2dFill     = null;
  protected Rectangle2D      m_rect2dAFill     = null;
  protected Rectangle2D      m_rect2dBFill     = null;

  protected double           m_dWidthDraw      = 0d;
  protected double           m_dHeightDraw     = 0d;

  protected RoundRectangle2D m_rrect2dDraw     = null;
  protected Rectangle2D      m_rect2dADraw     = null;
  protected Rectangle2D      m_rect2dBDraw     = null;

  protected int              m_nStringWidthMax = 0;
  protected int              m_nMinWidth       = 0;

  ////////////////////////////////////////////////
  public NebraskaButton(String strLabel){
    this(strLabel, 0);
  }
  ////////////////////////////////////////////////
  public NebraskaButton(String strLabel, int nMinWidth){
    super(strLabel);

    m_nMinWidth = nMinWidth;

    this.setContentAreaFilled(false);
    this.setMargin(INSETS_MARGIN);
    this.setFocusPainted(false);

    this.addComponentListener(this);
    this.addKeyListener(this);

    //determine the buttons initial size ----------------------

    //WARNING: Use UIManager font, else font here is not dynamic
    Font        font  = (Font)UIManager.get("Button.font");
    Frame       frame = JOptionPane.getRootFrame();
    FontMetrics fm    = frame.getFontMetrics(font);

    m_nStringWidthMax = fm.stringWidth(this.getText());
    m_nStringWidthMax =
      Math.max(m_nStringWidthMax, fm.stringWidth(this.getText()));

    //WARNING: use getMargin. it refers to dist btwn text and border.
    //also use getInsets. it refers to the width of the border
    int nWidth  = Math.max(m_nMinWidth,
      m_nStringWidthMax +
      this.getMargin().left +
      this.getInsets().left +
      this.getMargin().right +
      this.getInsets().right);

    this.setPreferredSize(new Dimension(nWidth, DEFAULT_HEIGHT));

    //set the initial draw and fill dimensions ------------------

    m_dWidthFill  = (double)this.getPreferredSize().width-1;
    m_dHeightFill = (double)this.getPreferredSize().height-1;

    m_dWidthDraw  =
      ((double)this.getPreferredSize().width-1) - (BORDER_WIDTH - 1);
    m_dHeightDraw =
      ((double)this.getPreferredSize().height-1)- (BORDER_WIDTH - 1);

    this.setShape();
  }
  ////////////////////////////////////////////////
  public void setButtonText(String strText){
      super.setText(strText);

    int nWidth  = Math.max(
      m_nMinWidth,
      m_nStringWidthMax + 
      this.getInsets().left + 
      this.getInsets().right);
    int nHeight = Math.max(0,  this.getPreferredSize().height);
    this.setPreferredSize(new Dimension(nWidth, nHeight));

    m_dWidthFill  = this.getBounds().width  - 1;
    m_dHeightFill = this.getBounds().height - 1;

    if(m_dWidthFill <= 0 || m_dHeightFill <= 0){
      m_dWidthFill  = (double)this.getPreferredSize().width  - 1;
      m_dHeightFill = (double)this.getPreferredSize().height - 1;
    }

    m_dWidthDraw  = m_dWidthFill  - (BORDER_WIDTH - 1);
    m_dHeightDraw = m_dHeightFill - (BORDER_WIDTH - 1);

    this.setShape();
  }
  ////////////////////////////////////////////////
  protected void setShape(){

    //area --------------------------------------

    double dArcLengthFill = Math.min(m_dWidthFill, m_dHeightFill);
    double dOffsetFill = dArcLengthFill / 2;

    m_rrect2dFill = new RoundRectangle2D.Double(
      0d, 0d, m_dWidthFill, m_dHeightFill, 
      dArcLengthFill, dArcLengthFill);
    //WARNING: arclength and archeight are divided by 2
    //when they get into the roundedrectangle shape

    m_rect2dAFill = new Rectangle2D.Double(
      0d, dOffsetFill, m_dWidthFill - dOffsetFill, 
      m_dHeightFill - dOffsetFill);
    m_rect2dBFill = new Rectangle2D.Double(
      dOffsetFill, 0d, m_dWidthFill - dOffsetFill, 
    m_dHeightFill - dOffsetFill);

    m_areaFill = new Area(m_rrect2dFill);
    m_areaFill.add(new Area(m_rect2dAFill));
    m_areaFill.add(new Area(m_rect2dBFill));

    //border ------------------------------------------------

    double dArcLengthDraw = Math.min(m_dWidthDraw, m_dHeightDraw);
    double dOffsetDraw = dArcLengthDraw / 2;

    m_rrect2dDraw = new RoundRectangle2D.Double(
      (BORDER_WIDTH - 1) / 2,
      (BORDER_WIDTH - 1) / 2,
      m_dWidthDraw,
      m_dHeightDraw,
      dArcLengthDraw,
      dArcLengthDraw);

    m_rect2dADraw = new Rectangle2D.Double(
      (BORDER_WIDTH - 1) / 2,
      dOffsetDraw + (BORDER_WIDTH - 1) / 2,
      m_dWidthDraw - dOffsetDraw,
      m_dHeightDraw - dOffsetDraw);

    m_rect2dBDraw = new Rectangle2D.Double(
      dOffsetDraw + (BORDER_WIDTH - 1) / 2,
      (BORDER_WIDTH - 1) / 2,
      m_dWidthDraw - dOffsetDraw,
      m_dHeightDraw - dOffsetDraw);

    m_areaDraw = new Area(m_rrect2dDraw);
    m_areaDraw.add(new Area(m_rect2dADraw));
    m_areaDraw.add(new Area(m_rect2dBDraw));
  }
  ////////////////////////////////////////////////
  protected void paintComponent(Graphics g){

    Graphics2D g2 = (Graphics2D)g;

    RenderingHints hints = new RenderingHints(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON
      );
    g2.setRenderingHints(hints);

    if(getModel().isArmed()){
      g2.setColor(Color.cyan);
    }
    else{
      if (this.hasFocus()) {
        g2.setColor(Color.blue);
      }
      else {
        g2.setColor(Color.yellow);
      }
    }

    g2.fill(m_areaFill);

    super.paintComponent(g2);
  }
  ////////////////////////////////////////////////
  protected void paintBorder(Graphics g){

    Graphics2D g2 = (Graphics2D)g;

    RenderingHints hints = new RenderingHints(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON
      );
    g2.setRenderingHints(hints);

    g2.setColor(Color.black);

    Stroke strokeOld = g2.getStroke();
    g2.setStroke(
      new BasicStroke(
        BORDER_WIDTH, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)
    );
    g2.draw(m_areaDraw);

    if(this.hasFocus()){
      g2.setColor(Color.white);
      g2.draw(m_areaDraw);
    }

    g2.setStroke(strokeOld);
  }
  ////////////////////////////////////////////////
  public boolean contains(int nX, int nY){
    if(null == m_shape || m_shape.getBounds().equals(getBounds())){
      m_shape = new Rectangle2D.Float(
        0, 0, this.getBounds().width, this.getBounds().height);
    }
    return m_shape.contains(nX, nY);
  }
  ////////////////////////////////////////////////
  ////////////////////////////////////////////////
  //Needed if we want this button to resize
  public void componentResized(ComponentEvent e){
    m_shape = new Rectangle2D.Float(
      0, 0, this.getBounds().width, this.getBounds().height);

    m_dWidthFill  = (double)this.getBounds().width - 1;
    m_dHeightFill = (double)this.getBounds().height -1;

    m_dWidthDraw  = ((double)this.getBounds().width-1) - 
      (BORDER_WIDTH - 1);
    m_dHeightDraw = ((double)this.getBounds().height-1)- 
      (BORDER_WIDTH - 1);

    this.setShape();
  };
  ////////////////////////////////////////////////
  public void componentHidden(ComponentEvent e){};
  public void componentMoved(ComponentEvent e){};
  public void componentShown(ComponentEvent e){};
  ////////////////////////////////////////////////
  ////////////////////////////////////////////////
  //This is so the button is triggered when it has focus 
  //and we press the Enter key.
  public void keyPressed(KeyEvent e){
    if(e.getSource() == this && e.getKeyCode() == KeyEvent.VK_ENTER){
      this.doClick();
    }
  }
  ////////////////////////////////////////////////
  public void keyReleased(KeyEvent e){}
  public void keyTyped(KeyEvent e){}
  ////////////////////////////////////////////////
  ////////////////////////////////////////////////
  public static void main(String[] args){
    JFrame frame = new JFrame("Nebraska Button Tester");

    frame.getContentPane().setLayout(new FlowLayout());
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    frame.getContentPane().add(new NebraskaButton("OK", 100));
    frame.getContentPane().add(new NebraskaButton("Cancel", 100));
    frame.getContentPane().add(new NebraskaButton("Help", 100));

    frame.pack();
    frame.setVisible(true);
  }
}

John Bannick
Chief Technical Officer
7-128 Software

jbannick@7128.com