JFrame 在所有代码 运行 之前不会更新其绘制

JFrame doesnt update its paint until all code has run

所以我有一个很奇怪的问题,我只是在学习JFrames/Panels,根据我的理解,如果我向框架添加一个新组件,我必须调用 revalidate() 来让框架使用说组件。

在下面所示的 A* 实现中,算法进度显示在整个 while() 循环中使用 repaint()。

这会显示算法运行的进度,并且在我决定尝试向 gui 添加菜单之前一直运行良好。

所以现在我需要能够将 Display() 组件(它是一个 JPanel)添加到框架中并让它像以前一样运行,在算法运行时显示它。但是目前有大约一秒钟的暂停,并且只绘制了算法的最终状态,即好像它立即只绘制了 while() 循环中的最后一次 repaint() 调用。

在此感谢任何帮助。

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);
    }
}```

与大多数用户界面工具包一样,Swing 是单线程的。这意味着只有一个“事件队列”来处理对可视组件的所有更改和处理所有用户输入。

Repainting is one of those events.如果事件队列的处理被一个long-运行事件监听器挂起,后面的事件将不会被处理

您的 ActionListener 是从处理事件队列的线程调用的。因此,如果您的 actionPerformed 方法需要很长时间,则不会处理其他事件,包括绘制事件,直到 actionPerformed 方法 returns.

Thread.sleep(以及类似的方法)绝不能直接或间接地从 ActionListener 或任何其他事件侦听器中调用。睡眠调用应始终发生在不同的线程上。

有一些简单的方法可以在事件分派线程中定期执行代码,但在您的情况下,这还不够。

问题在于绘画依赖于您的 Map 对象和 Node 对象(我认为)。这意味着在事件分派线程之外更新 Map 或 Nodes 或它们的任何后代对象或数据是不安全的。在绘图方法同时读取地图的同时更改地图的状态将导致奇怪的视觉行为。

这个问题可以通过使用 class 来解决,它只代表您的绘画动作,并且保留自己的信息副本,因此它不依赖于任何其他对象。

例如,如果您的显示 class 正在绘制线条,您可以:

  • 在新线程中调用 findPath
  • 让显示包含 java.awt.geom.Line2D 个对象,而不是对地图的引用
  • 让 findPath 在事件调度线程中将 Line2D 对象添加到 Display,随着 findPath 的进行

这可能看起来像这样:

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

在上面的某处:

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

没有看到您的 Display class 如何决定绘制什么,我无法提供如何创建 Line2D 对象的示例。但我希望您在显示 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);
    }
}