Swing 是 Java 經典桌面 GUI 框架,雖然現代開發多轉向 JavaFX/Compose/Web,但 Swing 的事件分派執行緒 (EDT)、模型-視圖-控制器 (MVC)、佈局管理器、自定繪圖 等核心概念,是理解所有 GUI 框架的基石。本系列以「連連看」遊戲為例,分階段講解 Swing 遊戲開發完整流程。
| 類別 | 用途 | 特徵 | |
|---|---|---|---|
JFrame | 主應用視窗 | 可獨立存在、有標題列/邊框/最小化/最大化/關閉、支援選單列 | |
JDialog | 對話框 | 依附於父視窗、可模態(阻塞父視窗)/非模態 | |
JApplet | 瀏覽器嵌入小程式 | 已淘汰,瀏覽器不再支援 Java Plugin |
| 佈局 | 特性 | 適用場景 | 關鍵約束 |
|---|---|---|---|
BorderLayout | 五大區域 (NORTH, SOUTH, EAST, WEST, CENTER) | 主視窗整體結構、對話框 | CENTER 自動填滿剩餘空間,每區域最多一個元件 |
GridLayout | 規則網格 (rows × cols) | 計算機鍵盤、棋盤、表單欄位 | 所有格子等大、按序填入 |
FlowLayout | 左到右、換行流動 | 工具列、按鈕群、簡單面板 | 元件維持偏好大小、靠左/置中/靠右對齊 |
BoxLayout | 單向堆疊 (X_AXIS/Y_AXIS) | 表單垂直堆疊、工具列水平排列 | 可配合 glue/strut/rigid area 控制間距 |
GridBagLayout | 最強彈性網格 (元件可跨行列) | 複雜表單、精確對齊介面 | GridBagConstraints 參數多、學習曲線陡 |
CardLayout | 卡片堆疊、一次顯示一張 | 精靈對話框、分頁切換、遊戲狀態切換 | 需手動切換顯示哪張卡片 |
GroupLayout | IDE 視覺化設計器首選 (Matisse) | 手寫極難維護 | 不建議手寫 |
MigLayout (第三方) | 字串約束、極度簡潔強大 | 需引入庫 | 現代專案強烈推薦 |
public class GamePanel extends JPanel {
public GamePanel() {
// 1. 使用 BorderLayout 分割三大區域
setLayout(new BorderLayout());
setBackground(Color.DARK_GRAY);
// 2. 頂部工具列 (NORTH)
JPanel toolBar = createToolBar();
toolBar.setPreferredSize(new Dimension(800, 60));
add(toolBar, BorderLayout.NORTH);
// 3. 右側互動區 (EAST)
JPanel sidePanel = createSidePanel();
sidePanel.setPreferredSize(new Dimension(200, 540));
add(sidePanel, BorderLayout.EAST);
// 4. 中央遊戲區 (CENTER) - 自動填滿剩餘空間
BoardPanel boardPanel = new BoardPanel();
add(boardPanel, BorderLayout.CENTER);
}
private JPanel createToolBar() {
JPanel bar = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10));
bar.setBackground(new Color(45, 45, 45));
bar.setBorder(BorderFactory.createMatteBorder(0, 0, 2, 0, Color.GRAY));
JButton newGameBtn = new JButton("新遊戲");
JButton hintBtn = new JButton("提示");
JLabel scoreLabel = new JLabel("分數: 0");
scoreLabel.setForeground(Color.WHITE);
bar.add(newGameBtn);
bar.add(hintBtn);
bar.add(Box.createHorizontalGlue()); // 推到右邊
bar.add(scoreLabel);
return bar;
}
private JPanel createSidePanel() {
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.setBackground(new Color(35, 35, 35));
panel.setBorder(BorderFactory.createEmptyBorder(20, 10, 20, 10));
// 玩家資訊卡片
JPanel infoCard = createInfoCard();
panel.add(infoCard);
panel.add(Box.createVerticalStrut(20));
// 道具欄
JPanel itemPanel = createItemPanel();
panel.add(itemPanel);
return panel;
}
}
// 線條邊框
Border line = BorderFactory.createLineBorder(Color.GRAY, 2);
// 立體邊框 (凸起/凹陷)
Border raised = BorderFactory.createRaisedBevelBorder();
Border lowered = BorderFactory.createLoweredBevelBorder();
Border bevel = BorderFactory.createBevelBorder(BevelBorder.RAISED,
new Color(100,100,100), new Color(50,50,50)); // 高光/陰影色
// 空白邊框 (內距)
Border padding = BorderFactory.createEmptyBorder(10, 15, 10, 15); // 上右下左
// 標題邊框 (含標題文字)
Border titled = BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(Color.BLUE),
"玩家資訊",
TitledBorder.LEFT, TitledBorder.TOP);
// 組合邊框 (外層 + 內層)
Border compound = BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(Color.GRAY),
BorderFactory.createEmptyBorder(10, 10, 10, 10)
);
// 套用到元件
panel.setBorder(compound);
ActionEvent, MouseEvent, KeyEvent)ActionListener, MouseListener)source.addActionListener(listener)
public class GamePanel extends JPanel implements ActionListener {
private JButton newGameBtn;
private JButton hintBtn;
public GamePanel() {
// ... 佈局代碼 ...
newGameBtn = new JButton("新遊戲");
newGameBtn.addActionListener(this); // 註冊監聽器
hintBtn = new JButton("提示");
hintBtn.addActionListener(this);
}
@Override
public void actionPerformed(ActionEvent e) {
Object source = e.getSource(); // 取得事件來源
if (source == newGameBtn) {
startNewGame();
} else if (source == hintBtn) {
showHint();
}
}
private void startNewGame() {
// 重置遊戲狀態、重新洗牌、重繪棋盤
boardPanel.resetGame();
score = 0;
updateScoreLabel();
}
private void showHint() {
// 尋找可消除配對、高亮顯示
Pair hint = boardPanel.findHint();
if (hint != null) {
boardPanel.highlightPair(hint);
// 2 秒後取消高亮
Timer timer = new Timer(2000, evt -> boardPanel.clearHighlight());
timer.setRepeats(false);
timer.start();
}
}
}
newGameBtn.addActionListener(e -> startNewGame());
hintBtn.addActionListener(e -> showHint());
// 滑鼠事件範例
boardPanel.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
boardPanel.handleTileClick(e.getX(), e.getY());
}
}
});
// 資訊對話框
JOptionPane.showMessageDialog(parent, "遊戲說明內容...", "關於", JOptionPane.INFORMATION_MESSAGE);
// 確認對話框
int result = JOptionPane.showConfirmDialog(parent, "確定要離開嗎?", "確認", JOptionPane.YES_NO_OPTION);
if (result == JOptionPane.YES_OPTION) System.exit(0);
// 輸入對話框
String name = JOptionPane.showInputDialog(parent, "請輸入玩家名稱:", "新玩家", JOptionPane.QUESTION_MESSAGE);
// 選項對話框
Object[] options = {"簡單", "普通", "困難"};
int choice = JOptionPane.showOptionDialog(parent, "選擇難度", "難度設定",
JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[1]);
Swing 單執行緒規則:所有 GUI 元件的建立、修改、查詢必須在事件分派執行緒 (EDT) 上進行。違反會導致畫面凍結、資料不一致、難以重現的 Bug。
// 1. 程式進入點:在 EDT 上啟動
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new KyodaiGame().setVisible(true));
}
// 2. 背景執行緒完成後更新 UI:SwingUtilities.invokeLater()
new Thread(() -> {
// 耗時操作:載入資源、網路請求、複雜計算
ImageIcon background = loadBackgroundImage();
// 回到 EDT 更新 UI
SwingUtilities.invokeLater(() -> {
backgroundLabel.setIcon(background);
loadingLabel.setVisible(false);
});
}).start();
// 3. 定時任務:javax.swing.Timer (自動在 EDT 執行)
Timer gameTimer = new Timer(1000, e -> {
timeRemaining--;
timeLabel.setText("時間: " + timeRemaining);
if (timeRemaining <= 0) gameOver();
});
gameTimer.start();
// ❌ 錯誤:在背景執行緒直接修改 UI
new Thread(() -> {
scoreLabel.setText("分數: " + score); // 危險!
}).start();
// ❌ 錯誤:在 EDT 執行耗時操作導致介面凍結
public void actionPerformed(ActionEvent e) {
Thread.sleep(3000); // 整個 UI 凍結 3 秒
doSomething();
}
public class BoardPanel extends JPanel {
private Tile[][] board; // 遊戲邏輯資料模型
private int tileSize = 48;
public BoardPanel() {
setPreferredSize(new Dimension(12 * tileSize, 10 * tileSize));
setBackground(Color.BLACK);
setFocusable(true); // 可接收鍵盤焦點
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g); // 必須先呼叫父類 (清除背景)
Graphics2D g2d = (Graphics2D) g;
// 啟用抗鋸齒
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// 繪製棋盤格
drawBoard(g2d);
// 繪製方塊
drawTiles(g2d);
// 繪製選中/提示高亮
drawHighlights(g2d);
}
private void drawBoard(Graphics2D g) {
g.setColor(new Color(30, 30, 30));
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
int x = c * tileSize;
int y = r * tileSize;
g.fillRect(x, y, tileSize - 1, tileSize - 1); // -1 留出格線
}
}
}
private void drawTiles(Graphics2D g) {
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
Tile tile = board[r][c];
if (tile != null && tile.getImage() != null) {
int x = c * tileSize;
int y = r * tileSize;
g.drawImage(tile.getImage(), x, y, tileSize, tileSize, this);
}
}
}
}
}
public class BoardPanel extends JPanel {
public BoardPanel() {
// Swing 預設開啟雙重緩衝,但可明確確認
setDoubleBuffered(true);
}
// 若需手動控制 (極少見):
// BufferStrategy strategy = getBufferStrategy();
// if (strategy == null) { createBufferStrategy(2); return; }
// Graphics g = strategy.getDrawGraphics();
// try { paintComponent(g); } finally { g.dispose(); }
// strategy.show();
}
public class ResourceManager {
private static final Map imageCache = new ConcurrentHashMap<>();
private static final Map audioCache = new ConcurrentHashMap<>();
public static Image getImage(String path) {
return imageCache.computeIfAbsent(path, p -> {
try {
// 從 classpath 載入 (JAR 內也適用)
URL url = ResourceManager.class.getResource("/resources/" + p);
if (url == null) throw new IllegalArgumentException("Resource not found: " + p);
return ImageIO.read(url);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
public static AudioClip getSound(String path) {
return audioCache.computeIfAbsent(path, p -> {
URL url = ResourceManager.class.getResource("/sounds/" + p);
return Applet.newAudioClip(url); // 簡單音效足夠
});
}
public static void playSound(String path) {
AudioClip clip = getSound(path);
if (clip != null) clip.play();
}
}
SwingUtilities.invokeLater() 啟動、javax.swing.Timer 定時、背景執行緒回 UI 用 invokeLater()paintComponent(Graphics),必須呼叫 super.paintComponent(),善用 Graphics2D 進階渲染TitledBorder 分組:「玩家資訊」、「遊戲道具」、「系統設定」MigLayout 重寫主視窗佈局,體驗字串約束的簡潔性第二部分將深入:遊戲邏輯模型設計 (MVC 分離)、棋盤演算法 (連連看尋路)、動畫系統、狀態機管理、持久化存檔、打包部署。敬請期待!