Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making a robust, resizable Swing Chess GUI [closed]

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.

Chess Champ at minimum size, before a game is started

ChessChamp at minimum size, before a game is started

Chess Champ at minimum size, after the new game button is activated

Chess Champ at minimum size, after the new game button is activated

Chess Champ stretched wider than minimum size

ChessChamp stretched wider than minimum size

Chess Champ stretched taller than minimum size

Chess Champ stretched taller than minimum size

like image 242
Andrew Thompson Avatar asked Jan 15 '14 16:01

Andrew Thompson


2 Answers

Notes

  • 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);     } } 
like image 182
Andrew Thompson Avatar answered Sep 17 '22 03:09

Andrew Thompson


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.

like image 25
camickr Avatar answered Sep 19 '22 03:09

camickr