javaswinguser-interfacemazecustom-painting

Swing - UI changes are not affected


I'm sketching the grid on the user interface using the width and height that I specified when I created the GridPanel, accordingly. For representational purposes, I am laying the grid starting at 30, 30, and ending at x - 60, y - 60. Depending on the grid dimensions, dynamic shrink and grow are implemented using scaleX and scaleY.

The image below displays the internals of the cell object.

Cell object

GenerateMaze is a recursive backtracking algorithm used to generate perfect mazes. (Surely not complete, but I still should be able to see the result.)

public class GridPanel extends JPanel {
    private class Cell {
        private int y, x;
        private int[] south;
        private int[] north;
        private int[] west;
        private int[] east;

        private Cell(int y, int x) {
            this.y = y;
            this.x = x;

            this.south = new int[4];
            this.north = new int[4];
            this.west = new int[4];
            this.east = new int[4];
        }
    }

    private int w, h;
    private final int scaleX, scaleY;
    private final int Vx, Vy;
    private final Cell[][] grid;
    private boolean[][] discovered;

    public GridPanel(int w, int h) {
        setLayout(new GridLayout(1, 1));

        this.w = w;
        this.h = h;

        scaleX = (int) (w / Math.sqrt(w));
        scaleY = (int) (h / Math.sqrt(h));

        Vx = w / scaleX;
        Vy = w / scaleY;

        discovered = new boolean[Vy][Vx];

        grid = new Cell[Vy][Vx];
        init(grid);
    }

    private void init(Cell[][] grid) {
        for (int j = 0; j < grid.length; j++) {
            for (int i = 0; i < grid[0].length; i++) {
                grid[j][i] = new Cell(j, i);
            }
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        setDoubleBuffered(true);
        g.setColor(Color.CYAN);

        for (int y = 30, j = 0; y < h - 60; y += scaleY, j++) {
            for (int x = 30, i = 0; x < w - 60; x += scaleX, i++) {
                Cell cell = grid[j][i];

                cell.east = new int[]{x, y, x, y + scaleY};
                cell.west = new int[]{x + scaleX, y, x + scaleX, y + scaleY};
                cell.north = new int[]{x, y, x + scaleX, y};
                cell.south = new int[]{x, y + scaleY, x + scaleX, y + scaleY};

                g.drawLine(cell.east[0], cell.east[1], cell.east[2], cell.east[3]);
                g.drawLine(cell.west[0], cell.west[1], cell.west[2], cell.west[3]);
                g.drawLine(cell.south[0], cell.south[1], cell.south[2], cell.south[3]);
                g.drawLine(cell.north[0], cell.north[1], cell.north[2], cell.north[3]);
            }
        }

        generateMaze(g, 0, 0);
    }

    private void generateMaze(Graphics g, int y, int x) {
        discovered[y][x] = true;

        while (true) {
            Cell current = grid[y][x];

            if (validUp(y) && !discovered[y - 1][x]) {
                removeWall(g, current.north);
                generateMaze(g, y - 1, x);
            }
            if (validDown(y) && !discovered[y + 1][x]) {
                removeWall(g, current.south);
                generateMaze(g, y + 1, x);
            }
            if (validRight(x) && !discovered[y][x + 1]) {
                removeWall(g, current.east);
                generateMaze(g, y, x + 1);
            }
            if (validLeft(x) && !discovered[y][x - 1]) {
                removeWall(g, current.west);
                generateMaze(g, y, x - 1);
            }

            // All neighbors have been visited, break the loop
            break;
        }
    }


    private boolean validLeft(int x) {
        if (x - 1 < 0) return false;
        return true;
    }

    private boolean validRight(int x) {
        if (x + 1 < grid[0].length) return true;
        return false;
    }

    private void removeWall(Graphics g, int[] coordinate) {
        SwingUtilities.invokeLater(() -> {
            g.setColor(Color.BLACK);
            g.drawLine(coordinate[0], coordinate[1], coordinate[2], coordinate[3]);
        });
    }

    private boolean validDown(int y) {
        if (y + 1 < grid.length) return true;
        return false;
    }

    private boolean validUp(int y) {
        if (y - 1 < 0) return false;
        return true;
    }
}

° Application Frame:

public class Maze extends JFrame {
    private int w, h;

    public Maze(int w, int h) throws HeadlessException {
        super("Perfect Maze");
        setResizable(false);
        setBackground(Color.BLACK);

        this.w = w;
        this.h = h;

        add(new GridPanel(w, h));

        setSize(w, h);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setVisible(true);
    }
}

° Runner:

public class Main {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            new Maze(800, 800);
        });
    }
}

Given that the user interface appears and the application closes successfully, I believe the removeWall function is the cause of the issue.

It did not work when I attempted to add method: repaint() at the end of the method.


Solution

  • Introduction

    I redid your drawing code and came up with the following GUI. This is your maze grid with no walls removed.

    Starting GUI

    If you expand the GUI by clicking the box in the upper right, the grid resizes. It's hard to see because Stack Overflow scales the image.

    Expanded GUI

    Explanation

    Oracle has a helpful tutorial, Creating a GUI With Swing. Skip the Learning Swing with the NetBeans IDE section. Pay particular attention to the Performing Custom Painting section.

    When I create a Swing GUI, I use the model-view-controller (MVC) pattern. This pattern allows me to separate my concerns and focus on one small part of the Java application at a time.

    A Swing model is made up of one or more plain Java getter/setter classes.

    A Swing view is made up of one JFrame and as many JPanels or JDialogs as you need.

    Each Action or Listener is a Swing controller. There's usually not one controller to "rule them all".

    Model

    I rewrote your Cell class. You don't need the coordinate because the cell position in the grid provides the coordinate. The coordinate is the x, y position of the cell in the grid, starting with 0,0 and going up to 39,39.

    public class Cell {
        
        private boolean north, south, east, west;
        
        public Cell() {
            this.north = true;
            this.south = true;
            this.east = true;
            this.west = true;
        }
    
        public boolean isNorth() {
            return north;
        }
    
        public void setNorth(boolean north) {
            this.north = north;
        }
    
        public boolean isSouth() {
            return south;
        }
    
        public void setSouth(boolean south) {
            this.south = south;
        }
    
        public boolean isEast() {
            return east;
        }
    
        public void setEast(boolean east) {
            this.east = east;
        }
    
        public boolean isWest() {
            return west;
        }
    
        public void setWest(boolean west) {
            this.west = west;
        }
        
    }
    

    Your code to create the maze will remove the walls of a cell by setting the appropriate boolean to false; The Swing view code will paint the status of the grid. Period. Nothing else.

    Here's the PerfectMazeModel class.

    public class PerfectMazeModel {
        
        public final Cell[][] grid;
        
        public PerfectMazeModel() {
            this.grid = new Cell[40][40];
            for (int y = 0; y < grid.length; y++) {
                for (int x = 0; x < grid[y].length; x++) {
                    grid[y][x] = new Cell();
                }
            }
        }
    
        public Cell[][] getGrid() {
            return grid;
        }
        
    }
    

    Here we create the grid. You don't have to make the grid square.

    View

    I use a JFrame and create a drawing JPanel. The drawing code is a bit complicated.

    public class DrawingPanel extends JPanel {
    
        private static final long serialVersionUID = 1L;
        
        private final PerfectMazeModel model;
        
        public DrawingPanel(PerfectMazeModel model) {
            this.model = model;
            this.setPreferredSize(new Dimension(800, 800));
        }
        
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            
            Cell[][] grid = model.getGrid();
            int margin = 20;
            int width = this.getWidth() - 2 * margin;
            int height = this.getHeight() - 2 * margin;
            int cellWidth = width / grid[0].length;
            int cellHeight = height / grid.length;
            int cellDimension = Math.min(cellWidth, cellHeight);
            
            int dx = margin;
            int dy = margin;
            for (int y = 0; y < grid.length; y++) {
                for (int x = 0; x < grid[y].length; x++) {
                    Cell cell = grid[y][x];
                    if (cell.isNorth()) {
                        g.drawLine(dx, dy, dx + cellDimension, dy);
                    }
                    if (cell.isWest()) {
                        g.drawLine(dx, dy, dx, dy + cellDimension);
                    }
                    if (cell.isSouth()) {
                        int cx = dx + cellDimension;
                        int cy = dy + cellDimension;
                        g.drawLine(dx, cy, cx, cy);
                    }
                    if (cell.isEast()) {
                        int cx = dx + cellDimension;
                        int cy = dy + cellDimension;
                        g.drawLine(cx, dy, cx, cy);
                    }
                    dx += cellDimension;
                }
                dx = margin;
                dy += cellDimension;
            }
        }
    }
    

    We get the cell dimension based on the current width and height of the drawing JPanel.

    x and y are the logical coordinates of the grid cell. dx and dy are the upper left corner of the grid cell. cx and cy are the upper right and lower left points of the grid cell. We draw the line based on the boolean flag.

    The cell/grid model isn't perfect because the east line of one cell is the same line as the west line of the adjacent cell. The same holds true for the north and south lines of a cell.

    Controller

    Since I'm just showing the model and drawing code, there are no controllers. I've left the addition of the code to remove lines for you.

    Code

    Here's the complete runnable code. I made the additional classes inner classes so I could post this code as one block.

    import java.awt.BorderLayout;
    import java.awt.Dimension;
    import java.awt.Graphics;
    
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.SwingUtilities;
    
    public class PerfectMaze implements Runnable {
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new PerfectMaze());
        }
        
        private final DrawingPanel drawingPanel;
        
        private final PerfectMazeModel model;
        
        public PerfectMaze() {
            this.model = new PerfectMazeModel();
            this.drawingPanel = new DrawingPanel(model);
        }
    
        @Override
        public void run() {
            JFrame frame = new JFrame("Perfect Maze");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            
            frame.add(drawingPanel, BorderLayout.CENTER);
            
            frame.pack();
            frame.setLocationByPlatform(true);
            frame.setVisible(true);
        }
        
        public void drawingPanelRepaint() {
            this.drawingPanel.repaint();
        }
        
        public class DrawingPanel extends JPanel {
    
            private static final long serialVersionUID = 1L;
            
            private final PerfectMazeModel model;
            
            public DrawingPanel(PerfectMazeModel model) {
                this.model = model;
                this.setPreferredSize(new Dimension(800, 800));
            }
            
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                
                Cell[][] grid = model.getGrid();
                int margin = 20;
                int width = this.getWidth() - 2 * margin;
                int height = this.getHeight() - 2 * margin;
                int cellWidth = width / grid[0].length;
                int cellHeight = height / grid.length;
                int cellDimension = Math.min(cellWidth, cellHeight);
                
                int dx = margin;
                int dy = margin;
                for (int y = 0; y < grid.length; y++) {
                    for (int x = 0; x < grid[y].length; x++) {
                        Cell cell = grid[y][x];
                        if (cell.isNorth()) {
                            g.drawLine(dx, dy, dx + cellDimension, dy);
                        }
                        if (cell.isWest()) {
                            g.drawLine(dx, dy, dx, dy + cellDimension);
                        }
                        if (cell.isSouth()) {
                            int cx = dx + cellDimension;
                            int cy = dy + cellDimension;
                            g.drawLine(dx, cy, cx, cy);
                        }
                        if (cell.isEast()) {
                            int cx = dx + cellDimension;
                            int cy = dy + cellDimension;
                            g.drawLine(cx, dy, cx, cy);
                        }
                        dx += cellDimension;
                    }
                    dx = margin;
                    dy += cellDimension;
                }
            }
        }
        
        public class PerfectMazeModel {
            
            public final Cell[][] grid;
            
            public PerfectMazeModel() {
                this.grid = new Cell[40][40];
                for (int y = 0; y < grid.length; y++) {
                    for (int x = 0; x < grid[y].length; x++) {
                        grid[y][x] = new Cell();
                    }
                }
            }
    
            public Cell[][] getGrid() {
                return grid;
            }
            
        }
        
        public class Cell {
            
            private boolean north, south, east, west;
            
            public Cell() {
                this.north = true;
                this.south = true;
                this.east = true;
                this.west = true;
            }
    
            public boolean isNorth() {
                return north;
            }
    
            public void setNorth(boolean north) {
                this.north = north;
            }
    
            public boolean isSouth() {
                return south;
            }
    
            public void setSouth(boolean south) {
                this.south = south;
            }
    
            public boolean isEast() {
                return east;
            }
    
            public void setEast(boolean east) {
                this.east = east;
            }
    
            public boolean isWest() {
                return west;
            }
    
            public void setWest(boolean west) {
                this.west = west;
            }
            
        }
    
    }