Java 制作一个健壮的、可调整大小的 Swing Chess GUI

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/21142686/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 06:53:00  来源:igfitidea点击:

Making a robust, resizable Swing Chess GUI

javaswinguser-interfacelayout-manager

提问by Andrew Thompson

How would I go about making this resizable Chess GUI?

我将如何制作这个可调整大小的国际象棋 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).

我们公司的任务是制作国际象棋游戏。它需要在 Windows、OS X 和 Linux/Unix 机器上运行,我们选择 Java 来实现这一点,同时维护一个通用的代码库(便于维护和降低成本)。

My task is to create the GUI. The User Design Team has cleared the following spec. with the client.

我的任务是创建 GUI。用户设计团队已清除以下规范。与客户。

The chess game (Chess Champ) will be robust to resizing and straightforward, it includes:

  • A tool-bar at the top, with UI components:
    • Newbutton
    • Savebutton
    • Restorebutton
    • Resignbutton
    • 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) 将具有强大的调整大小和简单明了的功能,它包括:

  • 顶部的工具栏,带有 UI 组件:
    • New按钮
    • Save按钮
    • Restore按钮
    • Resign按钮
    • 用于向玩家提供消息的标签。

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

  • 捕获的零件列表
  • 提升棋子时选择棋子的选择器
  • 比赛统计
  • 提示等。

细节仍在与客户和逻辑团队解决。所以目前,只需用一个包含?文本的标签来标记它。

GUI 的其余部分将由棋盘本身组成。它将有:

  • 棋盘的主要区域。如果用户指向一个棋子,它应该显示带有边框的焦点。它也应该是键盘可访问的。客户将提供多个棋子的精灵表(各种尺寸、样式和颜色),以允许用户更改游戏的外观。
  • 国际象棋棋盘将有标签指示列(从左到右:A、B、C、D、E、F、G 和 H)和行(从上到下:8、7、6、5、4、3、2 & 1)。
  • 棋盘和列/行标签将被 1px 的黑色边框包围,周围有 8px 的填充。
  • 随着玩家增加游戏的大小,棋盘应保持方形,否则会填满可用空间。
  • 棋盘后面的背景颜色应该是赭色,但在下面的模型中,我们将棋盘后面的区域设为绿色,以突出调整大小的行为。

Chess Champ at minimum size, before a game is started

最小尺寸的国际象棋冠军,在比赛开始之前

ChessChamp at minimum size, before a game is started

ChessChamp 最小尺寸,在比赛开始前

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

ChessChamp 拉伸得比最小尺寸更宽

Chess Champ stretched taller than minimum size

国际象棋冠军比最小尺寸拉伸得更高

Chess Champ stretched taller than minimum size

国际象棋冠军比最小尺寸拉伸得更高

采纳答案by Andrew Thompson

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 ActionListenerto 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 GridBagLayoutas the only component with no GridBagContraintsspecified. 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!

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

  • 不过,为了简化游戏逻辑,我们维护了一个单独的 8x8 按钮阵列。

  • 为了允许键盘功能,我们在棋盘位置使用按钮。这也提供了内置的焦点指示。删除按钮的边距以允许它们缩小到图标的大小。我们可以ActionListener向按钮添加一个,它会同时响应键盘和鼠标事件。

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

  • 棋子图像是从代码和标记问答的示例图像中获得的,而这些图像又是从标签中“填充”Unicode 字符中开发出来的。

    使用图像更简单,而填充 Unicode 字符更通用,而且“更轻”。IE 支持 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://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);
    }
}

回答by camickr

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.

我注意到在调整大小时,您可以在棋盘和右侧/底部线边框之间获得一个小间隙。GridLayout 会发生这种情况,因为空间并不总是可以被 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 Layoutto 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.

您可能正在寻找使用标准 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 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.

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