Hacer una GUI Swing Chess robusta y de tamaño variable

¿Cómo voy a hacer esta GUI de ajedrez a tamaño variable?


A nuestra empresa se le ha encomendado la tarea de hacer un juego de ajedrez. Tiene que funcionar en máquinas con Windows, OS X y Linux / Unix, y hemos elegido Java para lograr esto, al tiempo que mantenemos una base de código común (útil tanto para el mantenimiento como para mantener bajos los costos).

Mi tarea es crear la GUI. El equipo de diseño del usuario ha aprobado las siguientes especificaciones. con el cliente.

El juego de ajedrez (Chess Champ) será robusto al cambio de tamaño y sencillo, incluye:

  • Una barra de herramientas en la parte superior, con componentes de UI:
    • Nuevo botón
    • Botón Guardar
    • Botón Restaurar
    • Botón de Renunciar
    • Una etiqueta para proporcionar mensajes al jugador.

En el lado izquierdo del juego, necesitamos un área que se reservará para su uso futuro, podría incluir cosas como:

  • Listas de piezas capturadas
  • Un selector para la elección de la pieza cuando se promocionan los peones
  • Estadísticas del juego
  • Sugerencias, etc.

Los detalles de esto todavía se están resolviendo con el cliente y el equipo lógico. Entonces, por el momento, simplemente márcalo con una etiqueta que contenga ? como texto.

El rest de la GUI consistirá en el tablero de ajedrez en sí. Tendrá:

  • El área principal para el tablero de ajedrez. Si el usuario apunta a una pieza de ajedrez, debe mostrar el foco con un borde. También debe ser accesible desde el teclado. El cliente suministrará varias hojas de sprites de ajedrez (de una variedad de tamaños, estilos y colores) para permitir al usuario cambiar el aspecto del juego.
  • El tablero de ajedrez tendrá tags que indican las columnas (de izquierda a derecha: A, B, C, D, E, F, G y H) y filas (de arriba a abajo: 8, 7, 6, 5, 4, 3, 2 Y 1).
  • El tablero de ajedrez y las tags de columna / fila estarán bordeados por un borde negro de 1px, con un relleno de 8px alrededor.
  • A medida que el jugador aumenta el tamaño del juego, el tablero de ajedrez debe permanecer en forma cuadrada, pero llena el espacio disponible.
  • El color de fondo detrás del tablero de ajedrez debe ser ocre, pero en las maquetas a continuación hemos hecho que el área detrás del tablero de ajedrez sea verde para resaltar el comportamiento de cambio de tamaño.

Ajedrez Champ en el tamaño mínimo, antes de que se inicie un juego

ChessChamp al tamaño mínimo, antes de que se inicie un juego

Chess Champ en el tamaño mínimo, después de que se active el nuevo botón de juego

Chess Champ en el tamaño mínimo, después de que se active el nuevo botón de juego

Chess Champ se extendió más ancho que el tamaño mínimo

ChessChamp se extendía más ancho que el tamaño mínimo

Chess Champ se estiró más alto que el tamaño mínimo

Chess Champ se estiró más alto que el tamaño mínimo

Notas

  • El tablero de ajedrez completo con columnas a la izquierda y arriba es provisto por un GridLayout 9×9. La primera celda del diseño de la cuadrícula es una etiqueta sin texto.

  • Sin embargo, para simplificar la lógica del juego, mantenemos una matriz de botones 8×8 por separado.

  • Para permitir la funcionalidad del teclado, usamos botones para los lugares del tablero de ajedrez. Esto también proporciona una indicación de enfoque incorporada. El margen del botón se elimina para permitir que se contraigan al tamaño del icono. Podemos agregar un ActionListener al botón y responderá tanto al teclado como al mouse.

  • Para mantener un tablero cuadrado, empleamos un pequeño truco. El tablero de ajedrez se agrega a un GridBagLayout como el único componente sin GridBagContraints especificado. De esa manera, siempre está centrado. Para obtener el comportamiento de cambio de tamaño requerido, el tablero de ajedrez consulta el tamaño real del componente principal y devuelve un tamaño preferido que es el máximo que puede, mientras que sigue siendo cuadrado y no excede el tamaño más pequeño del ancho o alto del elemento principal.

  • La imagen de la pieza de ajedrez se obtuvo de las imágenes de ejemplo para las preguntas y respuestas sobre el código y el marcado , que a su vez se desarrolló a partir de caracteres ‘Rellenar’ Unicode en las tags .

    Usar imágenes es más simple, mientras que rellenar caracteres Unicode es más versátil además de ser más “ligero”. ¡IE para apoyar 4 colores diferentes en 3 tamaños separados de 3 estilos diferentes de la pieza de ajedrez requeriría 36 hojas de sprite separadas!


 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://sofes.miximages.com/layout-manager/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); } } 

Observé que al cambiar el tamaño puede obtener un pequeño espacio entre el tablero de ajedrez y el borde de la línea derecha / inferior. Esto sucede con un GridLayout porque el espacio no siempre es divisible por 9.

Probablemente esté buscando soluciones usando el estándar JDK, pero si desea deshacerse de este pequeño espacio, entonces puede usar el diseño relativo para administrar el tablero de ajedrez y las tags. La brecha seguirá existiendo pero la he movido a las tags para que no pueda ver la diferencia fácilmente.

 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://sofes.miximages.com/layout-manager/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); } } 

Requiere más trabajo porque necesita administrar las filas por separado, no en una cuadrícula. Además, cambio el código que usa una imagen de 48x48 para facilitar el cambio de tamaño de prueba en mi monitor más pequeño.