user8578415
user8578415

Reputation: 421

JPanel doesn't update in real time when using delays

I am creating a GUI program to solve a maze. I already got the maze solving algorithm down, but weird stuff happens when I try to add a delay.

The code is pretty long, so here's something that does the same in terms of GUI delays.

import java.awt.*;
import javax.swing.*;

public class Example extends JPanel {
    JButton[] buttons = new JButton[4];
    public Example() {
    setLayout(new GridLayout(1, 4));
    for(int i = 0; i < 4; i++)
        add(buttons[i] = new JButton());
    }

    public static class SubClass extends JPanel {
        Example example;
        JButton start;

        public SubClass() {
            setLayout(new BorderLayout());
            add(example = new Example(), BorderLayout.CENTER);
            start = new JButton("Start");
            start.addActionListener(e -> {
                for(int i = 0; i < 4; i++) {
                    //insert delays here
                    example.buttons[i].setBackground(Color.BLACK);
                }
            });
            add(start, BorderLayout.SOUTH);
        }
    }

    public static void main(String[] args){
        JFrame frame = new JFrame("Title");
        frame.setSize(500, 500);
        frame.setLocation(500, 250);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setContentPane(new SubClass());
        frame.setVisible(true);
    }
}

When I run the code without any delays, it works perfectly. However, whenever I try to add a delay, rather than the buttons updating one at a time, the whole window just freezes until all of the buttons are done updating, and then the entire thing updates at once.

I tried using java.util.Timer, javax.swing.Timer, and Thread.sleep, but none of them seemed to work.

Also, when I don't have the enclosing panel (SubClass) and just change the inner panel (Example) directly from the main method, it does work, and the buttons update in real time like they're supposed to.

For example, if I changed the main method to

    public static void main(String[] args){
        Example ex = new Example();
        JFrame frame = new JFrame("Title");
        frame.setSize(500, 500);
        frame.setLocation(500, 250);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setContentPane(ex);
        frame.setVisible(true);

        for(int i = 0; i < 4; i++) {
            //insert delays here
            ex.buttons[i].setBackground(Color.BLACK);
        }
    }

Looking back on it, I should have made the panels in reverse order, switching the subclass and the enclosing class, but at this point I'm done writing my program, and it works in all aspects except this one, so I don't want to do too much extra work and rewrite a bunch of stuff. I also don't know if that would solve the problem anyway.

Edit: Here is the full code:

import javax.swing.*;
import javax.swing.Timer;
import java.awt.*;
import java.util.*;
import javax.swing.border.LineBorder;

public class StackGUIMaze extends JPanel {
    public static final String SET_START = "setStart";
    public static final String SET_END = "setEnd";
    public static final String BUILD = "build";
    public static final String RUNNING = "running";
    private static final char OPEN = 'O'; //Open
    private static final char CLOSED = 'X'; //Closed
    private static final char TRIED = '*';
    private static final char SUCCEEDED = '+';
    private static final char FAILED = '#';
    private static final char POINTER = '^';
    private static final char END = 'E';
    private static final char START = 'S';
    private static final int DELAY = 0;
    private static final Direction NORTH = Direction.NORTH;
    private static final Direction SOUTH = Direction.SOUTH;
    private static final Direction EAST = Direction.EAST;
    private static final Direction WEST = Direction.WEST;
    private static final Direction[] DIRECTIONS = {SOUTH, EAST, NORTH, WEST};
    private final int ROWS;
    private final int COLUMNS;
    private Space pointer;
    private Space end;
    private Branch moves = new Branch(null, null);
    private Space[][] buttons;
    private String mode = "";
    private Space start;
    private Timer timer = new Timer(DELAY, e -> {
        step();
        if(pointer == end) {
            this.timer.stop();
        }
    });

    public StackGUIMaze(int r, int c) {
        ROWS = r;
        COLUMNS = c;
        setLayout(new GridLayout(ROWS, COLUMNS));
        buttons = new Space[ROWS][COLUMNS];
        for(int i = 0; i < ROWS; i++) {
            buttons[i] = new Space[COLUMNS];
            for(int j = 0; j < COLUMNS; j++) {
                buttons[i][j] = new Space(OPEN, i, j);
                buttons[i][j].setBackground(Color.WHITE);
                buttons[i][j].setContentAreaFilled(false);
                buttons[i][j].setBorder(new LineBorder(new Color(100, 100, 100)));            
                buttons[i][j].setOpaque(true);
                buttons[i][j].addActionListener(e -> {
                    if(e.getSource() instanceof Space) {
                        switch(mode) {
                            case BUILD:
                                ((Space) e.getSource()).setStatus(((Space) e.getSource()).getStatus() == OPEN ? CLOSED : OPEN); //Toggles state of space
                                break;
                            case SET_START:
                                ((Space) e.getSource()).setStatus(START);
                                break;
                            case SET_END:
                                ((Space) e.getSource()).setStatus(END);
                                break;
                        }
                    }
                });
                add(buttons[i][j]);
            }
        }
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Title");
                frame.setSize(500, 500);
                frame.setLocation(500, 250);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setContentPane(new FullPanel(12, 12));
                frame.setVisible(true);
        });
    }

    private static void delay(int n) {
        try {Thread.sleep(n);} catch(InterruptedException ignored) {}
    }

    private void setEnd(int r, int c) {
        end = null;
        buttons[r][c].setStatus(END);
    }

    public Space getPointer() {
        return pointer;
    }

    public Space getEnd() {
        return end;
    }

    private void backtrack(){
        while(openMoves().isEmpty()) {
            pointer.setStatus(FAILED);
            move(lastMove().getDirection().opposite());
            lastMove().getParent().pop();
        }
    }

    private Space to(Direction d) {
        try {
            switch (d) { //No breaks necessary, since each case ends in a return statement
                case NORTH:
                    return buttons[pointer.getRow() - 1][pointer.getCol()];
                case SOUTH:
                    return buttons[pointer.getRow() + 1][pointer.getCol()];
                case EAST:
                    return buttons[pointer.getRow()][pointer.getCol() + 1];
                case WEST:
                    return buttons[pointer.getRow()][pointer.getCol() - 1];
                default:
                    System.out.println("I don't know how you got here, somehow a nonexistent direction was called");
                    return null;
            }
        } catch(ArrayIndexOutOfBoundsException e) {
            return null;
        }
    }

    public void setPointer(int r, int c) {
        pointer = buttons[r][c];
    }

    private Branch lastMove() {
        Branch temp = moves;
        while(!temp.isEmpty()) {
            temp = temp.peek();
        }
        return temp;
    }

    private boolean go(Direction d) {
        boolean b = checkOpen(d);
        pointer.setStatus(TRIED);
        if (checkOpen(d)) {
            move(d);
            if(pointer.getStatus() == OPEN) pointer.setStatus(TRIED);
            lastMove().push(new Branch(lastMove(), d));
            return true;
        } else {
            return false;
        }
    }

    private void move(Direction d) {
        delay(50);
        switch(d) { //No breaks necessary, since each case ends in a return statement
            case NORTH:
                buttons[pointer.getRow() - 1][pointer.getCol()].setStatus(POINTER);
                break;
            case SOUTH:
                buttons[pointer.getRow() + 1][pointer.getCol()].setStatus(POINTER);
                break;
            case EAST:
                buttons[pointer.getRow()][pointer.getCol() + 1].setStatus(POINTER);
                break;
            case WEST:
                buttons[pointer.getRow()][pointer.getCol() - 1].setStatus(POINTER);
                break;
            default:
                System.out.println("I don't know how you got here, somehow a nonexistent direction was called");
        }
    }

    private ArrayList<Direction> openMoves() {
        ArrayList<Direction> arr = new ArrayList<>();
        for(Direction d : DIRECTIONS) {
            if(checkOpen(d)) arr.add(d);
        }
        return arr;
    }

    private boolean checkOpen(Direction d) {
        try {
            char s;
            switch (d) { //No breaks necessary, since each case ends in a return statement
                case NORTH:
                    s = buttons[pointer.getRow() - 1][pointer.getCol()].getStatus();
                    break;
                case SOUTH:
                    s = buttons[pointer.getRow() + 1][pointer.getCol()].getStatus();
                    break;
                case EAST:
                    s = buttons[pointer.getRow()][pointer.getCol() + 1].getStatus();
                    break;
                case WEST:
                    s = buttons[pointer.getRow()][pointer.getCol() - 1].getStatus();
                    break;
                default:
                    System.out.println("I don't know how you got here, somehow a nonexistent direction was called");
                    return false;
            }
            return s == OPEN || s == END || s == START;
        } catch(ArrayIndexOutOfBoundsException e) {
            return false;
        }
    }

    private boolean isSolved() {
        return end == pointer;
    }

    public void step() {
        while(pointer != end) {
            if(openMoves().isEmpty() && pointer != start) backtrack();
            while(!openMoves().isEmpty()) {
                for(Direction d : DIRECTIONS) {
                    if(pointer == end) return;
                    go(d);
                }
            }
        }
    }

    private enum Direction {
        NORTH, SOUTH, EAST, WEST;
        Direction opposite() {
            switch(this) {
                case NORTH:
                    return SOUTH;
                case SOUTH:
                    return NORTH;
                case EAST:
                    return WEST;
                case WEST:
                    return EAST;
                default:
                    System.out.println("How did you even get here? There are only 4 directions.");
                    return null;
            }
        }
    }

    public static class FullPanel extends JPanel {
        StackGUIMaze maze;
        JButton start;
        JPanel subPanel;

        public FullPanel(int r, int c) {
            maze = new StackGUIMaze(r, c);
            setLayout(new BorderLayout());
            add(maze, BorderLayout.CENTER);

            JButton start = new JButton();
            start.setText("Start");
            start.addActionListener(e -> {
                if(maze.getPointer() == null || maze.getEnd() == null) {
                    System.out.println("Could not solve, as the start/end is missing.");
                } else {
                    for (Space[] sp : maze.buttons) {
                        for (Space s : sp) {
                            s.setEnabled(false);
                        }
                    }
                    maze.timer.start();
                }
            });

            JButton setEnd = new JButton("Set end");
            setEnd.addActionListener(e -> maze.mode = SET_END);

            JButton setStart = new JButton("Set start");
            setStart.addActionListener(e -> maze.mode = SET_START);

            JButton buildButton = new JButton("Build maze");
            buildButton.addActionListener(e -> maze.mode = BUILD);

            subPanel = new JPanel();
            subPanel.add(setStart);
            subPanel.add(setEnd);
            subPanel.add(buildButton);
            subPanel.add(start);
            add(subPanel, BorderLayout.SOUTH);
        }
    }

    private class Branch extends Stack<Branch> {
        Branch parent;
        private Direction direction;

        public Branch(Branch b, Direction d) {
            parent = b;
            direction = d;
        }

        public Direction getDirection() {
            return direction;
        }

        public Branch getParent() {
            return parent;
        }
    }

    private class Space extends JButton {
        int row;
        int col;
        private char status;

        public Space(char status, int row, int col) {
            this.status = status;
            this.row = row;
            this.col = col;
        }

        public char getStatus() {
            return status;
        }

        public void setStatus(char s) {
            char st = status;
            this.status = s;
            switch(s) {
                case OPEN:
                    setBackground(Color.WHITE);
                    break;
                case CLOSED:
                    setBackground(Color.BLACK);
                    break;
                case TRIED:
                    if(st == FAILED) status = FAILED;
                    else setBackground(Color.LIGHT_GRAY);
                    break;
                case FAILED:
                    setBackground(Color.RED);
                    break;
                case SUCCEEDED:
                    setBackground(Color.GREEN);
                    break;
                case POINTER:
                    if(pointer != null && !mode.equals(RUNNING)) pointer.setStatus(TRIED);
                    else if(pointer != null) pointer.setStatus(OPEN);
                    setBackground(Color.GRAY);
                    pointer = this;
                    break;
                case END:
                    if(end != null) end.setStatus(OPEN);
                    setBackground(Color.CYAN);
                    end = this;
                    break;
                case START:
                    if(start != null) pointer.setStatus(START);
                    setBackground(Color.MAGENTA);
                    start = this;
                    pointer = start;
                    break;
                default:
                    System.out.println("Invalid status passed to method setStatus()");
            }
        }

        public int getRow() {
            return row;
        }

        public void setRow(int row) {
            this.row = row;
        }

        public int getCol() {
            return col;
        }

        public void setCol(int col) {
            this.col = col;
        }
    }
}

I realize that it probably isn't optimal, but I just want to get it to work before I start trying to refine it.

Upvotes: 3

Views: 538

Answers (1)

MadProgrammer
MadProgrammer

Reputation: 347314

So, you have two basic problems:

  1. You're blocking the Event Dispatching Thread inside the ActionListener. The UI won't be updated until after the actionPerformed method returns
  2. Setting the background of JButton is a little more complicated the other components. To start with, I disabled the contentAreaFilled and borderPainted properties and made the button opaque which allowed them to become filled.

For example..

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Example extends JPanel {

    JButton[] buttons = new JButton[4];

    public Example() {
        setLayout(new GridLayout(1, 4));
        for (int i = 0; i < 4; i++) {
            add(buttons[i] = new JButton());
            buttons[i].setContentAreaFilled(false);
            buttons[i].setBorderPainted(false);
            buttons[i].setOpaque(true);
        }
    }

    public static class SubClass extends JPanel {

        Example example;
        JButton start;

        private int counter = 0;

        public SubClass() {
            setLayout(new BorderLayout());
            add(example = new Example(), BorderLayout.CENTER);
            start = new JButton("Start");
            start.addActionListener(e -> {
                counter = 0;
                Timer timer = new Timer(200, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        if (counter >= 4) {
                            ((Timer) e.getSource()).stop();
                            return;
                        }
                        example.buttons[counter].setBackground(Color.BLACK);
                        counter++;
                    }
                });
                timer.start();
            });
            add(start, BorderLayout.SOUTH);
        }
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Title");
                frame.setSize(500, 500);
                frame.setLocation(500, 250);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setContentPane(new SubClass());
                frame.setVisible(true);
            }
        });
    }
}

Upvotes: 2

Related Questions