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.

The Java Swing and Java2D APIs make it easy to add custom-shaped buttons to your user interface. Architecturally,
you just:
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.
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