尝试可视化递归工作流程并不是一件简单的事情。有太多问题需要回答,大多数递归工作流并不意味着以这种方式进行监控。您可以实施 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)解决方案是故意设计用于向用户提供某种反馈(即使用访问者模式)的解决方案,它将能够提供状态信息和用户“暂停”的机会线程,同时不会以其他方式损害核心算法。虽然第一个解决方案“有点”做到了这一点,但您可以清楚地看到它仍然需要大量工作才能维护并使其正常工作(事实上,我想我错过了某个地方的逆转更新:/)