Search code examples
javaswingjpaneljlabelreversi

Updating a JLabel while the program's running


I'm currently trying to program a GUI for a reversi game(https://en.wikipedia.org/wiki/Reversi), using Java Swing. A part of my task is to print the current number of player's (blue) and machine's (red) tiles at the bottom of my frame Configuration in the beginning.

I have created a JLabel for each type of tiles and added it to their own JPanel. However, when I try to update the labels, whole panel starts to either replicate itself infinitely Replicates the whole board with each turn, or doesn't change the label at all. I tried writing the setText-command at every possible place, and still nothing works. What am I doing wrong? Where should I update the labels?

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Arrays;
import java.util.Objects;

import static javax.swing.WindowConstants.EXIT_ON_CLOSE;

public class example {
    JPanel panel = new JPanel();
    private final int width = 60;
    private final int diameter = (int) Math.round(width * 0.85);
    private final int padding = (int) Math.round(width * 0.05);
    private String[][] board;
    private int SIZE = 8;

    public example() {
        board = newBoard();
        JFrame frame = new JFrame();
        frame.setTitle("Reversi");
        frame.setLocationRelativeTo(null);
        frame.setLocation(450, 150);
        frame.setBackground(Color.BLACK);
        frame.setPreferredSize(new Dimension(width * 8, width * 9));
        frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
        frame.setResizable(false);
        frame.add(new Shell());

        JPanel bottom = new JPanel();

        //Label for a number of player's tiles
        JLabel humanPoints = new JLabel();
        humanPoints.setText(String.valueOf(getNumberOfHumanTiles()));
        humanPoints.setForeground(Color.BLUE);
        humanPoints.setFont(new Font("Verdana", Font.BOLD, 20));
        bottom.add(humanPoints);

        //Just a test button, that creates a random combination of tiles
        //To test, if the labels update correctly.
        JButton testButton = new JButton("Test");
        testButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                board = newBoard();
                for (int i = 0; i < (int)(Math.random() * SIZE); i++) {
                    board[(int)(Math.random() * SIZE)][(int)(Math.random() * SIZE)] = "X";
                }
                for (int i = 0; i < (int)(Math.random() * SIZE); i++) {
                    board[(int)(Math.random() * SIZE)][(int)(Math.random() * SIZE)] = "O";
                }
                panel.repaint();
            }
        });
        bottom.add(testButton);


        //Label for a number of machine's tiles
        JLabel machinePoints = new JLabel();
        machinePoints.setText(String.valueOf(getNumberOfMachineTiles()));
        machinePoints.setForeground(Color.RED);
        machinePoints.setFont(new Font("Verdana", Font.BOLD, 20));
        bottom.add(machinePoints);

        frame.add(bottom, BorderLayout.SOUTH);

        //A panel for a board 8x8.
        panel = new JPanel(new GridLayout(SIZE, SIZE)) {
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);

                //Prints the tiles, according to the value of each tile
                for (int i = 0; i < SIZE; i++) {
                    for (int j = 0; j < SIZE; j++) {
                        int row = i;
                        int col = j;
                        JPanel tile = new JPanel() {
                            @Override
                            protected void paintComponent(Graphics g) {
                                super.paintComponent(g);
                                g.setColor(Color.GREEN);
                                g.fillRect(0, 0, width, width);
                                g.setColor(Color.BLACK);
                                g.drawRect(0, 0, width, width);
                                switch (board[row][col]) {
                                    case "X":
                                        g.setColor(Color.BLUE);
                                        g.fillOval(padding, padding, diameter, diameter);
                                        g.setColor(Color.BLACK);
                                        g.drawOval(padding, padding, diameter, diameter);
                                        break;
                                    case "O":
                                        g.setColor(Color.RED);
                                        g.fillOval(padding, padding, diameter, diameter);
                                        g.setColor(Color.BLACK);
                                        g.drawOval(padding, padding, diameter, diameter);
                                        break;
                                    default:
                                        break;
                                }
                            }
                        };
                        panel.add(tile);
                    }
                }
                //If you write it here, it goes crazy and breaks the whole structure of a board
                //But prints a correct number of tiles
                //humanPoints.setText(String.valueOf(getNumberOfHumanTiles()));
                //machinePoints.setText(String.valueOf(getNumberOfMachineTiles()));
            }
        };
        frame.add(panel, BorderLayout.CENTER);
        frame.pack();
        frame.setVisible(true);
    }

    //Creates a start configuration of a board
    private String[][] newBoard() {
        String[][] newBoard = new String[SIZE][SIZE];
        for (int i = 0; i < SIZE; i++) {
            Arrays.fill(newBoard[i], ".");
        }

        newBoard[SIZE / 2 - 1][SIZE / 2 - 1] = "X";
        newBoard[SIZE / 2][SIZE / 2] = "X";
        newBoard[SIZE / 2 - 1][SIZE / 2] = "O";
        newBoard[SIZE / 2][SIZE / 2 - 1] = "O";
        return newBoard;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new example();
            }
        });
    }

    public int getNumberOfHumanTiles() {
        int count = 0;
        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                if (Objects.equals(board[i][j], "X")) {
                    count++;
                }
            }
        }
        return count;
    }

    public int getNumberOfMachineTiles() {
        int count = 0;
        for (int i = 0; i < SIZE; i++) {
            for (int j = 0; j < SIZE; j++) {
                if (Objects.equals(board[i][j], "O")) {
                    count++;
                }
            }
        }
        return count;
    }

}

Solution

  • Introduction

    Oracle has a helpful tutorial, Creating a GUI With Swing. Skip the Learning Swing with the NetBeans IDE section.

    All Swing applications must start with a call to the SwingUtilities invokeLater method. This method ensures that the Swing components are created and executed on the Event Dispatch Thread.

    The code posted in the question was difficult for me to understand. I restructured the code so it was easier for me to understand.

    Here's a picture of the GUI.

    Reversi

    This isn't the full game. It's an example to show how to structure game code.

    • Left-click on an empty square to place a piece.

    • Left-click on a piece to reverse its color.

    Try it and see. I didn't use JLabels for the pieces. Since the code in the question had JPanel drawing code, I went ahead and drew the pieces.

    Explanation

    The first thing I did was create a model class. A model class is a plain Java getter/settee class. This allows me to put all the game fields in one place and treat the model as a single object.

    I created separate classes for the JFrame and the drawing JPanel. This put the JFrame code in one place and the game board drawing code JPanel in a separate place. Like code grouped together in one place is easier to understand and modify. Small methods and classes are easier to understand and modify.

    I created a BoardListener class extending MouseAdapter (implementing MouseListener) to listen for mouse presses. Left-clicking on an empty square places a piece. Left-clicking on a piece changes its color.

    Code

    Here's the complete runnable code. I made the additional classes inner classes so I could post the code as one block. The additional classes should be additional files in a Java project.

    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.FlowLayout;
    import java.awt.Font;
    import java.awt.Graphics;
    import java.awt.Point;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.MouseAdapter;
    import java.awt.event.MouseEvent;
    import java.util.Random;
    
    import javax.swing.BorderFactory;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.SwingUtilities;
    
    public class ReversiExample implements Runnable {
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new ReversiExample());
        }
    
        private final DrawingPanel drawingPanel;
    
        private JLabel humanPoints, machinePoints;
    
        private final Random random;
    
        private final ReversiExampleModel model;
    
        public ReversiExample() {
            this.random = new Random();
            this.model = new ReversiExampleModel();
            this.drawingPanel = new DrawingPanel(this, model);
        }
    
        @Override
        public void run() {
            JFrame frame = new JFrame("Reversi Example");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setBackground(Color.BLACK);
    
            frame.add(drawingPanel, BorderLayout.CENTER);
            frame.add(createButtonPanel(), BorderLayout.SOUTH);
    
            frame.pack();
            frame.setLocationByPlatform(true);
            frame.setVisible(true);
        }
    
        private JPanel createButtonPanel() {
            JPanel panel = new JPanel(new FlowLayout());
            panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    
            JLabel label = new JLabel("Human Tiles:");
            panel.add(label);
    
            // Label for a number of player's tiles
            humanPoints = new JLabel();
            humanPoints.setText(model.getHumanTilesDisplay());
            humanPoints.setForeground(Color.BLUE);
            humanPoints.setFont(new Font("Verdana", Font.BOLD, 20));
            panel.add(humanPoints);
    
            // Just a test button, that creates a random combination of tiles
            // To test, if the labels update correctly.
            JButton testButton = new JButton("Test");
            testButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent event) {
                    Dimension d = model.getBoardSize();
                    int randomXPieces = random.nextInt(8) + 3;
                    model.setHumanTiles(randomXPieces);
                    for (int i = 0; i < randomXPieces; i++) {
                        int row = random.nextInt(d.height);
                        int col = random.nextInt(d.width);
                        model.setBoardCell(row, col, "X");
                    }
    
                    int randomOPieces = random.nextInt(8) + 3;
                    model.setMachineTiles(randomOPieces);
                    for (int i = 0; i < randomOPieces; i++) {
                        int row = random.nextInt(d.height);
                        int col = random.nextInt(d.width);
                        model.setBoardCell(row, col, "O");
                    }
    
                    updateButtonPanel();
                    drawingPanel.repaint();
                }
            });
            panel.add(testButton);
    
            label = new JLabel("Machine Tiles:");
            panel.add(label);
    
            // Label for a number of machine's tiles
            machinePoints = new JLabel();
            machinePoints.setText(model.getMachineTilesDisplay());
            machinePoints.setForeground(Color.RED);
            machinePoints.setFont(new Font("Verdana", Font.BOLD, 20));
            panel.add(machinePoints);
    
            return panel;
        }
    
        public void updateButtonPanel() {
            humanPoints.setText(model.getHumanTilesDisplay());
            machinePoints.setText(model.getMachineTilesDisplay());
        }
    
        public void repaintDrawingPanel() {
            drawingPanel.repaint();
        }
    
        public class DrawingPanel extends JPanel {
    
            private static final long serialVersionUID = 1L;
    
            private final int border, cellWidth, diameter, padding;
    
            private final ReversiExampleModel model;
    
            public DrawingPanel(ReversiExample view, ReversiExampleModel model) {
                this.model = model;
                this.cellWidth = model.getCellWidth();
                this.diameter = (int) Math.round(cellWidth * 0.85);
                this.padding = (int) Math.round(cellWidth * 0.05);
                this.border = model.getBorder();
                Dimension d = model.getBoardSize();
                int width = d.width * cellWidth + border + border;
                int height = d.height * cellWidth + border + border;
                this.setPreferredSize(new Dimension(width, height));
                this.addMouseListener(new BoardListener(view, model));
            }
    
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                g.setColor(Color.BLACK);
                g.fillRect(0, 0, this.getWidth(), this.getHeight());
    
                // Prints the tiles, according to the value of each tile
                Dimension d = model.getBoardSize();
                for (int row = 0; row < d.height; row++) {
                    int y = row * cellWidth + border;
                    for (int col = 0; col < d.width; col++) {
                        int x = col * cellWidth + border;
                        drawCells(g, y, x);
                        drawPieces(g, row, y, col, x);
                    }
                }
    
            }
    
            private void drawCells(Graphics g, int y, int x) {
                g.setColor(Color.GREEN);
                g.fillRect(x, y, cellWidth, cellWidth);
                g.setColor(Color.BLACK);
                g.drawRect(x, y, cellWidth, cellWidth);
            }
    
            private void drawPieces(Graphics g, int row, int y, int col, int x) {
                switch (model.getBoardCell(row, col)) {
                case "X":
                    g.setColor(Color.BLUE);
                    g.fillOval(x + padding, y + padding, diameter, diameter);
                    g.setColor(Color.BLACK);
                    g.drawOval(x + padding, y + padding, diameter, diameter);
                    break;
                case "O":
                    g.setColor(Color.RED);
                    g.fillOval(x + padding, y + padding, diameter, diameter);
                    g.setColor(Color.BLACK);
                    g.drawOval(x + padding, y + padding, diameter, diameter);
                    break;
                default:
                    break;
                }
            }
    
        }
    
        public class BoardListener extends MouseAdapter {
    
            private final ReversiExample view;
    
            private final ReversiExampleModel model;
    
            public BoardListener(ReversiExample view, ReversiExampleModel model) {
                this.view = view;
                this.model = model;
            }
    
            @Override
            public void mousePressed(MouseEvent event) {
                Point p = event.getPoint();
                int border = model.getBorder();
                int cellWidth = model.getCellWidth();
                int row = (p.y - border) / cellWidth;
                int col = (p.x - border) / cellWidth;
                String value = model.getBoardCell(row, col);
                if (value.equals(" ")) {
                    model.addPiece(row, col);
                } else {
                    model.reverseColor(row, col);
                }
    
                model.nextTurn();
                view.updateButtonPanel();
                view.repaintDrawingPanel();
            }
    
        }
    
        public class ReversiExampleModel {
    
            private boolean playersTurn;
    
            private int humanTiles, machineTiles;
            private final int border, cellWidth;
    
            private final Dimension boardSize;
    
            private String[][] board;
    
            public ReversiExampleModel() {
                this.boardSize = new Dimension(8, 8);
                this.board = new String[boardSize.height][boardSize.width];
                this.humanTiles = 0;
                this.machineTiles = 0;
                this.border = 10;
                this.cellWidth = 60;
                this.playersTurn = true;
    
                for (int row = 0; row < boardSize.height; row++) {
                    for (int col = 0; col < boardSize.width; col++) {
                        board[row][col] = " ";
                    }
                }
            }
    
            public Dimension getBoardSize() {
                return boardSize;
            }
    
            public void setBoardCell(int row, int col, String value) {
                this.board[row][col] = value;
            }
    
            public String getBoardCell(int row, int col) {
                return board[row][col];
            }
    
            public void addPiece(int row, int col) {
                if (playersTurn) {
                    board[row][col] = "X";
                    humanTiles++;
                } else {
                    board[row][col] = "O";
                    machineTiles++;
                }
            }
    
            public void reverseColor(int row, int col) {
                if (board[row][col].equals("X")) {
                    board[row][col] = "O";
                    humanTiles--;
                    machineTiles++;
                } else if (board[row][col].equals("O")) {
                    board[row][col] = "X";
                    humanTiles++;
                    machineTiles--;
                }
            }
    
            public void setHumanTiles(int humanTiles) {
                this.humanTiles = humanTiles;
            }
    
            public void setMachineTiles(int machineTiles) {
                this.machineTiles = machineTiles;
            }
    
            public void incrementHumanTiles(int increment) {
                this.humanTiles += increment;
            }
    
            public void incrementMachineTiles(int increment) {
                this.machineTiles += increment;
            }
    
            public String getHumanTilesDisplay() {
                return Integer.toString(humanTiles);
            }
    
            public String getMachineTilesDisplay() {
                return Integer.toString(machineTiles);
            }
    
            public int getBorder() {
                return border;
            }
    
            public int getCellWidth() {
                return cellWidth;
            }
    
            public boolean isPlayersTurn() {
                return playersTurn;
            }
    
            public void nextTurn() {
                this.playersTurn = !this.playersTurn;
            }
    
        }
    
    }