摘要: 使用棧的數據結構及相應的回溯算法實現迷宮創建及求解,帶點JavaGUI 的基礎知識。
難度: 中級
迷宮問題是棧的典型應用,棧通常也與回溯算法連用。 回溯算法的基本描述是:
(1) 選擇一個起始點;
(2) 如果已達目的地, 則跳轉到 (4); 如果沒有到達目的地, 則跳轉到 (3) ;
(3) 求出當前的可選項;
a. 若有多個可選項,則通過某種策略選擇一個選項,行進到下一個位置,然后跳轉到 (2);
b. 若行進到某一個位置發現沒有選項時,就回退到上一個位置,然后回退到 (2) ;
(4) 退出算法。
在回溯算法的實現中,通常要使用棧來保存行進中的位置及選項。本文給出自己寫的迷宮回溯算法實現及簡要說明。
1. 首先給出棧的抽象數據結構 StackADT<T> : 主要是入棧、出棧、判斷空、長度、展示操作;
package zzz.study.javagui.maze; public interface StackADT<T> { /* 判斷是否為空棧;若為空,返回TRUE, 否則返回FALSE */ public boolean isEmpty(); /* 入棧操作: 將元素 e 壓入棧中 */ public void push(T e); /* 出棧操作: 若棧非空,將棧頂元素彈出並返回;若棧空,則拋出異常 */ public T pop(); /* 返回棧頂元素,但並不將其彈出 */ public T peek(); /* 返回棧長度,即棧中元素的數目 */ public int size(); /* 遍歷操作: 若棧非空,遍歷棧中所有元素 */ public String toString(); }
2. 可變長的棧的實現 DyStack<T>: 借助 ArrayList 及一個指針來實現。注意到這里使用泛型來保證棧的通用性。
package zzz.study.javagui.maze; import java.util.ArrayList; public class DyStack<T> implements StackADT<T> { private final int INIT_STACK_SIZE = 20; ArrayList<T> ds; // 棧元素列表 private int top; // 棧頂索引:當前棧頂元素的下一個元素的索引 /* * 構造器: * 使用默認容量創建一個空棧 * */ public DyStack() { top = 0; ds = new ArrayList<T>(INIT_STACK_SIZE); } /* * 構造器: * 使用指定容量創建一個空棧 * */ public DyStack(int capacity) { top = 0; ds = new ArrayList<T>(capacity); } public boolean isEmpty() { if (top == 0) return true; else return false; } public void clear() { top = 0; ds.clear(); } public void push(T e) { ds.add(top, e); top++; } public T pop() { if (ds.isEmpty()) throw new StackEmptyException("The stack has been empty!"); top--; T result = ds.get(top); ds.set(top, null); return result; } public T peek() { if (ds.isEmpty()) return null; return ds.get(top - 1); } public int size() { return top; } public String toString() { StringBuilder content = new StringBuilder(" "); for (int i = 0; i < top; i++) { content.append(" --> "); content.append(ds.get(i)); } return content.toString(); } public int getTop() { return top; } } class StackEmptyException extends RuntimeException { public StackEmptyException(String msg) { super(msg); } }
3. 迷宮位置的數據結構 Position: 這里為了"節約內存"方向選擇了 byte 類型,實際上在小型程序里是不必要的,帶來了繁瑣的類型轉換,也帶來了一些隱藏的問題。比如 Set<Byte> dirs 包含 byte 1; 但 dirs.contains(1) 會返回 false , 而 dirs.contains((byte)1) 才會返回 true. Position 要在集合中使用,最好實現 equals 和 hashCode 方法。注意 equals 不要寫成了 equal, hashCode 不要寫成 hashcode , 這些我都犯過錯的 :)
package zzz.study.javagui.maze; /* * 迷宮中的通道位置模型: * row: 所處位置的行坐標 * col: 所處位置的列坐標 * dir: 將要進行的搜索方向: 正東 1; 正南 2; 正西3; 正北 4; */ public class Position { private int row; private int col; private byte dir; public Position() { row = 0; col = 0; dir = 0; } public Position(int row, int col, byte dir) { this.row = row; this.col = col; this.dir = dir; } public Position(int row, int col) { this(row, col, 0); } public Position(int row, int col, int dir) { this(row, col, (byte)dir); } public int getRow() { return row; } public int getCol() { return col; } public short getDir() { return dir; } public String toString() { String dirStr = ""; switch (dir) { case 1: dirStr = "正東"; break; case 2: dirStr = "正南"; break; case 3: dirStr = "正西"; break; case 4: dirStr = "正北"; break; default: dirStr = "UNKNOWN"; break; } return "(" + row + "," + col + "," + dirStr + ")"; } public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof Position) { Position p = (Position) obj; if (p.row == this.row && p.col == this.col && p.dir == this.dir) { return true; } } return false; } public int hashCode() { int result = 17; result = result * 31 + row; result = result * 31 + col; result = result * 31 + dir; return result; } }
4. 迷宮的核心實現 Maze :
里面注釋說的比較清楚了,有五點說明一下:
(1) 由於要使用回溯算法,必須使用一個 Map<Position, List<triedDirs>> 來記錄每個位置已經嘗試過的方向,使用一個棧 stackForCreate 記錄當前行進的位置軌跡; 當回溯到前面的位置時,不再重復已經嘗試過的方向,避免重復嘗試陷入無限循環;
(2) 方向選擇上,耗費了一點空間,簡單地實現了方向選擇的概率設置;也就是將未嘗試的方向列表按概率次數擴展成新的方向列表,然后隨機從這個新的方向列表中選擇;
(3) 在抵達迷宮邊界時,對方向加以限制,只允許往出口方向走;否則,回走會形成環路,由於回溯的特性,會將環路里面的牆全部"吃掉"!
(4) 在迷宮展示上,為了簡便使用了字符 IIII 完美等於 5 個空格完美等於 2 個 G, 實現了對齊問題; 雖然使用等寬字體,但似乎未起作用, 也嘗試過 T, [T], [I], 這樣的符號,但與空格難以對齊。寫這個程序還是費了不少心思的 ^_^ 注意到 Maze 繼承了 Observable , 支持 GUI 展示, 可以展示迷宮生成的過程, 也可以看到空格是如何一步步"吃掉"由 IIII 組成的牆的, interesting ~~
(5) 為了展示出誤導路徑, 采用分治策略將迷宮切分成若干個子迷宮矩陣分別求解, 並將上一個子迷宮矩陣的終止點與下一個子迷宮矩陣的起始點銜接起來確保一定有一條通路從入口抵達出口。
(6) 為什么創建迷宮的代碼比求解迷宮的代碼更多呢?因為求解迷宮可以盡可能地朝正東或正南即出口方向走,但創建迷宮必須選擇隨機方向。
package zzz.study.javagui.maze; import java.util.*; import java.util.concurrent.TimeUnit; public class Maze extends Observable { // 定義迷宮大小:行數 rows 和列數 cols private final int rows; private final int cols; // 定義迷宮出口點位置: 行坐標 EXIT_ROW 和 列坐標 EXIT_COL private final int EXIT_ROW; private final int EXIT_COL; // 定義迷宮矩陣mazeMatrix 和 標記數組 mark private boolean[][] mazeMatrix; // true: 可通行; false: 不可通行 private short[][] mark; private String mazeStr = ""; // 迷宮的字符串表示 private String solution = ""; // 迷宮的解的字符串表示 // 定義移動方向表 private byte[][] move = { {0, 1}, // 正東 , move[0] 方向一 {1, 0}, // 正南 , move[1] 方向二 {0, -1}, // 正西 , move[2] 方向三 {-1, 0}, // 正北 , move[3] 方向四 }; // 存放所有方向, 使用該集合與某個位置已嘗試方向的差集來獲取其未嘗試的方向 private static final Set<Byte> allDirs = new HashSet<Byte>(Arrays.asList(new Byte[] {0, (byte)1, (byte)2, (byte)3})); private DyStack<Position> stack; // 使用棧存放迷宮通路路徑 private boolean isCreatedFinished; // 迷宮是否創建完成 private Random rand = new Random(); public Maze(int rows, int cols) { this.rows = rows; this.cols = cols; EXIT_ROW = rows - 1; EXIT_COL = cols - 1; mazeMatrix = new boolean[rows][cols]; mark = new short[rows][cols]; } /** * 迷宮求解:求解迷宮並設置解的表示 */ public void solve() { if (hasPath()) { setSolutionStr(); } else { noSolution(); } } /** * 迷宮矩陣的字符串表示 */ public String toString() { StringBuilder mazeBuf = new StringBuilder("\n"); String mazeCell = ""; for (int i = 0; i < rows; i++) { if (i == 0) { mazeBuf.append("Entrance => "); } else { // the width of "Entrance => " is Equal to the width of 20 spaces. mazeBuf.append(indent(20)); } mazeBuf.append('|'); for (int j = 0; j < cols; j++) { if (mazeMatrix[i][j] == false) { mazeCell = String.format("%4s", "IIII"); } else { // 存在通路 if (mark[i][j] == 1) { mazeCell = String.format("%2s", "GG"); } else { mazeCell = String.format("%5s", ""); } } mazeBuf.append(mazeCell); } if (i == rows - 1) { mazeBuf.append("| => Exit\n"); } else { mazeBuf.append("|\n"); } } mazeStr = mazeBuf.toString(); return mazeStr; } /** * 監聽按鈕事件后發生改變,並通知觀察者此變化的發生 */ public void change() { setChanged(); notifyObservers(); } public String getSolution() { return solution; } public boolean isCreatedFinished() { return isCreatedFinished; } /** * 將迷宮還原為初始狀態 */ public void reset() { for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { mazeMatrix[i][j] = false; } } isCreatedFinished = false; } public void createMaze() { for (int i = 0; i <= EXIT_ROW; i++) { for (int j = 0; j <= EXIT_COL; j++) { // 初始無通路 mazeMatrix[i][j] = false; } } if (rows < 10 && cols < 10) { StackADT<Position> createPaths = new DyStack<Position>(rows+cols); createMaze(0,0, EXIT_ROW, EXIT_COL, createPaths); isCreatedFinished = true; change(); } else { StackADT<Position> createPaths = new DyStack<Position>(rows+cols); List<int[][]> smallParts = divideMaze(rows, cols, 4); for (int[][] parts: smallParts) { createMaze(parts[0][0], parts[0][1], parts[1][0], parts[1][1], createPaths); if (parts[0][1] != 0) { // 銜接點打通, 保證總是有一條從入口到出口的通路 mazeMatrix[parts[0][0]][parts[0][1]-1] = true; } } isCreatedFinished = true; change(); } } /* * divide [1:rows-1] into n sectors */ private static List<Integer> divideN(int rows, int n) { int each = rows/n; int start = 0; List<Integer> divs = new ArrayList<Integer>(); for (int i=0; i<n;i++) { divs.add(start + i*each); } divs.add(rows-1); return divs; } private static List<int[][]> divideMaze(int rows, int cols, int n) { List<Integer> nrowParts = divideN(rows, n); List<Integer> ncolParts = divideN(cols, n); System.out.println("nrowParts: " + nrowParts); List<int[][]> results = new ArrayList<int[][]>(); int rowsize = nrowParts.size(); int colsize = ncolParts.size(); for (int i=0; i<rowsize-1; i++) { for (int j=0; j<colsize-1; j++) { int[][] smallParts = new int[2][2]; int startRow = nrowParts.get(i); int exitRow = (i == rowsize-2) ? (nrowParts.get(i+1)) : (nrowParts.get(i+1)-1); int startCol = ncolParts.get(j); int exitCol = (j == colsize-2) ? (ncolParts.get(j+1)) : (ncolParts.get(j+1)-1); smallParts[0][0] = startRow; smallParts[0][1] = startCol; smallParts[1][0] = exitRow; smallParts[1][1] = exitCol; System.out.println("div: " + startRow + " " + startCol + " " + exitRow + " " + exitCol); results.add(smallParts); } } return results; } /* * 生成迷宮, 采用 Recursive Backtracking. Refer to: * <a href="http://weblog.jamisbuck.org/2010/12/27/maze-generation-recursive-backtracking"/> */ public void createMaze(int startRow, int startCol, int exitRow, int exitCol, StackADT<Position> stackForCreate) { mazeMatrix[startRow][startCol] = true; int currRow = startRow; int currCol = startCol; byte nextdir = 0; // 當前可能選擇的方向 int nextRow = currRow; // 下一個可能到達的相鄰位置 int nextCol = currCol; // 每個位置已經嘗試過的方向,用於回溯時確定有效的下一個方向 Map<Position, Set<Byte>> triedPaths = new HashMap<Position, Set<Byte>>(); List<Byte> allDirsWalked = new ArrayList<Byte>(); while (currRow != exitRow || currCol != exitCol) { do { nextdir = getNextRandDir(currRow, currCol, startRow, startCol, exitRow, exitCol, triedPaths); System.out.println("nextdir: " + nextdir); allDirsWalked.add(nextdir); while (nextdir == -1) { Position p = stackForCreate.pop(); currRow = p.getRow(); currCol = p.getCol(); nextdir = getNextRandDir(currRow, currCol, startRow, startCol, exitRow, exitCol, triedPaths); allDirsWalked.add(nextdir); System.out.println("Back to: " + p); } nextRow = currRow + move[nextdir][0]; // 取得下一個可能到達的相鄰位置 nextCol = currCol + move[nextdir][1]; addTriedPaths(currRow, currCol, nextdir, triedPaths); System.out.println(currRow + " " + currCol + " " + nextdir + " " + nextRow + " " + nextCol); } while (!checkBound(nextRow, nextCol, startRow, startCol, exitRow, exitCol)); // 已嘗試過的路徑, 分兩種情況: 所有方向都嘗試過或仍有方向沒有嘗試過 // 如果所有方向都嘗試過, 那么需要回退到上一個位置再嘗試 if (mazeMatrix[nextRow][nextCol]) { if (hasAllPathTried(currRow, currCol, triedPaths)) { Position p = stackForCreate.pop(); currRow = p.getRow(); currCol = p.getCol(); System.out.println("Back to: " + p); } continue; } mazeMatrix[nextRow][nextCol] = true; stackForCreate.push(new Position(currRow, currCol, nextdir)); currRow = nextRow; currCol = nextCol; // 更新 UI 界面, 顯示迷宮當前狀態 try { change(); TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException ie) { System.err.println("pause between maze-creating steps interrupted"); } } mazeMatrix[exitRow][exitCol] = true; statDirWalked(allDirsWalked); } /* * 當前位置的所有方向是否都已經嘗試過 */ private boolean hasAllPathTried(int currRow, int currCol, Map<Position, Set<Byte>> triedPaths) { Set<Byte> triedDirs = triedPaths.get(new Position(currRow, currCol)); if (triedDirs == null) { triedDirs = new HashSet<Byte>(); } Set<Byte> allDirsCopy = new HashSet<Byte>(allDirs); allDirsCopy.removeAll(triedDirs); return allDirsCopy.isEmpty(); } /* * 記錄當前位置已經嘗試過的方向, 避免后續走重復路子 */ private void addTriedPaths(int currRow, int currCol, byte nextdir, Map<Position, Set<Byte>> triedPaths) { Position currPos = new Position(currRow, currCol); Set<Byte> triedDirs = triedPaths.get(currPos); if (triedDirs == null) { triedDirs = new HashSet<Byte>(); } triedDirs.add(nextdir); triedPaths.put(currPos, triedDirs); } // 抵達迷宮最上邊界時, 僅允許往東或往南走 private static final byte[] firstRowAllowedDirs = new byte[] { (byte)0, (byte)1 }; // 抵達迷宮最下邊界時, 僅允許往東或往北走 private static final byte[] lastRowAllowedDirs = new byte[] { (byte)0, (byte)3 }; // 抵達迷宮最左邊界時, 僅允許往東或往南走 private static final byte[] firstColAllowedDirs = new byte[] { (byte)0, (byte)1 }; // 抵達迷宮最右邊界時, 僅允許往南或往西走 private static final byte[] lastColAllowedDirs = new byte[] { (byte)1, (byte)2 }; /* * 獲取下一個隨機的方向, [0,1,2,3] , 若均已嘗試, 返回 -1 */ private byte getNextRandDir(int currRow, int currCol, int startRow, int startCol, int exitRow, int exitCOl, Map<Position, Set<Byte>> triedPaths) { Set<Byte> triedDirs = (Set<Byte>) triedPaths.get(new Position(currRow, currCol)); if (triedDirs == null) { triedDirs = new HashSet<Byte>(); } // 如果抵達迷宮邊界, 則優先向出口方向走, 避免回走會形成環路, 破壞所有的牆 if (reachUpBound(currRow, startRow, exitRow)) { if (triedDirs.contains(firstRowAllowedDirs[0]) && triedDirs.contains(firstRowAllowedDirs[1])) { return -1; } return firstRowAllowedDirs[rand.nextInt(2)]; } if (reachLowBound(currRow, startRow, exitRow)) { if (triedDirs.contains(lastRowAllowedDirs[0]) && triedDirs.contains(lastRowAllowedDirs[1])) { return -1; } return lastRowAllowedDirs[rand.nextInt(2)]; } if (reachLeftBound(currCol, startCol, exitCOl)) { if (triedDirs.contains(firstColAllowedDirs[0]) && triedDirs.contains(firstColAllowedDirs[1])) { return -1; } return firstColAllowedDirs[rand.nextInt(2)]; } if (reachRightBound(currCol, startCol, exitCOl)) { if (triedDirs.contains(lastColAllowedDirs[0]) && triedDirs.contains(lastColAllowedDirs[1])) { return -1; } return lastColAllowedDirs[rand.nextInt(2)]; } Set<Byte> allDirsCopy = new HashSet<Byte>(allDirs); allDirsCopy.removeAll(triedDirs); List<Byte> possibleDirs = getRandomDirs(allDirsCopy); Byte[] nonTriedDirs = possibleDirs.toArray(new Byte[possibleDirs.size()]); if (nonTriedDirs.length == 0) { return -1; } else { byte nextdir = nonTriedDirs[rand.nextInt(nonTriedDirs.length)]; return nextdir; } } /* * 抵達迷宮上邊界 */ private boolean reachUpBound(int currRow, int startRow, int exitRow) { if (startRow < exitRow) { return currRow == startRow; } else { return currRow == exitRow; } } /* * 抵達迷宮下邊界 */ private boolean reachLowBound(int currRow, int startRow, int exitRow) { if (startRow > exitRow) { return currRow == startRow; } else { return currRow == exitRow; } } /* * 抵達迷宮左邊界 */ private boolean reachLeftBound(int currCol, int startCol, int exitCol) { if (startCol < exitCol) { return currCol == startCol; } else { return currCol == exitCol; } } /* * 抵達迷宮右邊界 */ private boolean reachRightBound(int currCol, int startCol, int exitCol) { if (startCol > exitCol) { return currCol == startCol; } else { return currCol == exitCol; } } /* * 統計隨機選擇的方向出現的比例 */ private void statDirWalked(List<Byte> allDirWalked) { int[] counts = new int[4]; int backCount = 0; for (Byte b: allDirWalked) { if (b != -1) { counts[b] += 1; } else { backCount++; } } int total = allDirWalked.size(); for (int i=0; i < counts.length; i++) { System.out.printf("P[%d]=%g ", i, (double)counts[i] / total); } System.out.println("back count: " + backCount); System.out.println(allDirWalked); } // 各方向出現的概率設置, private static final int P0 = 36; private static final int P1 = 36; private static final int P2 = 14; private static final int P3 = 14; /* * 擴展 nonTriedDirs 使得 0 (向前) , 1 (向下) 出現的概率更大一些, 減少回退的幾率 */ private List<Byte> getRandomDirs(Set<Byte> nonTriedDirs) { List<Byte> selectDirs = new ArrayList<Byte>(); if (nonTriedDirs.contains((byte)0)) { selectDirs.addAll(createNnums((byte) 0, P0)); } if (nonTriedDirs.contains((byte)1)) { selectDirs.addAll(createNnums((byte) 1, P1)); } if (nonTriedDirs.contains((byte)2)) { selectDirs.addAll(createNnums((byte) 2, P2)); } if (nonTriedDirs.contains((byte)3)) { selectDirs.addAll(createNnums((byte) 3, P3)); } return selectDirs; } private static List<Byte> createNnums(byte b, int num) { List<Byte> occurs = new ArrayList<Byte>(); for (int i=0; i<num; i++) { occurs.add(b); } return occurs; } private boolean checkBound(int row, int col, int startRow, int startCol, int exitRow, int exitCol) { boolean rowBound = false; if (startRow < exitRow) { rowBound = (row <= exitRow && row >= startRow); } else { rowBound = (row <= startRow && row >= exitRow); } boolean colBound = false; if (startCol < exitCol) { colBound = (col <= exitCol && col >= startCol); } else { colBound = (col <= startCol && col >= exitCol); } return rowBound && colBound; } /* * 求解迷宮路徑 */ private boolean hasPath() { int row = 0, col = 0, dir = 0; // 當前位置的行列位置坐標及搜索移動方向 int nextRow, nextCol; // 下一步移動要到達的位置坐標 boolean found = false; Position position = new Position(0, 0, (byte) 0); // 通道的臨時存放點 stack = new DyStack<Position>(rows + cols); // 創建指定容量的空棧 mark[0][0] = 1; stack.push(position); while (!stack.isEmpty() && !found) { try { position = stack.pop(); // 如四個搜索方向的相鄰位置都無通道,則出棧並退回到最近一次經過的位置 row = position.getRow(); col = position.getCol(); dir = position.getDir(); while (dir < 4 && !found) { nextRow = row + move[dir][0]; // 取得下一個可能到達的相鄰位置 nextCol = col + move[dir][1]; if (nextRow == EXIT_ROW && nextCol == EXIT_COL) { // 找到出口點,即存在通路徑 found = true; position = new Position(row, col, (byte) ++dir); stack.push(position); position = new Position(EXIT_ROW, EXIT_COL, (byte) 1); stack.push(position); } else if (checkBound(nextRow, nextCol, 0, 0, EXIT_ROW, EXIT_COL) && mazeMatrix[nextRow][nextCol] == true && mark[nextRow][nextCol] == 0) { // 沒有找到出口點,但當前搜索方向的相鄰位置為通道,則前進到相鄰位置,並在相鄰位置依序按照前述四個方向進行搜索移動 mark[nextRow][nextCol] = 1; position = new Position(row, col, (byte) ++dir); stack.push(position); row = nextRow; col = nextCol; dir = 0; } else { /* 沒有找到出口點,且當前搜索方向的相鄰位置為牆,或已搜索過,或超出迷宮邊界, * 則向當前位置的下一個搜索方向進行搜索移動 */ ++dir; } } } catch (Exception e) { System.out.print("棧空!"); e.printStackTrace(); } } mark[EXIT_ROW][EXIT_COL] = 1; if (found) return true; else return false; } private void setSolutionStr() { solution = "\n所找到的通路路徑為: \n" + stack + "\n\n"; solution += "其中,(x,y,z)表示從坐標點(x,y)向z方向移動\n\n"; } private void noSolution() { // 迷宮無解的字符串表示 solution = "迷宮無解!\n"; } /* * 顯示迷宮時,為美觀起見, 縮進 n 個字符 */ private String indent(int n) { StringBuilder indentBuf = new StringBuilder(); while (n-- > 0) { indentBuf.append(' '); } return indentBuf.toString(); } }
5. GUI 界面: 主要使用 "觀察者" 模式來實現。其中 Maze 是被觀察者, 當 Maze 發生變化時,會去通知后面會給出的觀察者 MazePanel ; 而 Maze 的觸發是在 Model 類里。
Model 類: 監聽按鈕點擊事件, 獲取輸入來觸發生成迷宮。 注意到這里另起了線程去執行, 使得在 maze.CreateMaze 方法里的 sleep 不會阻塞 Ui 線程更新界面; 寫 GUI 記住兩句話: (1) 更新組件和界面相關的事情一定要在事件分發線程里做; 與界面無關的計算和 IO 不要在事件分發線程里做,因為那樣會阻塞 UI 線程,導致界面無法更新(假死); (2) 事件處理方法 actionPerformed 和 SwingUtilities.invokeLater 里的代碼是在事件分發線程里做的;
package zzz.study.javagui.maze; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Observer; import java.util.regex.Pattern; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; /** * 監聽按鈕事件,並改變 Maze 對象(被觀察者) */ public class Model implements ActionListener { private MazeGUI app; // 用於從中獲取數據的 GUI 界面 public void actionPerformed(ActionEvent event) { JTextField inputRow = app.getInputRow(); JTextField inputCol = app.getInputCol(); JPanel mazePanel = app.getMazePanel(); String rowStr = inputRow.getText(); String colStr = inputCol.getText(); String regex = "^\\s*[1-9][0-9]?\\s*$"; if (rowStr.matches(regex) && colStr.matches(regex)) { int rows = Integer.parseInt(inputRow.getText()); int cols = Integer.parseInt(inputCol.getText()); final Maze maze = new Maze(rows,cols); maze.addObserver((Observer) mazePanel); new Thread(new Runnable() { public void run() { if (maze.isCreatedFinished()) { maze.reset(); maze.change(); } maze.createMaze(); } }).start(); } else { JOptionPane.showMessageDialog(null, "對不起,您的輸入有誤, 請輸入 [1-99] 之間的任意數字!", "警告", JOptionPane.WARNING_MESSAGE); } } public void setGUI(MazeGUI app) { this.app = app; } }
6. MazePanel : Maze 的觀察者, 當 Maze 狀態發生變化時,調用 change 方法時,就會通知該面板更新其顯示。注意到更新 UI 在 SwingUtilities.invokeLater 方法中完成;其它事情則在外面做。
package zzz.study.javagui.maze; import java.awt.BorderLayout; import java.awt.Font; import java.util.Observable; import java.util.Observer; import javax.swing.*; import javax.swing.border.TitledBorder; /** * 迷宮面板: 按鈕事件的觀察者 */ public class MazePanel extends JPanel implements Observer { private String title; private String text; private JTextArea infoArea; public MazePanel(String title, Font font) { this(title, ""); infoArea.setFont(font); } public MazePanel(String title, String text) { this.title = title; this.text = text; infoArea = new JTextArea(text); //infoArea.setEnabled(false); setLayout(new BorderLayout()); setBorder(new TitledBorder(title)); add(infoArea, BorderLayout.CENTER); add(new JScrollPane(infoArea)); } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getText() { return text; } public void setText(String text) { this.text = text; } public void update(Observable o, Object arg) { Maze m = (Maze)o; if (m.isCreatedFinished()) { m.solve(); text = "" + m + "\n" + m.getSolution(); } else { text = "" + m + "\n"; } infoArea.setText(text); SwingUtilities.invokeLater(new Runnable() { public void run() { updateUI(); } }); } }
7. MazeGUI 界面: 組裝界面組件, 啟動應用。
package zzz.study.javagui.maze; import java.awt.BorderLayout; import java.awt.Container; import java.awt.FlowLayout; import java.awt.Font; import java.util.Enumeration; import javax.swing.*; import javax.swing.border.TitledBorder; import javax.swing.plaf.FontUIResource; /** * 迷宮程序的主界面 * @author shuqin1984 */ public class MazeGUI extends JFrame { private JTextField inputRow; // 用戶輸入行數 private JTextField inputCol; // 用戶輸入列數 private JPanel mazePanel; // 顯示迷宮的面板 public MazeGUI() { super("程序演示:模擬走迷宮"); } private final Font font = new Font("Monospaced",Font.PLAIN, 14); public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { MazeGUI app = new MazeGUI(); app.launch(); } }); } private static void InitGlobalFont(Font font) { FontUIResource fontRes = new FontUIResource(font); for (Enumeration<Object> keys = UIManager.getDefaults().keys(); keys.hasMoreElements(); ) { Object key = keys.nextElement(); Object value = UIManager.get(key); if (value instanceof FontUIResource) { UIManager.put(key, fontRes); } } } /** * 啟動應用程序 */ public void launch() { JFrame f = new MazeGUI(); f.setBounds(100, 100, 800, 600); f.setVisible(true); f.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); InitGlobalFont(font); Container contentPane = f.getContentPane(); contentPane.setLayout(new BorderLayout()); JPanel inputPanel = createInputPanel(); contentPane.add(inputPanel, BorderLayout.NORTH); mazePanel = new MazePanel("顯示迷宮和迷宮的解", font); contentPane.add(mazePanel, BorderLayout.CENTER); f.setContentPane(contentPane); } /** * 創建並返回輸入面板 */ public JPanel createInputPanel() { JPanel inputPanel = new JPanel(new FlowLayout()); inputPanel.setBorder(new TitledBorder("用戶輸入信息提示")); JLabel labelInfo = new JLabel("請輸入迷宮大小:",null,SwingConstants.LEFT); JLabel labelRow = new JLabel("行"); JLabel labelCol = new JLabel("列"); JLabel labelSpace = new JLabel(" "); if (inputRow == null) inputRow = new JTextField(3); if (inputCol == null) inputCol = new JTextField(3); inputPanel.add(labelInfo); inputPanel.add(inputRow); inputPanel.add(labelRow); inputPanel.add(inputCol); inputPanel.add(labelCol); inputPanel.add(labelSpace); JButton button = new JButton("生成迷宮"); inputPanel.add(button); Model m = new Model(); m.setGUI(this); button.addActionListener(m); return inputPanel; } public JTextField getInputRow() { return inputRow; } public JTextField getInputCol() { return inputCol; } public JPanel getMazePanel() { return mazePanel; } }
8. 截圖,無圖無真相~~
【本文完】