1. Swing 與 AWT 的超精簡導論
如果把 Java 比作汽車,Swing 與 AWT — 就是它的「儀表板」與「方向盤」。AWT (Abstract Window Toolkit) — 是 Java 最早的視窗應用程式庫。它使用作業系統的原生控制項,因此在 Windows、Linux 和 Mac 上外觀各不相同。
Swing — 建立在 AWT 之上的較現代層,完全以 Java 編寫。Swing 更美觀、更彈性,支援許多額外功能,且在所有平台上的外觀幾乎一致(嗯,幾乎)。在兩者中,所有介面元件(按鈕、文字欄位、核取方塊等)都支援事件處理。
在這堂課我們將使用 Swing:它對新手比較簡單,且在教學範例中更常見。讓我們以最熱門的任務——對按鈕點擊做出反應——來理解事件處理的運作。
建立按鈕
在 Swing 中,按鈕可這樣建立:
JButton button = new JButton("點擊我!");
這裡的「點擊我!」是按鈕上的文字。
加入監聽器
若要對點擊做出回應,需要為事件「註冊」監聽器:
button.addActionListener(new MyActionListener());
但 MyActionListener 是誰?它是一個實作了介面 ActionListener 的類別:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class MyActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("按鈕被按下了!");
}
}
當使用者按下按鈕時,Swing 會呼叫所有透過 addActionListener 註冊之監聽器的 actionPerformed 方法。
完整範例:含按鈕的簡單視窗
讓我們把一切組合起來,建立一個可運作的應用程式:
import javax.swing.*;
import java.awt.event.*;
public class SimpleButtonApp {
public static void main(String[] args) {
JFrame frame = new JFrame("事件範例");
JButton button = new JButton("點擊我!");
// 為按鈕加入監聽器
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("按鈕被按下了!");
JOptionPane.showMessageDialog(frame, "太棒了! 按鈕已按下!");
}
});
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(button);
frame.setSize(300, 200);
frame.setVisible(true);
}
}
這裡發生了什麼:
- 建立視窗(JFrame)。
- 建立按鈕(JButton)。
- 加入監聽器(使用匿名類別,避免另外建立檔案)。
- 在 actionPerformed 內將訊息輸出到主控台,並顯示對話框(JOptionPane)。
- 把按鈕加到視窗上,並將視窗顯示給使用者。
試試看吧:複製這段程式碼、執行,然後按下按鈕!
2. 匿名類別與 Lambda 運算式
在現代的 Java 程式碼中,為了一個處理器而寫獨立類別——就像開坦克去買麵包。更方便的做法是使用 匿名類別 或 Lambda 運算式。
匿名類別
我們已在上面看過這種做法:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 處理程式碼
}
});
這裡的類別直接在原地宣告,沒有名稱。
Lambda 運算式 (Java 8+)
若監聽器介面是 函式式 的(也就是只有一個抽象方法),就可以使用 Lambda:
button.addActionListener(e -> {
System.out.println("按鈕透過 Lambda 被按下!");
});
若主體只有一行,還可以更精簡:
button.addActionListener(e -> System.out.println("Lambda: 按鈕被按下!"));
這不僅能縮短程式碼,還讓它更易讀。給好奇者的小知識:介面 ActionListener 是函式式的,因為其中只有一個方法 actionPerformed。
4. 其他事件型別
世界不只有限於按鈕!在 Swing 與 AWT 中,幾乎每個介面元件都支援各自的事件。以下是最常見的幾種:
滑鼠事件: MouseListener 與 MouseAdapter
若想對滑鼠點擊做出反應,請使用 MouseListener:
button.addMouseListener(new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("在按鈕上滑鼠點擊!");
}
// 其餘方法可以留空
@Override public void mousePressed(MouseEvent e) {}
@Override public void mouseReleased(MouseEvent e) {}
@Override public void mouseEntered(MouseEvent e) {}
@Override public void mouseExited(MouseEvent e) {}
});
為了一個處理器而寫五個空方法並不方便。這時可以用 MouseAdapter:
button.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("在按鈕上滑鼠點擊(透過適配器)!");
}
});
MouseAdapter 已實作介面中的所有方法,而你只需覆寫需要的。
鍵盤事件: KeyListener/KeyAdapter
若需要攔截按鍵按下:
button.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
System.out.println("按鍵被按下: " + e.getKeyChar());
}
});
文字變更事件: DocumentListener
對於文字欄位(JTextField、JTextArea)有專門的監聽器:
JTextField textField = new JTextField();
textField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
System.out.println("文字已變更: " + textField.getText());
}
@Override public void removeUpdate(DocumentEvent e) {}
@Override public void changedUpdate(DocumentEvent e) {}
});
順帶一提,多數事件都有對應的適配器,可避免撰寫空方法。
5. 實作: 含視窗與按鈕的小範例
來做一個最簡單的 GUI 應用:一個帶按鈕的視窗,按下按鈕會遞增計數並顯示訊息。
範例: 點擊計數器
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class ClickCounterApp {
public static void main(String[] args) {
JFrame frame = new JFrame("點擊計數器");
JButton button = new JButton("點擊我!");
JLabel label = new JLabel("次數: 0");
// 點擊計數器(必須是 final 或 effectively final)
final int[] count = {0};
button.addActionListener(e -> {
count[0]++;
label.setText("次數: " + count[0]);
});
frame.setLayout(new FlowLayout());
frame.add(button);
frame.add(label);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(250, 100);
frame.setVisible(true);
}
}
說明:
- 建立視窗、按鈕與用於顯示計數的標籤。
- 使用單元素陣列(final int[] count = {0};),來繞過 Lambda 對 final/有效 final 變數的限制。
- 每次按下按鈕時,遞增計數並更新標籤文字。
練習點子:試著每第五次按下時改變按鈕顏色!
6. 事件模型在內部如何運作
讓我們稍微看看底層。當你呼叫:
button.addActionListener(listener);
按鈕(物件 JButton)會把你的監聽器加入內部清單。 當使用者按下按鈕時,內部會發生以下流程:
- 按鈕建立事件物件(ActionEvent)。
- 按鈕遍歷監聽器清單,對每個監聽器呼叫方法 actionPerformed。
- 你的處理器執行所需動作(例如,更新標籤)。
重點要記得:每個事件都可以有任意數量的監聽器——你可以為同一個按鈕加入多個處理器,它們會依序被呼叫。
7. 視覺化示意: 事件處理
graph TD
A[使用者按下按鈕] --> B{JButton}
B --> C[建立 ActionEvent]
C --> D[對所有監聽器呼叫 actionPerformed]
D --> E[處理器執行動作]
7. 在 Swing 與 AWT 中處理事件的常見錯誤
錯誤 №1: 遺漏註冊監聽器。 按鈕沒有對點擊做出反應,因為你忘了呼叫 addActionListener。
錯誤 №2: 在處理器中執行繁重工作。 如果在事件處理器中執行長時間的迴圈或下載網路資源,介面會「卡住」。對於耗時工作請使用額外的執行緒或 SwingWorker。
錯誤 №3: 在 Lambda 中嘗試修改非 final/有效 final 的變數。 在 Lambda 內只能使用 final 或「有效 final」變數。對於計數器可以使用陣列或專用類別(想更進階可用 AtomicInteger)。
錯誤 №4: 忘記移除監聽器。 如果你移除了元件,卻沒有移除監聽器,可能導致記憶體洩漏。當元件不再需要時,請取消訂閱。
錯誤 №5: 未實作介面中的所有方法。 如果直接實作例如 MouseListener,別忘了所有方法!更好的方式是使用適配器(MouseAdapter、KeyAdapter)。
GO TO FULL VERSION