【问题标题】:Update Java Swing JFrame on every loop在每个循环中更新 Java Swing JFrame
【发布时间】:2022-11-23 07:20:43
【问题描述】:

我写了一些生成迷宫的代码,它使用递归回溯算法。我还编写了一个可视化迷宫的类,但截至目前,DrawMaze 类仅输出完成的迷宫。但我想看看建造迷宫的全过程。

迷宫是用 RecursiveBacktracker.java 类生成的。并作为二维节点数组发送到 DrawMaze.java。

我如何在 DrawMaze paintComponent 方法中的每次迭代(对于 i 和 j)更新 JFrame?

递归回溯.java

import java.util.*;

public class RecursiveBacktracking {
    Stack<Node> stack = new Stack<>();
    int w;
    int h;
    int numVisited;
    Node[][] maze;

    public RecursiveBacktracking(int w, int h){
        // Constructor
    }

    public void genMaze()  {
        // Generates the maze using recursive backtracking
    }

    public void graphMaze() {
        DrawMaze dm = new DrawMaze(w, h, maze);
        dm.showMaze();
    }

    public static void main(String[] args)  {
        RecursiveBacktracking rb = new RecursiveBacktracking(20, 20);
        rb.genMaze();
        rb.graphMaze();
    }

}

绘制迷宫.java:

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

public class DrawMaze extends JPanel  {
    Node[][] maze;
    int width;
    int height;

    public DrawMaze(int width, int height, Node[][] maze){
        this.maze = maze;
        this.width = width;
        this.height = height;
    }

    public void paintComponent(Graphics g){

        //  EVERY TIME i or j CHANGE VALUE
        // JFrame should update

        for(int i = 0; i < width; i++){
            for(int j = 0; j < height; j++){
                 // Draws all nodes & their walls
            }
        }

    }

    public void showMaze() {
        JFrame f = new JFrame("Maze Visualizer");
        f.add(this);
        f.setSize(width, height);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

}

【问题讨论】:

  • 试试SwingWorker
  • 更具体地说:Tasks that Have Interim Results 您需要将嵌套循环移出方法 paintComponent 并移入另一个方法,在循环体中您需要调用方法 repaint(或类似的会导致重绘的方法)。
  • 我会从不同的方向解决这个问题。而不是 RecursiveBacktracking 能够控制工作流,它应该允许外部控制,例如 nextStep,这只会触发算法的一次交互。这将允许您使用类似 Swing Timer 的东西来控制速度和更好地安排重绘

标签: java swing


【解决方案1】:

尝试可视化递归工作流程并不是一件简单的事情。有太多问题需要回答,大多数递归工作流并不意味着以这种方式进行监控。您可以实施 visitor pattern 来帮助提供一些反馈,但这对解决此问题的帮助很小。

Swing 使用被动渲染工作流。也就是说,它只会在它认为需要时才更新,不像视频游戏那样不断更新用户界面。

因此,您需要一些方法来获取当前状态,在短时间内停止执行递归(否则用户将看不到发生了什么)并重新绘制 UI,所有这些都不会违反单线程性质 Swing ...简单?

长答案是,您将需要为此进行设计。您可能可以使用某种“包装器”类,它可以在提供反馈的同时以某种方式控制递归,但您仍然需要某种方法来“控制”递归流并以某种有意义的方式获得有关其状态的反馈方式(不只是在代码中抛出一堆 Thread.sleeps 并希望它有效)

nb:我从Program for Rat in a Maze | Backtracking-2 那里窃取了我的迷宫求解器的核心逻辑。这里的重点不是尝试解决迷宫解决工作流程,而是提出一些关于如何可视化工作流程的想法

SwingWorker

一种方法可能是使用一点点暴力。这会雇用一个observer pattern,当职位发生变化时(以及完成时,但不是必需的)会收到通知并触发 UI 更新。当迷宫中的位置发生变化时,SwingWorker 将停止 1 秒以允许 UI 更新并让用户监控变化

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.GridBagLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingWorker;

public class TestRecursiveMaze {

    public static void main(String[] args) {
        new TestRecursiveMaze();
    }

    public TestRecursiveMaze() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                MazeSolverPane testPane = new MazeSolverPane();

                JPanel panel = new JPanel(new GridBagLayout());
                JButton start = new JButton("Start");
                panel.add(start);
                start.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        start.setEnabled(false);
                        testPane.startSolving(new MazeSolverPane.SolverListener() {
                            @Override
                            public void solverDidComplete() {
                                start.setEnabled(true);
                            }
                        });
                    }
                });

                JFrame frame = new JFrame();
                frame.add(testPane);
                frame.add(panel, BorderLayout.SOUTH);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class MazeSolverPane extends JPanel {

        public interface SolverListener {

            public void solverDidComplete();
        }

        private RecursiveMazeSolver solver;
        private Maze maze;

        private Rectangle cell = new Rectangle(25, 25);

        private int[] lastKnownPoint;

        public MazeSolverPane() {
            int pattern[][] = {
                {1, 0, 0, 1},
                {1, 1, 0, 1},
                {0, 1, 1, 1},
                {1, 1, 0, 1}
            };

            maze = new Maze(pattern, 0, 0, 3, 0);
        }

        public void startSolving(SolverListener listener) {
            if (solver != null) {
                return;
            }
            solver = new RecursiveMazeSolver(maze);
            repaint();

            SwingWorker<Void, int[]> worker = new SwingWorker<Void, int[]>() {

                @Override
                protected Void doInBackground() throws Exception {
                    solver.solveMaze(new RecursiveMazeSolver.SolverListener() {
                        @Override
                        public void solverDidMoveTo(RecursiveMazeSolver solver, int x, int y) {
                            publish(new int[] { x, y });
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException ex) {
                            }
                        }

                        @Override
                        public void solverDidFinish(RecursiveMazeSolver solver) {
                        }
                    });
                    return null;
                }

                @Override
                protected void process(List<int[]> chunks) {
                    lastKnownPoint = chunks.get(chunks.size() - 1);
                    repaint();
                }

            };
            worker.addPropertyChangeListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    if (worker.getState() == SwingWorker.StateValue.DONE) {
                        listener.solverDidComplete();
                    }
                }
            });
            worker.execute();
        }

        public Maze getMaze() {
            return maze;
        }

        public RecursiveMazeSolver getSolver() {
            return solver;
        }

        @Override
        public Dimension getPreferredSize() {
            Maze maze = getMaze();
            return new Dimension((cell.width * maze.getWidth()) + 1, (cell.height * maze.getHeight()) + 1);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Maze maze = getMaze();
            for (int y = 0; y < maze.getHeight(); y++) {
                for (int x = 0; x < maze.getWidth(); x++) {
                    Graphics2D g2d = (Graphics2D) g.create();
                    int xPos = x * (cell.width);
                    int yPos = y * (cell.height);
                    g2d.translate(xPos, yPos);

                    RecursiveMazeSolver solver = getSolver();
                    if (lastKnownPoint != null && lastKnownPoint[0] == x && lastKnownPoint[1] == y) {
                        g2d.setColor(Color.MAGENTA);
                    } else if (solver != null && solver.hasBreadCrumbAt(x, y)) {
                        g2d.setColor(Color.YELLOW);
                    } else if (maze.isWallAt(x, y)) {
                        g2d.setColor(Color.DARK_GRAY);
                    } else if (maze.isStart(x, y)) {
                        g2d.setColor(Color.GREEN);
                    } else if (maze.isEnd(x, y)) {
                        g2d.setColor(Color.RED);
                    } else {
                        g2d.setColor(getBackground());
                    }
                    g2d.fill(cell);
                    g2d.setColor(getForeground());
                    g2d.draw(cell);
                    g2d.dispose();
                }
            }
        }

    }

    public class Maze {

        protected static final int WALL = 0;
        protected static final int PATH = 1;

        private int[][] maze;
        private int startX, startY;
        private int endX, endY;

        public Maze(int[][] maze, int startX, int startY, int endX, int endY) {
            this.maze = maze;
            this.startX = startX;
            this.startY = startY;
            this.endX = endX;
            this.endY = endY;
        }

        public boolean isStart(int x, int y) {
            return x == getStartX() && y == getStartY();
        }

        public boolean isEnd(int x, int y) {
            return x == getEndX() && y == getEndY();
        }

        public int[][] getMaze() {
            return maze;
        }

        public int getStartX() {
            return startX;
        }

        public int getStartY() {
            return startY;
        }

        public int getEndX() {
            return endX;
        }

        public int getEndY() {
            return endY;
        }

        public boolean isWallAt(int x, int y) {
            return getMaze()[y][x] == WALL;
        }

        public boolean isPathAt(int x, int y) {
            return getMaze()[y][x] == PATH;
        }

        public int getWidth() {
            return getMaze()[0].length;
        }

        public int getHeight() {
            return getMaze().length;
        }
    }

    public class RecursiveMazeSolver {

        public interface SolverListener {
            public void solverDidMoveTo(RecursiveMazeSolver solver, int x, int y);
            public void solverDidFinish(RecursiveMazeSolver solver);
        }

        private Maze maze;
        private int sol[][];

        public RecursiveMazeSolver(Maze maze) {
            this.maze = maze;
        }

        public Maze getMaze() {
            return maze;
        }

        protected void setBreadCrumbAt(int x, int y) {
            sol[y][x] = 1;
        }

        protected void removeBreadCrumbAt(int x, int y) {
            sol[y][x] = 0;
        }

        public boolean hasBreadCrumbAt(int x, int y) {
            return sol == null ? false : sol[y][x] == 1;
        }

        public void solveMaze(SolverListener listener) {
            Maze maze = getMaze();
            sol = new int[maze.getWidth()][maze.getHeight()];

            solveMaze(maze.getStartX(), maze.getStartY(), listener);
        }

        protected boolean solveMaze(int x, int y, SolverListener listener) {
            Maze maze = getMaze();

            if ((x < 0 || x >= maze.getWidth())) {
                return false;
            }
            if ((y < 0 || y >= maze.getHeight())) {
                return false;
            }

            if (x == maze.getEndX() && y == maze.getEndY()) {
                setBreadCrumbAt(x, y);
                listener.solverDidMoveTo(this, x, y);
                listener.solverDidFinish(this);
                return true;
            }

            if (maze.isPathAt(x, y) && !hasBreadCrumbAt(x, y)) {
                setBreadCrumbAt(x, y);
                listener.solverDidMoveTo(this, x, y);

                if (solveMaze(x + 1, y, listener)) {
                    return true;
                }
                if (solveMaze(x, y + 1, listener)) {
                    return true;
                }
                if (solveMaze(x - 1, y, listener)) {
                    return true;
                }
                if (solveMaze(x, y - 1, listener)) {
                    return true;
                }

                removeBreadCrumbAt(x, y);
            }

            return false;
        }
    }
}

详情请见Worker Threads and SwingWorker

控制器工作流程

以下使用“受控”工作流程。这是一个在外部控制每个停止点的站点,因此每次调用 next 都会进行一次迭代以解决迷宫问题。这可以在调用者想要的任何时候完成,并且允许更高级别的控制,因为调用者决定何时要移动到下一次迭代。

虽然该示例使用了 Swing Timer,但您可以改用“下一步”按钮。

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.GridBagLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.StringJoiner;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class TestSteppedMaze {

    public static void main(String[] args) {
        new TestSteppedMaze();
    }

    public TestSteppedMaze() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                MazeSolverPane testPane = new MazeSolverPane();

                JPanel panel = new JPanel(new GridBagLayout());
                JButton start = new JButton("Start");
                panel.add(start);
                start.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        start.setEnabled(false);
                        testPane.startSolving(new MazeSolverPane.SolverListener() {
                            @Override
                            public void solverDidComplete() {
                                start.setEnabled(true);
                            }
                        });
                    }
                });

                JFrame frame = new JFrame();
                frame.add(testPane);
                frame.add(panel, BorderLayout.SOUTH);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class MazeSolverPane extends JPanel {

        public interface SolverListener {

            public void solverDidComplete();
        }

        private SteppedMazeSolver solver;
        private Maze maze;

        private Rectangle cell = new Rectangle(25, 25);

        public MazeSolverPane() {
            int pattern[][] = {
                {1, 0, 0, 1},
                {1, 1, 0, 1},
                {0, 1, 1, 1},
                {1, 1, 0, 1}
            };

            maze = new Maze(pattern, 0, 0, 3, 0);
        }

        public void startSolving(SolverListener listener) {
            if (solver != null) {
                return;
            }
            solver = new SteppedMazeSolver(maze);
            repaint();
            Timer timer = new Timer(1000, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (solver.next()) {
                        ((Timer) e.getSource()).stop();
                        listener.solverDidComplete();
                    }
                    repaint();
                }
            });
            timer.start();
        }

        public Maze getMaze() {
            return maze;
        }

        public SteppedMazeSolver getSolver() {
            return solver;
        }

        @Override
        public Dimension getPreferredSize() {
            Maze maze = getMaze();
            return new Dimension((cell.width * maze.getWidth()) + 1, (cell.height * maze.getHeight()) + 1);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Maze maze = getMaze();
            for (int y = 0; y < maze.getHeight(); y++) {
                for (int x = 0; x < maze.getWidth(); x++) {
                    Graphics2D g2d = (Graphics2D) g.create();
                    int xPos = x * (cell.width);
                    int yPos = y * (cell.height);
                    g2d.translate(xPos, yPos);

                    if (solver != null && solver.getCurrentPoint().equals(new Point(x, y))) {
                        g2d.setColor(Color.MAGENTA);
                    } else if (solver != null && solver.isVisited(x, y)) {
                        g2d.setColor(Color.YELLOW);
                    } else if (maze.isWallAt(x, y)) {
                        g2d.setColor(Color.DARK_GRAY);
                    } else if (maze.isStart(x, y)) {
                        g2d.setColor(Color.GREEN);
                    } else if (maze.isEnd(x, y)) {
                        g2d.setColor(Color.RED);
                    } else {
                        g2d.setColor(getBackground());
                    }
                    g2d.fill(cell);
                    g2d.setColor(getForeground());
                    g2d.draw(cell);
                    g2d.dispose();
                }
            }
        }

    }

    public class Maze {

        protected static final int WALL = 0;
        protected static final int PATH = 1;

        private int[][] maze;
        private int startX, startY;
        private int endX, endY;

        public Maze(int[][] maze, int startX, int startY, int endX, int endY) {
            this.maze = maze;
            this.startX = startX;
            this.startY = startY;
            this.endX = endX;
            this.endY = endY;
        }

        public boolean isStart(int x, int y) {
            return x == getStartX() && y == getStartY();
        }

        public boolean isEnd(int x, int y) {
            return x == getEndX() && y == getEndY();
        }

        public int[][] getMaze() {
            return maze;
        }

        public int getStartX() {
            return startX;
        }

        public int getStartY() {
            return startY;
        }

        public int getEndX() {
            return endX;
        }

        public int getEndY() {
            return endY;
        }

        public boolean isWallAt(int x, int y) {
            return getMaze()[y][x] == WALL;
        }

        public boolean isPathAt(int x, int y) {
            return getMaze()[y][x] == PATH;
        }

        public int getWidth() {
            return getMaze()[0].length;
        }

        public int getHeight() {
            return getMaze().length;
        }

        @Override
        public String toString() {
            StringJoiner outter = new StringJoiner("
");
            for (int y = 0; y < getHeight(); y++) {
                StringBuilder sb = new StringBuilder(getWidth());
                for (int x = 0; x < getWidth(); x++) {
                    sb.append(Integer.toString(maze[y][x]));
                }
                outter.add(sb);
            }
            return outter.toString();
        }
    }

    public class Point {

        private int x, y;

        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }

        public Point delta(int x, int y) {
            return new Point(getX() + x, getY() + y);
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Point)) {
                return false;
            }
            Point other = (Point) obj;
            if (other == this) {
                return true;
            } else if (other.getX() == getX() && other.getY() == getY()) {
                return true;
            }
            return false;
        }

        @Override
        public String toString() {
            return getX() + "x" + getY();
        }

    }

    public class SteppedMazeSolver {

        private Maze maze;

        private Deque<Point> visited;
        private List<Point> blocked;

        private Point currentPoint;

        public SteppedMazeSolver(Maze maze) {
            this.maze = maze;
            visited = new ArrayDeque<>();
            blocked = new ArrayList<>(16);
            currentPoint = new Point(maze.getStartX(), maze.getStartY());
            visited.add(currentPoint);
        }

        public Maze getMaze() {
            return maze;
        }

        public Point getCurrentPoint() {
            return currentPoint;
        }

        public boolean isVisited(int x, int y) {
            Iterator<Point> iterator = visited.iterator();
            while (iterator.hasNext()) {
                Point point = iterator.next();
                if (point.getX() == x && point.getY() == y) {
                    return true;
                }
            }
            return false;
        }

        protected boolean isAtEnd() {
            Maze maze = getMaze();
            return currentPoint.getX() == maze.getEndX() && currentPoint.getY() == maze.getEndY();
        }

        public boolean next() {
            if (isAtEnd()) {
                System.out.println("I've escaped");
                return true;
            }
            if (canMoveRight()) {
                System.out.println("Right");
                currentPoint = currentPoint.delta(1, 0);
                visited.add(currentPoint);
            } else if (canMoveLeft()) {
                System.out.println("Left");
                currentPoint = currentPoint.delta(-1, 0);
                visited.add(currentPoint);
            } else if (canMoveDown()) {
                System.out.println("Down");
                currentPoint = currentPoint.delta(0, 1);
                visited.add(currentPoint);
            } else if (canMoveUp()) {
                System.out.println("Up");
                currentPoint = currentPoint.delta(0, -1);
                visited.add(currentPoint);
            } else {
                System.out.println("Blocked at " + currentPoint);
                blocked.add(currentPoint);
                visited.removeLast();
                currentPoint = visited.getLast();
            }
            return isAtEnd();
        }

        protected boolean canMoveRight() {
            return canMoveTo(1, 0);
        }

        protected boolean canMoveLeft() {
            return canMoveTo(-1, 0);
        }

        protected boolean canMoveUp() {
            return canMoveTo(0, -1);
        }

        protected boolean canMoveDown() {
            return canMoveTo(0, 1);
        }

        protected boolean canMoveTo(int xDelta, int yDelta) {
            Point nextPoint = currentPoint.delta(xDelta, yDelta);

            if (nextPoint.getX() < 0 || nextPoint.getY() < 0) {
                return false;
            }
            Maze maze = getMaze();
            if (nextPoint.getX() >= maze.getWidth() || nextPoint.getY() >= maze.getHeight()) {
                return false;
            }
            if (blocked.contains(nextPoint)) {
                return false;
            }
            if (visited.contains(nextPoint)) {
                return false;
            }

            return maze.isPathAt(nextPoint.getX(), nextPoint.getY());
        }
    }
}

在任何人跳下我的喉咙之前,不,这不是正常意义上的“递归”解决方案。不是 next 方法不断调用自身直到完成,而是外部控制器正在执行此操作。

有关详细信息,请参阅How to Use Swing Timers

哪种解决方案更好?

好吧,老实说,两者都不是。两者都在某处做出妥协,以促进呈现其状态变化的能力。你也可以有一个基于“监视器锁”的解决方案,但你仍然在妥协底层解决方案以呈现状态。

一个“更好的”(ish)解决方案是故意设计用于向用户提供某种反馈(即使用访问者模式)的解决方案,它将能够提供状态信息和用户“暂停”的机会线程,同时不会以其他方式损害核心算法。虽然第一个解决方案“有点”做到了这一点,但您可以清楚地看到它仍然需要大量工作才能维护并使其正常工作(事实上,我想我错过了某个地方的逆转更新:/)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-06-11
    • 2015-09-08
    • 1970-01-01
    • 1970-01-01
    • 2016-06-09
    • 2012-01-29
    • 2011-05-25
    相关资源
    最近更新 更多