制作一个强大的、可调整大小的Swing国际象棋GUI

28

我该如何制作可调整大小的国际象棋GUI界面?


我们公司被委托制作一个国际象棋游戏。它需要在Windows、OS X和Linux/Unix机器上工作,并且我们选择了Java来实现这一点,同时保持通用的代码库(既方便维护,又能降低成本)。

我的任务是创建GUI界面。用户设计团队已经与客户明确了以下规格。

国际象棋游戏(Chess Champ)将具有可调整大小的健壮性和简单性,包括:

  • 位于顶部的工具栏,带有以下UI组件:
    • New按钮
    • Save按钮
    • Restore按钮
    • Resign按钮
    • 为玩家提供信息的标签。

在游戏的左侧,我们需要一个区域,该区域将保留用于未来使用,它可能包括以下内容:

  • 俘虏棋子的列表
  • 当升格兵时选择棋子的选择器
  • 游戏统计信息
  • 提示等。

这些细节仍在与客户和逻辑团队商定中。因此,暂时将其标记为包含“?”文本的标签。

GUI界面的其余部分将由国际象棋棋盘本身组成。它将具有:

  • 用于国际象棋棋盘的主要区域。如果用户指向一个棋子,它应该显示一个带边框的焦点。 它也应该支持键盘访问。 客户将提供多个棋子的精灵图(以各种大小、样式和颜色呈现),以允许用户更改游戏的外观。
  • 棋盘将有标签表示列 (从左往右: A, B, C, D, E, F, G 和 H) 和行 (从上到下: 8, 7, 6, 5, 4, 3, 2 和 1)。
  • 棋盘和列/行标签将被一个1像素的黑色边框包围,周围有8像素的填充。
  • 当玩家增加游戏尺寸时,棋盘应该保持正方形,但是其他方面应该填满可用空间
  • 棋盘背景颜色应该是赭色,但在下面的样例中,我们将棋盘背景变成了绿色以突出其调整大小的行为。
  • 在开始游戏之前,最小化的 Chess Champ 大小

    在开始游戏之前,最小化的Chess Champ 大小

    在点击“新游戏”按钮后,最小化的 Chess Champ 大小

    在点击“新游戏”按钮后,最小化的 Chess Champ 大小

    Chess Champ 横向拉伸超过最小大小

    Chess Champ 横向拉伸超过最小大小

    Chess Champ 纵向拉伸超过最小大小

    Chess Champ 纵向拉伸超过最小大小


    1
    请注意,这是对为使用JPanel创建国际象棋棋盘开发的代码的轻微改进。我不确定原帖作者是否已经放弃了该线程,但无论如何,他们似乎决定使用面板而不是按钮。由于规范更严格,我想把它移到自己的问答中,并且(如果原帖作者实际上已经放弃了原始帖子),我的答案可以成为“被接受的答案”。 - Andrew Thompson
    话虽如此,欢迎其他实现方式。如果它们非常出色,我将授予至少100分的奖励。此外,如果有人能够使用“null”布局来满足规范,他们将获得500分的奖励。 - Andrew Thompson
    1
    你可能需要修正你的坐标.. a1 方格应该在玩家的左手边,并且应该是黑色的。 - Dropout
    2
    刚刚查看了Wikipedia上的一个页面。它展示了棋盘的绘制方式,但数字的排列方式不同。在该页面上还显示了“5点”作为皇后的图像,并且皇后位于其自己的颜色上(这与您的相反)。 - camickr
    1
    那就是正确的方式!;) - Dropout
    显示剩余6条评论
    2个回答

    14

    注意事项

    • 棋盘与左侧和上方的列一起由9x9的GridLayout提供。网格布局的第一个单元格是一个没有文本的标签。

    • 为了简化游戏逻辑,我们仍然维护一个独立的8x8按钮数组。

    • 为了实现键盘功能,我们使用按钮来代表棋盘位置。这还提供了内置的焦点指示。删除按钮的边距,以使它们缩小到图标的大小。我们可以向按钮添加ActionListener,它将响应键盘和鼠标事件。

    • 为了保持正方形棋盘,我们采用了一些诡计。将棋盘添加到GridBagLayout中作为唯一一个未指定GridBagContraints的组件。这样它总是居中的。为了获得所需的调整大小行为,棋盘查询父组件的实际大小,并返回一个首选大小,即最大值,同时仍保持平方且不超过父对象的宽度或高度的较小值。

    • 棋子图像来自示例图像以及代码和标记Q&As,它又是从在标签中填充Unicode字符中开发出来的。

      使用图像更简单,而填充Unicode字符更具多功能性,同时也更轻量级。即支持3种不同棋子样式的3个不同大小的4种不同颜色将需要36个独立的精灵表!


    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://dev59.com/7Ww05IYBdhLWcg3wmC6_#7143398 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);
        }
    }
    

    2
    看起来我需要更新我的标准示例变体注释。 :-) - trashgod
    1
    @trashgod 谢谢!我唯一的遗憾是我不能再次给这两个答案点赞。 :) - Andrew Thompson
    +1,我看到关键在于重写getPreferredSize()方法。 - camickr
    我知道这是一些旧代码,你可能已经有一段时间没有看过了,但我想知道为什么你将chessPieceImages设置为Image对象的数组而不是ImageIcon对象。这是因为你想为每个棋子创建一个新的ImageIcon之类的东西吗? - Tejas Sharma

    8
    我注意到,在调整大小时,棋盘与右侧/底部线边界之间可能会出现小间隙。这是由于网格布局不总是可以被9整除的空间导致的。
    您可能正在寻找使用标准JDK的解决方案,但如果您想要消除这个小间隙,那么您可以使用相对布局来管理棋盘和标签。间隙仍将存在,但我已将其移到标签上,所以您不容易看到差别。
    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 https://dev59.com/7Ww05IYBdhLWcg3wmC6_#7143398 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);
        }
    }
    

    它需要更多的工作,因为您需要单独管理行,而不是在网格中。此外,我更改了您使用的代码,以便在我的较小显示器上更轻松地测试调整大小,使用48x48的图像。

    谢谢!我没有注意到这个微小的间隙,也忘记了GridLayout的这个方面。 - Andrew Thompson

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接