Search code examples
javaswingjframejpanelevent-dispatch-thread

JFrame doesnt update its paint until all code has run


So I have a very strange issue, I am just learning JFrames/Panels, from my understanding, if I add a new component to the frame, I must call revalidate() to get the frame to use said component.

In the implementation of A* shown below the algorithms progress is shown using repaint() throughout the while() loop.

This would shows the progress of the algorithm as it runs, and worked fine up until I decided to try and add a menu to the gui.

So now I need to be able to add the Display() component (which is a JPanel) to the frame and have it function just like it did before, showing the algorithm as it runs. But currently there is a pause of about a second, and only the final state of the algorithm is painted i.e. as if it instantly painted only the very final repaint() call in the while() loop.

Any help here is appreciated.

import java.awt.event.*;
import java.io.FileNotFoundException;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;
import javax.swing.*;

public class aStar {
    /** Width of the GUI */
    private static final int WIDTH = 1280;
    /** Height of the GUI */
    private static final int HEIGHT = 720;

    public static JFrame frame = new JFrame("A* Search Algorithm");

    public static Set<Node> closedSet;
    public static Queue<Node> openSet;

    public static String fileName;

    public static void findPath() {
        try {
            // Initialise the open and closed sets
            closedSet = new HashSet<>();
            openSet = new PriorityQueue<>((a, b) -> Double.compare(a.f, b.f));

            // Process the map
            Map map = new Map(fileName);
            openSet.add(map.start);
            frame.add(new Display(map));
            frame.revalidate();

            /**
             * The A* Algorithm
             */
            while (true) {
                Node current = openSet.poll();

                if (current == map.end) {
                    // We have reached the goal -- render the path and exit
                    renderPath(current, frame);
                    System.out.println("Done!");
                    return;
                }

                // Check through every neighbor of the current node
                for (Node n : map.neighborsOf(current)) {
                    // if its closed or a wall, ignore it
                    if (closedSet.contains(n)) {
                        continue;
                    }

                    // Set the node's h value
                    n.h = heuristic(n, map.end);

                    // Calculate the possible total cost of moving to this node from start
                    double tentativeG = calculateCost(current, n);

                    // Check whether the cost we've calculated is better than the node's current
                    // cost. If so, the path we're currently on is better so we update its g
                    // and add it to openSet
                    if (tentativeG < n.g) {
                        n.setG(tentativeG);
                        n.previous = current;

                        // We need to remove and add the node here in case it already exists
                        // within the PriorityQueue, so that we can force a re-sort.
                        openSet.remove(n);
                        openSet.add(n);
                    }
                }

                // Move current to closedSet
                openSet.remove(current);
                closedSet.add(current);

                // Color the open and closed sets accordingly
                for (Node n : openSet) {
                    n.color = Color.GREEN;
                }
                for (Node n : closedSet) {
                    n.color = Color.RED;
                }

                if (openSet.isEmpty()) {
                    // If openSet is empty, then we failed to find a path to the end
                    // In this case, we render the path to the node with the lowest `h`
                    // value, which is the node closest to the target.

                    Node minHNode = null;
                    for (int x = 0; x < map.width; x++) {
                        for (int y = 0; y < map.height; y++) {
                            Node candidate = map.get(x, y);
                            if (candidate.previous == null)
                                continue;

                            if (minHNode == null) {
                                minHNode = candidate;
                            } else if (candidate.h < minHNode.h) {
                                minHNode = candidate;
                            }
                        }
                    }

                    // Walk through the path we decided on and render it to the user
                    renderPath(minHNode, frame);
                    System.out.println("Failed to reach target. Rendered closest path instead.");
                    return;
                } else {
                    Thread.sleep(10);
                    frame.repaint();
                }
            }
        } catch (FileNotFoundException e) {
            System.err.println("error: Could not find the file \"" + fileName + "\"");
        } catch (InterruptedException e) {
            System.err.println("Error occurred while calling Thread.sleep()");
        } catch (MapException e) {
            System.out.println("error: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        // Build our GUI
        frame.setPreferredSize(new Dimension(WIDTH, HEIGHT));
        frame.setMinimumSize(new Dimension(WIDTH, HEIGHT));
        frame.setMaximumSize(new Dimension(WIDTH, HEIGHT));
        frame.setResizable(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        // Add the menubar and items
        JMenuBar menubar = new JMenuBar();
        frame.setJMenuBar(menubar);
        JMenu file = new JMenu("File");
        menubar.add(file);
        JMenuItem selectMap1 = new JMenuItem("Map 1");
        file.add(selectMap1);

        class selectMapName implements ActionListener {
            public void actionPerformed(ActionEvent e) {
                JMenuItem menuItem = (JMenuItem) e.getSource();
                JPopupMenu menu = (JPopupMenu) menuItem.getParent();
                int index = menu.getComponentZOrder(menuItem);
                onClick(index);
            }

            public void onClick(int index) {
                switch (index) {
                    case 0:
                        fileName = "map1.txt";
                        break;
                    case 1:
                        fileName = "map2.txt";
                        break;
                    case 2:
                        fileName = "map3.txt";
                        break;
                    case 3:
                        fileName = "map4.txt";
                        break;
                    case 4:
                        fileName = "map5.txt";
                        break;
                }
                findPath();
            }
        }
        // Add all the action listeners to the menu items
        selectMap1.addActionListener(new selectMapName());

        // Show the frame
        frame.setVisible(true);
    }

    private static void renderPath(Node startFrom, JFrame frame) {
        // Walk through the path we decided on and render it to the user
        Node temp = startFrom;
        while (temp.previous != null) {
            temp.color = Color.BLUE;
            temp = temp.previous;
        }

        // Repaint with the newly marked path
        frame.repaint();
    }

    /**
     * The heuristic used to determine the validity of a potential path. Currently
     * just returns the euclidian distance. May be better to use taxicab distance if
     * we are not moving diagonals
     * 
     * @param current The current Node
     * @param end     The end Node
     * @return {@code double} The h value for the current Node
     */
    private static double heuristic(Node current, Node end) {
        return Math.hypot(end.x - current.x, end.y - current.y);
    }

    private static double calculateCost(Node current, Node next) {
        double currentG = current.g == Double.MAX_VALUE ? 0 : current.g;
        return currentG + heuristic(current, next);
    }
}```

Solution

  • Swing, like most user interface toolkits, is single threaded. This means there is a single “event queue” for processing all changes to visual components and for processing all user input.

    Repainting is one of those events. If the processing of the event queue is held up by a long-running event listener, the subsequent events will not be processed.

    Your ActionListener is called from the thread that processes the event queue. So, if your actionPerformed method takes a long time, no other events will be processed, including paint events, until the actionPerformed method returns.

    Thread.sleep (and methods like it) must never be called, directly or indirectly, from an ActionListener or any other event listener. sleep calls should always happen on a different thread.

    There are some easy ways to periodically execute code in the event dispatch thread, but in your case, that won’t be enough.

    The problem is that the painting relies on your Map object and your Node objects (I think). This means it is not safe to update the Map or the Nodes, or any of their descendant objects or data, outside of the event dispatch thread. Altering the Map’s state while a painting method is concurrently reading it is going to result in strange visual behavior.

    This issue can be addressed by using a class which represents only your painting actions, and which retains its own copy of that information, so it is not depending on any other objects.

    For instance, if your Display class is drawing lines, you could:

    • call findPath in a new Thread
    • have Display contain java.awt.geom.Line2D objects, rather than a reference to the Map
    • have findPath add Line2D objects to the Display, in the event dispatch thread, as findPath progresses

    This might look something like this:

    public void onClick(int index) {
        switch (index) {
            // ...
        }
        new Thread(() -> findPath()).start();
    }
    

    And somewhere above that:

    Thread.sleep(10);
    EventQueue.invokeLater(() -> {
        display.addLine(new Line2D(start.x, start.y, end.x, end.y));
        display.repaint();
    });
    

    Without seeing how your Display class decides what to paint, I’m not able to provide an example of how to create Line2D objects. But I expect you might have some code like this in the Display class:

    private final Collection<Line2D> lines = new ArrayList<>();
    
    public void addLine(Line2D line) {
        lines.add(line);
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
    
        Graphics2D g2 = (Graphics2D) g;
        for (Line2D line : lines) {
            g.draw(line);
        }
    }