前言:從命令列邁向圖形化介面
早期程式透過 CLI (Command Line Interface) 互動,使用者需背誦指令語法。1984 年 Macintosh 推出 GUI (Graphical User Interface),以視覺化元件(視窗、按鈕、選單)大幅降低操作門檻,奠基現代軟體體驗。Java 提供 AWT (Abstract Window Toolkit) 與 Swing (JFC) 兩套 GUI 工具包,其中 Swing 以純 Java 實作、跨平台一致性高、元件豐富,成為桌面應用開發主流選擇。
9.1 GUI 架構核心:容器與元件
兩大基本建構單元
| 類別 | 代表類別 | 功能 | 常見子類別 |
| 容器 | Container | 承載其他元件、管理佈局 | JFrame、JDialog、JPanel、JApplet、JScrollPane |
| 元件 | Component | 互動單元、顯示資訊 | JButton、JLabel、JTextField、JCheckBox、JRadioButton、JComboBox、JList、JTable、JTree |
基本開發流程
- 建立頂層容器(
JFrame 或 JApplet)
- 設定佈局管理器(
LayoutManager)
- 建立元件實例
- 將元件加入容器(
container.add(component))
- 註冊事件監聽器
- 設定視窗屬性並顯示
匯入套件慣例
import javax.swing.*; // Swing 元件
import java.awt.*; // AWT 核心(Color、Font、LayoutManager、Event 等)
import java.awt.event.*; // 事件處理介面
9.2 按鈕 — 互動的起點
JButton 常用建構子與方法
| 建構子/方法 | 說明 |
JButton() | 無標籤按鈕 |
JButton(String text) | 文字標籤按鈕 |
JButton(Icon icon) | 圖示按鈕 |
JButton(String text, Icon icon) | 文字+圖示 |
setText(String) / getText() | 設定/取得按鈕文字 |
setForeground(Color) | 設定文字顏色 |
setEnabled(boolean) | 啟用/停用按鈕 |
addActionListener(ActionListener) | 註冊點擊事件監聽器 |
事件處理模式:匿名內部類別
JButton btn = new JButton("點我");
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Toolkit.getDefaultToolkit().beep(); // 發出蜂鳴聲
btn.setText("已點擊 " + (++count) + " 次");
}
});
完整範例:計數按鈕
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class CounterButton extends JFrame {
private int count = 0;
private JButton btn;
public CounterButton() {
setTitle("按鈕計數器");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new FlowLayout());
btn = new JButton("點我 (0)");
btn.addActionListener(e -> {
count++;
btn.setText("點我 (" + count + ")");
if (count % 10 == 0) Toolkit.getDefaultToolkit().beep();
});
add(btn);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(CounterButton::new);
}
}
9.3 標籤 — 資訊展示的基本單元
JLabel 進階功能
- 文字對齊:
setHorizontalAlignment(SwingConstants.LEFT/CENTER/RIGHT)、setVerticalAlignment(SwingConstants.TOP/CENTER/BOTTOM)
- 圖文混排:
new JLabel("文字", icon, SwingConstants.CENTER),配合 setHorizontalTextPosition()、setVerticalTextPosition() 控制相對位置
- 提示文字:
setToolTipText("滑鼠懸停顯示")
- 背景色:需先
setOpaque(true) 再 setBackground(Color)
範例:動態更新標籤
JLabel label = new JLabel("計數: 0", SwingConstants.CENTER);
label.setFont(new Font("微軟正黑體", Font.BOLD, 24));
label.setOpaque(true);
label.setBackground(Color.WHITE);
label.setForeground(Color.BLUE);
add(label);
JButton btn = new JButton("增加");
btn.addActionListener(e -> label.setText("計數: " + (++count)));
add(btn);
9.4 核取方塊與單選按鈕 — 選擇型輸入
JCheckBox:多重獨立選擇
JCheckBox cb1 = new JCheckBox("選項 A");
JCheckBox cb2 = new JCheckBox("選項 B", true); // 預設勾選
cb1.addItemListener(e -> {
if (e.getStateChange() == ItemEvent.SELECTED)
System.out.println("A 被勾選");
else
System.out.println("A 被取消");
});
JRadioButton + ButtonGroup:互斥單選
ButtonGroup group = new ButtonGroup();
JRadioButton rb1 = new JRadioButton("方案 1");
JRadioButton rb2 = new JRadioButton("方案 2", true); // 預設選中
JRadioButton rb3 = new JRadioButton("方案 3");
group.add(rb1); group.add(rb2); group.add(rb3);
// 監聽選擇變化
ItemListener listener = e -> {
if (e.getStateChange() == ItemEvent.SELECTED) {
JRadioButton src = (JRadioButton) e.getSource();
System.out.println("目前選擇: " + src.getText());
}
};
rb1.addItemListener(listener);
rb2.addItemListener(listener);
rb3.addItemListener(listener);
完整範例:顏色選擇器
public class ColorChooser extends JFrame {
private JButton previewBtn;
private ButtonGroup colorGroup;
public ColorChooser() {
setTitle("顏色選擇示範");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(new BorderLayout());
// 預覽按鈕
previewBtn = new JButton("預覽區域");
previewBtn.setFont(new Font("微軟正黑體", Font.BOLD, 18));
previewBtn.setBackground(Color.LIGHT_GRAY);
previewBtn.setOpaque(true);
previewBtn.setBorderPainted(false);
add(previewBtn, BorderLayout.CENTER);
// 選項面板
JPanel panel = new JPanel(new GridLayout(0, 1));
colorGroup = new ButtonGroup();
String[] colors = {"紅色", "綠色", "藍色", "黑色"};
Color[] colorVals = {Color.RED, Color.GREEN, Color.BLUE, Color.BLACK};
for (int i = 0; i < colors.length; i++) {
JRadioButton rb = new JRadioButton(colors[i], i == 3); // 預設黑色
final Color c = colorVals[i];
rb.addActionListener(e -> {
previewBtn.setBackground(c);
previewBtn.setForeground(getContrastColor(c)); // 文字對比色
});
colorGroup.add(rb);
panel.add(rb);
}
add(panel, BorderLayout.EAST);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
// 簡易對比色計算
private Color getContrastColor(Color bg) {
int luminance = (int)(0.299*bg.getRed() + 0.587*bg.getGreen() + 0.114*bg.getBlue());
return luminance > 186 ? Color.BLACK : Color.WHITE;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(ColorChooser::new);
}
}
9.5 列表方塊與下拉選單 — 多項目選擇
JList:可見多行、支援多選
String[] items = {"台北", "新北", "桃園", "台中", "台南", "高雄"};
JList list = new JList<>(items);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // 或 MULTIPLE_INTERVAL_SELECTION
list.setVisibleRowCount(4);
JScrollPane scrollPane = new JScrollPane(list);
add(scrollPane);
list.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
System.out.println("選擇: " + list.getSelectedValue());
}
});
JComboBox:下拉式、節省空間
String[] fonts = {"微軟正黑體", "新細明體", "標楷體", "Arial", "Times New Roman"};
JComboBox combo = new JComboBox<>(fonts);
combo.setEditable(true); // 允許使用者自行輸入
combo.addActionListener(e -> {
String selected = (String) combo.getSelectedItem();
System.out.println("選擇字型: " + selected);
});
add(combo);
動態增刪列表項目
DefaultListModel model = new DefaultListModel<>();
model.addElement("項目 1");
model.addElement("項目 2");
JList dynamicList = new JList<>(model);
JButton addBtn = new JButton("新增");
addBtn.addActionListener(e -> model.addElement("項目 " + (model.getSize() + 1)));
JButton delBtn = new JButton("刪除最後");
delBtn.addActionListener(e -> {
if (model.getSize() > 0) model.remove(model.getSize() - 1);
});
9.6 文字輸入元件 — 從單行到多行
三大文字元件比較
| 類別 | 用途 | 關鍵特性 |
JTextField | 單行文字輸入 | 設定欄寬 new JTextField(20)、setHorizontalAlignment()、addActionListener (Enter 鍵觸發) |
JPasswordField | 密碼輸入(遮罩顯示) | getPassword() 回傳 char[]、setEchoChar('*') 自訂遮罩字元 |
JTextArea | 多行文字編輯/顯示 | 設定行/列 new JTextArea(5, 30)、setLineWrap(true) 自動換行、setWrapStyleWord(true) 單字換行、搭配 JScrollPane |
完整範例:登入表單
public class LoginForm extends JFrame {
public LoginForm() {
setTitle("使用者登入");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
// 帳號
gbc.gridx = 0; gbc.gridy = 0; add(new JLabel("帳號:"), gbc);
gbc.gridx = 1;
JTextField userField = new JTextField(15);
add(userField, gbc);
// 密碼
gbc.gridx = 0; gbc.gridy = 1; add(new JLabel("密碼:"), gbc);
gbc.gridx = 1;
JPasswordField pwdField = new JPasswordField(15);
add(pwdField, gbc);
// 登入按鈕
gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2;
JButton loginBtn = new JButton("登入");
loginBtn.addActionListener(e -> {
String user = userField.getText();
String pwd = new String(pwdField.getPassword());
if ("admin".equals(user) && "1234".equals(pwd)) {
JOptionPane.showMessageDialog(this, "歡迎, " + user + "!");
} else {
JOptionPane.showMessageDialog(this, "帳號或密碼錯誤", "錯誤", JOptionPane.ERROR_MESSAGE);
}
});
add(loginBtn, gbc);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(LoginForm::new);
}
}
JTextArea 進階:自動換行與滾動
JTextArea textArea = new JTextArea("請輸入內容...", 10, 40);
textArea.setFont(new Font("Monospaced", Font.PLAIN, 14));
textArea.setLineWrap(true); // 自動換行
textArea.setWrapStyleWord(true); // 依單字換行,不斷詞
textArea.setTabSize(4); // Tab 寬度
JScrollPane scroll = new JScrollPane(textArea);
scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
add(scroll, BorderLayout.CENTER);
// 清除按鈕
JButton clearBtn = new JButton("清除");
clearBtn.addActionListener(e -> textArea.setText(""));
add(clearBtn, BorderLayout.SOUTH);
9.7 佈局管理器 — 掌控元件排版
Swing 採用委派模式:容器持有 LayoutManager,負責子元件大小與位置。
| 佈局管理器 | 特性 | 適用場景 |
FlowLayout | 左到右、上到下流式排列 | 簡單工具列、按鈕群組 |
BorderLayout | 五大區域:NORTH, SOUTH, EAST, WEST, CENTER | 主視窗整體架構 |
GridLayout | 規則網格,等寬等高 | 計算機鍵盤、表單欄位 |
GridBagLayout | 彈性網格,元件可跨行跨列 | 複雜表單、精細對齊 |
BoxLayout | 單行/單欄堆疊,支援彈性空間 | 工具列、側邊欄 |
CardLayout | 疊卡切換,同一時間顯示一張 | 精靈嚮導、分頁介面 |
GridBagLayout 實用範例
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4);
gbc.anchor = GridBagConstraints.WEST;
gbc.gridx = 0; gbc.gridy = 0; panel.add(new JLabel("姓名:"), gbc);
gbc.gridx = 1; gbc.weightx = 1.0; gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(new JTextField(15), gbc);
gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE;
panel.add(new JLabel("性別:"), gbc);
gbc.gridx = 1;
JComboBox gender = new JComboBox<>(new String[]{"男", "女", "其他"});
panel.add(gender, gbc);
gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2; gbc.anchor = GridBagConstraints.CENTER;
JButton submit = new JButton("送出");
panel.add(submit, gbc);
9.8 事件處理深度解析
事件模型三要素
- 事件源:產生事件的元件(按鈕、文字框、滑鼠等)
- 事件物件:封裝事件資訊(
ActionEvent、MouseEvent、KeyEvent 等)
- 事件監聽器:實作特定介面(
ActionListener、MouseListener 等)的處理類別
常見事件監聽器對照表
| 事件類型 | 監聽器介面 | 事件類別 | 觸發時機 |
| 動作 | ActionListener | ActionEvent | 按鈕點擊、菜單選擇、Enter 鍵 |
| 滑鼠 | MouseListener/MouseMotionListener | MouseEvent | 點擊、進入/離開、拖曳、移動 |
| 鍵盤 | KeyListener | KeyEvent | 按下、釋放、輸入字元 |
| 項目狀態 | ItemListener | ItemEvent | 核取方塊、單選按鈕、下拉選單選擇變化 |
| 列表選擇 | ListSelectionListener | ListSelectionEvent | JList、JTable 選取變化 |
| 視窗 | WindowListener | WindowEvent | 開啟、關閉、最小化、啟動/停用 |
| 焦點 | FocusListener | FocusEvent | 元件獲得/失去焦點 |
Lambda 表達式簡化監聽器 (Java 8+)
// 傳統匿名類別
btn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) { doSomething(); }
});
// Lambda 簡寫 (適用於函數式介面)
btn.addActionListener(e -> doSomething());
btn.addActionListener(e -> {
int count = getCount();
updateUI(count);
});
// 方法參考
btn.addActionListener(this::handleClick);
9.9 綜合實戰:通訊錄管理介面
public class AddressBookUI extends JFrame {
private DefaultListModel listModel = new DefaultListModel<>();
private JList contactList = new JList<>(listModel);
private JTextField nameField = new JTextField(15);
private JTextField phoneField = new JTextField(15);
private JTextField emailField = new JTextField(15);
public AddressBookUI() {
setTitle("通訊錄管理");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(new BorderLayout(10, 10));
// 左側:聯絡人列表
contactList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
contactList.setCellRenderer(new ContactCellRenderer());
JScrollPane listScroll = new JScrollPane(contactList);
listScroll.setPreferredSize(new Dimension(250, 400));
add(listScroll, BorderLayout.WEST);
// 右側:編輯表單
JPanel formPanel = new JPanel(new GridBagLayout());
formPanel.setBorder(BorderFactory.createTitledBorder("聯絡人資料"));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.anchor = GridBagConstraints.WEST;
gbc.fill = GridBagConstraints.HORIZONTAL;
addFormRow(formPanel, gbc, 0, "姓名:", nameField);
addFormRow(formPanel, gbc, 1, "電話:", phoneField);
addFormRow(formPanel, gbc, 2, "Email:", emailField);
// 按鈕列
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 2; gbc.anchor = GridBagConstraints.CENTER;
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 0));
JButton addBtn = new JButton("新增");
JButton updateBtn = new JButton("更新");
JButton delBtn = new JButton("刪除");
JButton clearBtn = new JButton("清空");
btnPanel.add(addBtn); btnPanel.add(updateBtn); btnPanel.add(delBtn); btnPanel.add(clearBtn);
formPanel.add(btnPanel, gbc);
add(formPanel, BorderLayout.CENTER);
// 事件綁定
addBtn.addActionListener(e -> addContact());
updateBtn.addActionListener(e -> updateContact());
delBtn.addActionListener(e -> deleteContact());
clearBtn.addActionListener(e -> clearForm());
contactList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) loadSelectedContact();
});
// 測試資料
listModel.addElement(new Contact("張三", "0912-345678", "zhang@example.com"));
listModel.addElement(new Contact("李四", "0923-456789", "li@example.com"));
pack();
setLocationRelativeTo(null);
setVisible(true);
}
private void addFormRow(JPanel panel, GridBagConstraints gbc, int row, String label, JComponent field) {
gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0; panel.add(new JLabel(label), gbc);
gbc.gridx = 1; gbc.weightx = 1.0; panel.add(field, gbc);
}
private void addContact() {
String name = nameField.getText().trim();
if (name.isEmpty()) return;
listModel.addElement(new Contact(name, phoneField.getText().trim(), emailField.getText().trim()));
clearForm();
}
private void updateContact() {
int idx = contactList.getSelectedIndex();
if (idx >= 0) {
listModel.set(idx, new Contact(
nameField.getText().trim(),
phoneField.getText().trim(),
emailField.getText().trim()
));
}
}
private void deleteContact() {
int idx = contactList.getSelectedIndex();
if (idx >= 0) listModel.remove(idx);
clearForm();
}
private void clearForm() {
nameField.setText(""); phoneField.setText(""); emailField.setText("");
contactList.clearSelection();
}
private void loadSelectedContact() {
Contact c = contactList.getSelectedValue();
if (c != null) {
nameField.setText(c.name);
phoneField.setText(c.phone);
emailField.setText(c.email);
}
}
// 內部資料類別
static class Contact {
String name, phone, email;
Contact(String n, String p, String e) { name = n; phone = p; email = e; }
public String toString() { return name + " (" + phone + ")"; }
}
// 自訂列表渲染器
static class ContactCellRenderer extends DefaultListCellRenderer {
public Component getListCellRendererComponent(JList> list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (value instanceof Contact) {
Contact c = (Contact) value;
setText(c.name);
setToolTipText(c.phone + " | " + c.email);
}
return this;
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(AddressBookUI::new);
}
}
最佳實踐總結
- EDT 事件派發執行緒:所有 GUI 操作必須在 EDT 執行,使用
SwingUtilities.invokeLater() 或 invokeAndWait()
- 長時間任務:使用
SwingWorker 背景執行,避免凍結介面
- 元件重用:封裝常用表單面板為自訂類別,提升開發效率
- 外觀一致性:統一字型、色系、間距,建立專案風格指南
- 無障礙支援:設定
setToolTipText、鍵盤導航順序 (setFocusTraversalPolicy)
本章完整涵蓋 Swing 核心元件、事件模型、佈局管理與實戰技巧。掌握這些基礎後,可進階學習 JavaFX 現代 UI 框架、客製化元件開發、資料綁定架構等主題,構建專業級桌面應用程式。