How would I go about making this resizable Chess GUI?
Our company has been tasked with making a Chess game. It needs to work on Windows, OS X and Linux/Unix machines, and we have chosen Java to achieve this, while maintaining a common code-base (handy for both maintenance, and keeping costs down).
My task is to create the GUI. The User Design Team has cleared the following spec. with the client.
The chess game (Chess Champ) will be robust to resizing and straightforward, it includes:
- A tool-bar at the top, with UI components:
- New button
- Save button
- Restore button
- Resign button
- A label for providing messages to the player.
On the left hand side of the game, we need an area which will be reserved for future use, it might include things like:
- Lists of captured pieces
- A selector for choice of piece when promoting pawns
- Game statistics
- Hints, etc.
The details of this are are still being settled with the client and the Logic Team. So for the moment, simply mark it with a label containing
?
as text.The rest of the GUI will consist of the chess board itself. It will have:
- The main area for the chess board. If the user points at a chess piece, it should show focus with a border. It should also be keyboard accessible. The client will be supplying multiple sprite sheets of chess pieces (of a variety of sizes, styles and colors) to allow the user to change the look of the game.
- The chess board will have labels indicating the columns (left to right: A, B, C, D, E, F, G & H) and rows (top to bottom: 8, 7, 6, 5, 4, 3, 2 & 1).
- The chess board and column/row labels will be bordered by a 1px black border, with an 8px padding around that.
- As the player increases the size of the game, the chess board should remain square, but otherwise fill the available space.
- The background color behind the chess board should be ochre, but in the mock-ups below we have made the area behind the chess board green in order to highlight the resize behavior.
The chess board complete with columns on the left and above it is provided by a 9x9 GridLayout
. The first cell of the grid layout is a label with no text.
To simplify the game logic though, we maintain a separate 8x8 array of buttons.
To allow keyboard functionality we use buttons for the chess board places. This also provides inbuilt focus indication. The margin of the button is removed to allow them to shrink to the size of the icon. We can add an ActionListener
to the button and it will respond to both keyboard and mouse events.
To maintain a square board, we employ a little trickery. The chess board is added to a GridBagLayout
as the only component with no GridBagContraints
specified. That way it is always centered. To get the resizing behavior required, the chess board queries the actual size of the parent component, and returns a preferred size that is the maximum it can, while still square and not exceeding the smaller size of the width or height of the parent.
The chess piece image was obtained from Example images for code and mark-up Q&As, which was in turn developed out of 'Fill' Unicode characters in labels.
Using images is simpler, whereas filling Unicode characters is more versatile as well as being 'lighter'. I.E. to support 4 different colors in 3 separate sizes of 3 different chess piece styles would require 36 separate sprite sheets!
import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; import javax.swing.*; import javax.swing.border.*; import java.net.URL; import javax.imageio.ImageIO; public class ChessGUI { private final JPanel gui = new JPanel(new BorderLayout(3, 3)); private JButton[][] chessBoardSquares = new JButton[8][8]; private Image[][] chessPieceImages = new Image[2][6]; private JPanel chessBoard; private final JLabel message = new JLabel( "Chess Champ is ready to play!"); private static final String COLS = "ABCDEFGH"; public static final int QUEEN = 0, KING = 1, ROOK = 2, KNIGHT = 3, BISHOP = 4, PAWN = 5; public static final int[] STARTING_ROW = { ROOK, KNIGHT, BISHOP, KING, QUEEN, BISHOP, KNIGHT, ROOK }; public static final int BLACK = 0, WHITE = 1; ChessGUI() { initializeGui(); } public final void initializeGui() { // create the images for the chess pieces createImages(); // set up the main GUI gui.setBorder(new EmptyBorder(5, 5, 5, 5)); JToolBar tools = new JToolBar(); tools.setFloatable(false); gui.add(tools, BorderLayout.PAGE_START); Action newGameAction = new AbstractAction("New") { @Override public void actionPerformed(ActionEvent e) { setupNewGame(); } }; tools.add(newGameAction); tools.add(new JButton("Save")); // TODO - add functionality! tools.add(new JButton("Restore")); // TODO - add functionality! tools.addSeparator(); tools.add(new JButton("Resign")); // TODO - add functionality! tools.addSeparator(); tools.add(message); gui.add(new JLabel("?"), BorderLayout.LINE_START); chessBoard = new JPanel(new GridLayout(0, 9)) { /** * Override the preferred size to return the largest it can, in * a square shape. Must (must, must) be added to a GridBagLayout * as the only component (it uses the parent as a guide to size) * with no GridBagConstaint (so it is centered). */ @Override public final Dimension getPreferredSize() { Dimension d = super.getPreferredSize(); Dimension prefSize = null; Component c = getParent(); if (c == null) { prefSize = new Dimension( (int)d.getWidth(),(int)d.getHeight()); } else if (c!=null && c.getWidth()>d.getWidth() && c.getHeight()>d.getHeight()) { prefSize = c.getSize(); } else { prefSize = d; } int w = (int) prefSize.getWidth(); int h = (int) prefSize.getHeight(); // the smaller of the two sizes int s = (w>h ? h : w); return new Dimension(s,s); } }; chessBoard.setBorder(new CompoundBorder( new EmptyBorder(8,8,8,8), new LineBorder(Color.BLACK) )); // Set the BG to be ochre Color ochre = new Color(204,119,34); chessBoard.setBackground(ochre); JPanel boardConstrain = new JPanel(new GridBagLayout()); boardConstrain.setBackground(ochre); boardConstrain.add(chessBoard); gui.add(boardConstrain); // create the chess board squares Insets buttonMargin = new Insets(0, 0, 0, 0); for (int ii = 0; ii < chessBoardSquares.length; ii++) { for (int jj = 0; jj < chessBoardSquares[ii].length; jj++) { JButton b = new JButton(); b.setMargin(buttonMargin); // our chess pieces are 64x64 px in size, so we'll // 'fill this in' using a transparent icon.. ImageIcon icon = new ImageIcon( new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB)); b.setIcon(icon); if ((jj % 2 == 1 && ii % 2 == 1) //) { || (jj % 2 == 0 && ii % 2 == 0)) { b.setBackground(Color.WHITE); } else { b.setBackground(Color.BLACK); } chessBoardSquares[jj][ii] = b; } } /* * fill the chess board */ chessBoard.add(new JLabel("")); // fill the top row for (int ii = 0; ii < 8; ii++) { chessBoard.add( new JLabel(COLS.substring(ii, ii + 1), SwingConstants.CENTER)); } // fill the black non-pawn piece row for (int ii = 0; ii < 8; ii++) { for (int jj = 0; jj < 8; jj++) { switch (jj) { case 0: chessBoard.add(new JLabel("" + (9-(ii + 1)), SwingConstants.CENTER)); default: chessBoard.add(chessBoardSquares[jj][ii]); } } } } public final JComponent getGui() { return gui; } private final void createImages() { try { URL url = new URL("http://i.stack.imgur.com/memI0.png"); BufferedImage bi = ImageIO.read(url); for (int ii = 0; ii < 2; ii++) { for (int jj = 0; jj < 6; jj++) { chessPieceImages[ii][jj] = bi.getSubimage( jj * 64, ii * 64, 64, 64); } } } catch (Exception e) { e.printStackTrace(); System.exit(1); } } /** * Initializes the icons of the initial chess board piece places */ private final void setupNewGame() { message.setText("Make your move!"); // set up the black pieces for (int ii = 0; ii < STARTING_ROW.length; ii++) { chessBoardSquares[ii][0].setIcon(new ImageIcon( chessPieceImages[BLACK][STARTING_ROW[ii]])); } for (int ii = 0; ii < STARTING_ROW.length; ii++) { chessBoardSquares[ii][1].setIcon(new ImageIcon( chessPieceImages[BLACK][PAWN])); } // set up the white pieces for (int ii = 0; ii < STARTING_ROW.length; ii++) { chessBoardSquares[ii][6].setIcon(new ImageIcon( chessPieceImages[WHITE][PAWN])); } for (int ii = 0; ii < STARTING_ROW.length; ii++) { chessBoardSquares[ii][7].setIcon(new ImageIcon( chessPieceImages[WHITE][STARTING_ROW[ii]])); } } public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { ChessGUI cg = new ChessGUI(); JFrame f = new JFrame("ChessChamp"); f.add(cg.getGui()); // Ensures JVM closes after frame(s) closed and // all non-daemon threads are finished f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // See https://stackoverflow.com/a/7143398/418556 for demo. f.setLocationByPlatform(true); // ensures the frame is the minimum size it needs to be // in order display the components within it f.pack(); // ensures the minimum size is enforced. f.setMinimumSize(f.getSize()); f.setVisible(true); } }; // Swing GUIs should be created and updated on the EDT // http://docs.oracle.com/javase/tutorial/uiswing/concurrency SwingUtilities.invokeLater(r); } }
I notice that when resizing you can get a small gap between the chessboard and the right/botton line border. This happens with a GridLayout because the space is not always divisible by 9.
You are probably looking for solutions using the standard JDK, but if you would like to get rid of this small gap then you can use the Relative Layout to manage the chess board and labels. The gap will still exist but I have moved it to the labels so you can't easily see the difference.
import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; import javax.swing.*; import javax.swing.border.*; import java.net.URL; import javax.imageio.ImageIO; public class ChessGUI2 { private final JPanel gui = new JPanel(new BorderLayout(3, 3)); private JButton[][] chessBoardSquares = new JButton[8][8]; private Image[][] chessPieceImages = new Image[2][6]; private JPanel chessBoard; private final JLabel message = new JLabel( "Chess Champ is ready to play!"); private static final String COLS = "ABCDEFGH"; public static final int QUEEN = 0, KING = 1, ROOK = 2, KNIGHT = 3, BISHOP = 4, PAWN = 5; public static final int[] STARTING_ROW = { ROOK, KNIGHT, BISHOP, KING, QUEEN, BISHOP, KNIGHT, ROOK }; ChessGUI2() { initializeGui(); } public final void initializeGui() { // create the images for the chess pieces createImages(); // set up the main GUI gui.setBorder(new EmptyBorder(5, 5, 5, 5)); JToolBar tools = new JToolBar(); tools.setFloatable(false); gui.add(tools, BorderLayout.PAGE_START); Action newGameAction = new AbstractAction("New") { @Override public void actionPerformed(ActionEvent e) { setupNewGame(); } }; tools.add(newGameAction); tools.add(new JButton("Save")); // TODO - add functionality! tools.add(new JButton("Restore")); // TODO - add functionality! tools.addSeparator(); tools.add(new JButton("Resign")); // TODO - add functionality! tools.addSeparator(); tools.add(message); gui.add(new JLabel("?"), BorderLayout.LINE_START); // chessBoard = new JPanel(new GridLayout(0, 9)) { chessBoard = new JPanel() { /** * Override the preferred size to return the largest it can, in * a square shape. Must (must, must) be added to a GridBagLayout * as the only component (it uses the parent as a guide to size) * with no GridBagConstaint (so it is centered). */ @Override public final Dimension getPreferredSize() { Dimension d = super.getPreferredSize(); Dimension prefSize = null; Component c = getParent(); if (c == null) { prefSize = new Dimension( (int)d.getWidth(),(int)d.getHeight()); } else if (c!=null && c.getWidth()>d.getWidth() && c.getHeight()>d.getHeight()) { prefSize = c.getSize(); } else { prefSize = d; } int w = (int) prefSize.getWidth(); int h = (int) prefSize.getHeight(); // the smaller of the two sizes int s = (w>h ? h : w); return new Dimension(s,s); } }; RelativeLayout rl = new RelativeLayout(RelativeLayout.Y_AXIS); rl.setRoundingPolicy( RelativeLayout.FIRST ); rl.setFill(true); chessBoard.setLayout( rl ); chessBoard.setBorder(new CompoundBorder( new EmptyBorder(8,8,8,8), new LineBorder(Color.BLACK) )); // Set the BG to be ochre Color ochre = new Color(204,119,34); chessBoard.setBackground(ochre); JPanel boardConstrain = new JPanel(new GridBagLayout()); boardConstrain.setBackground(ochre); boardConstrain.add(chessBoard); gui.add(boardConstrain); // our chess pieces are 64x64 px in size, so we'll // 'fill this in' using a transparent icon.. ImageIcon icon = new ImageIcon( //new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB)); new BufferedImage(48, 48, BufferedImage.TYPE_INT_ARGB)); // create the chess board squares Insets buttonMargin = new Insets(0, 0, 0, 0); for (int ii = 0; ii < chessBoardSquares.length; ii++) { for (int jj = 0; jj < chessBoardSquares[ii].length; jj++) { JButton b = new JButton(); b.setMargin(buttonMargin); b.setIcon(icon); if ((jj % 2 == 1 && ii % 2 == 1) //) { || (jj % 2 == 0 && ii % 2 == 0)) { b.setBackground(Color.WHITE); } else { b.setBackground(Color.BLACK); } chessBoardSquares[jj][ii] = b; } } /* * fill the chess board */ RelativeLayout topRL = new RelativeLayout(RelativeLayout.X_AXIS); topRL.setRoundingPolicy( RelativeLayout.FIRST ); topRL.setFill(true); JPanel top = new JPanel( topRL ); top.setOpaque(false); chessBoard.add(top, new Float(1)); top.add(new JLabel(""), new Float(1)); // fill the top row for (int ii = 0; ii < 8; ii++) { JLabel label = new JLabel(COLS.substring(ii, ii + 1), SwingConstants.CENTER); top.add(label, new Float(1)); } // fill the black non-pawn piece row for (int ii = 0; ii < 8; ii++) { RelativeLayout rowRL = new RelativeLayout(RelativeLayout.X_AXIS); rowRL.setRoundingPolicy( RelativeLayout.FIRST ); rowRL.setFill(true); JPanel row = new JPanel( rowRL ); row.setOpaque(false); chessBoard.add(row, new Float(1)); for (int jj = 0; jj < 8; jj++) { switch (jj) { case 0: row.add(new JLabel("" + (9-(ii + 1)), SwingConstants.CENTER), new Float(1)); default: row.add(chessBoardSquares[jj][ii], new Float(1)); } } } } public final JComponent getChessBoard() { return chessBoard; } public final JComponent getGui() { return gui; } private final void createImages() { try { URL url = new URL("http://i.stack.imgur.com/memI0.png"); BufferedImage bi = ImageIO.read(url); for (int ii = 0; ii < 2; ii++) { for (int jj = 0; jj < 6; jj++) { chessPieceImages[ii][jj] = bi.getSubimage( // jj * 64, ii * 64, 64, 64); jj * 64, ii * 64, 48, 48); } } } catch (Exception e) { e.printStackTrace(); System.exit(1); } } /** * Initializes the icons of the initial chess board piece places */ private final void setupNewGame() { message.setText("Make your move!"); // set up the black pieces for (int ii = 0; ii < STARTING_ROW.length; ii++) { chessBoardSquares[ii][0].setIcon(new ImageIcon( chessPieceImages[0][STARTING_ROW[ii]])); } for (int ii = 0; ii < STARTING_ROW.length; ii++) { chessBoardSquares[ii][1].setIcon(new ImageIcon( chessPieceImages[0][PAWN])); } // set up the white pieces for (int ii = 0; ii < STARTING_ROW.length; ii++) { chessBoardSquares[ii][6].setIcon(new ImageIcon( chessPieceImages[1][PAWN])); } for (int ii = 0; ii < STARTING_ROW.length; ii++) { chessBoardSquares[ii][7].setIcon(new ImageIcon( chessPieceImages[1][STARTING_ROW[ii]])); } } public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { ChessGUI2 cg = new ChessGUI2(); JFrame f = new JFrame("ChessChamp"); f.add(cg.getGui()); // Ensures JVM closes after frame(s) closed and // all non-daemon threads are finished f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // See http://stackoverflow.com/a/7143398/418556 for demo. f.setLocationByPlatform(true); // ensures the frame is the minimum size it needs to be // in order display the components within it f.pack(); // ensures the minimum size is enforced. f.setMinimumSize(f.getSize()); f.setVisible(true); } }; // Swing GUIs should be created and updated on the EDT // http://docs.oracle.com/javase/tutorial/uiswing/concurrency SwingUtilities.invokeLater(r); } }
It does require more work because you need to manage the rows separately, not in a grid. Also, I change the code you use a 48x48 image to make testing resizing easier on my smaller monitor.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With