Making a Robust, Resizable Swing Chess Gui

Making a robust, resizable Swing Chess GUI

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);
}
}

Create a Chess board with JPanel

Note

The GUI seen here has been improved and moved to Making a robust, resizable Swing Chess GUI.

I will leave the animated GIF here (because it's cute) and the original, stripped down code (of just 125 code lines, the final code seen on the other thread is 218 LOC).

Chess Game Layout

import java.awt.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.border.*;

public class ChessBoardWithColumnsAndRows {

private final JPanel gui = new JPanel(new BorderLayout(3, 3));
private JButton[][] chessBoardSquares = new JButton[8][8];
private JPanel chessBoard;
private final JLabel message = new JLabel(
"Chess Champ is ready to play!");
private static final String COLS = "ABCDEFGH";

ChessBoardWithColumnsAndRows() {
initializeGui();
}

public final void initializeGui() {
// 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);
tools.add(new JButton("New")); // TODO - add functionality!
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.setBorder(new LineBorder(Color.BLACK));
gui.add(chessBoard);

// 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("" + (ii + 1),
SwingConstants.CENTER));
default:
chessBoard.add(chessBoardSquares[jj][ii]);
}
}
}
}

public final JComponent getChessBoard() {
return chessBoard;
}

public final JComponent getGui() {
return gui;
}

public static void main(String[] args) {
Runnable r = new Runnable() {

@Override
public void run() {
ChessBoardWithColumnsAndRows cb =
new ChessBoardWithColumnsAndRows();

JFrame f = new JFrame("ChessChamp");
f.add(cb.getGui());
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
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);
}
};
SwingUtilities.invokeLater(r);
}
}

Notes

  • The chess board complete with columns on the left and a row 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. Remove the margin of the button to allow them to shrink to the size of the icon. Add an ActionListener to the button and it will respond to both keyboard and mouse events.
  • The small ? in the left hand side of the GUI is meant to imply that area is 'reserved for future use'. We might use it to show lists of captured pieces, a selector for choice of piece when promoting pawns, game statistics, ...
  • The chess piece images were 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!

how to make a button listner give you the cordinates of the button that is pressed

You can simply get the pressed button via the listeners ActionEvent parameter's .getSource() method.

e.g.,

public class MyListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
AbstractButton myBtn = (AbstractButton) e.getSource();
// call whatever methods needed on myBtn
}
}

With e above being the ActionEvent method parameter for the actionPerformed method.
Then call whatever methods on the object obtained that you need to call.

Side problem: your posted code you compare Strings with ==. Don't do that since this compares references and you want to instead compare the actual text that the String contains. Use the .equals(...) method instead.

For example, if we had a JPanel that held a grid of JPanels (cleaner looking that JButtons), we could add a MouseListener to each JPanel cell, and just like action listener, we can get the pressed component.

In the code below, I've used the Swing putClientProperty method and getClientProperty to have my JPanel cells "know" what row and column they're in. Try it:

Edit: now using JLabels that can hold icons:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;

import javax.swing.*;

@SuppressWarnings("serial")
public class MyGrid extends JPanel {
public static final String ROW = "row";
public static final String COL = "col";
private static final int SIDES = 8;
private static final int CELL_SZ = 60;
private static final Dimension CELL_DIMENSION = new Dimension(CELL_SZ, CELL_SZ);
private static final Color DARK = Color.GRAY;
private static final int LT = 250;
private static final Color LIGHT = new Color(LT, LT, LT);
private JLabel[][] cells = new JLabel[SIDES][SIDES];
private Icon whiteIcon;
private Icon redIcon;

public MyGrid() {
whiteIcon = createIcon(LIGHT);
redIcon = createIcon(Color.RED);
MyMouse myMouse = new MyMouse();
setLayout(new GridLayout(SIDES, SIDES));
for (int row = 0; row < cells.length; row++) {
for (int col = 0; col < cells[row].length; col++) {
JLabel cell = new JLabel();
cell.setOpaque(true);
cell.setPreferredSize(CELL_DIMENSION);
cell.putClientProperty(ROW, row);
cell.putClientProperty(COL, col);
cell.addMouseListener(myMouse);
Color bg = row % 2 == col % 2 ? LIGHT : DARK;
if (bg.equals(DARK) && row < 3) {
cell.setIcon(redIcon);
} else if (bg.equals(DARK) && row > 4) {
cell.setIcon(whiteIcon);
}
cell.setBackground(bg);
cells[row][col] = cell;
add(cell);
}
}
}

private Icon createIcon(Color color) {
BufferedImage img = new BufferedImage(CELL_SZ, CELL_SZ, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = img.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int gap = 4;
g2.setColor(color);
g2.fillOval(gap, gap, CELL_SZ - 2 * gap, CELL_SZ - 2 * gap);
g2.dispose();
return new ImageIcon(img);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(() -> createAndShowGui());
}

private static void createAndShowGui() {
MyGrid mainPanel = new MyGrid();
JFrame frame = new JFrame("MyGrid");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(mainPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JComponent;

public class MyMouse extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
JComponent cell = (JComponent) e.getSource();
if (cell != null) {
Integer row = (Integer) cell.getClientProperty(MyGrid.ROW);
Integer col = (Integer) cell.getClientProperty(MyGrid.COL);

System.out.printf("[%d, %d]%n", row, col);
}
}
}

How to extend JComponent and use it to write a customized game board?

I only get one black square in the upper left hand corner.

That's mainly because of the following call:

g.fillRect(this.getX(), this.getY(), this.getWidth(), this.getHeight());

getX() returns the horizontal pixel offset/location of the Component which is invoked upon, relative to the Container that contains that Component. getY() accordingly returns the vertical pixel offset/location of the Component which is invoked upon, relative to the Container that contains the Component.

getWidth() and getHeight() return the size of the Component.

So imagine that the Component at row with index 2 and column with index 3, will have its coordinates at about x == 3 * w / 8 and y == 2 * h / 8 where w and h is the size (width and height respectively) of the parent Container (ie the Board_Layout panel). Let's assume that Board_Layout has a size of 300x300 when you show the graphical user interface... This means that the Square at the location I mentioned will only paint the region which starts at x == 112 and y == 75 and expands at one 8th of the width (and height) of Board_Layout (because there are 8 rows and 8 columns in the grid). But the size of the Square itself is also at one 8th of the width (and height) of Board_Layout, ie about 37x37. So the painted region which starts and expands from the location 112,75 will not be shown at all (because it lies completely outside the Square's size).

Only the top left Square will have some paint on it because its bounds in the parent happen to intersect the drawn region.

To fix this, the location given at the Graphics object should be relative to each Square and not its parent Board_Layout. For example:

g.fillRect(0, 0, getWidth(), getHeight());


Related Topics



Leave a reply



Submit