Java Swing คือ GUI toolkit ที่มาพร้อมกับ JDK แอป Swing ทุกตัวมีโครงเหมือนกันเสมอ คือสร้าง component อย่างปุ่มหรือช่องกรอกข้อความ จัดวางด้วย layout manager แล้วผูก listener ไว้ให้รันโค้ดเมื่อผู้ใช้คลิก พิมพ์ หรือขยับเมาส์ ถ้าเข้าใจสามส่วนนี้ชัด หน้าจอ desktop ส่วนใหญ่ก็สร้างได้ไม่ยาก
บทความนี้อธิบายความสัมพันธ์ระหว่าง AWT กับ Swing, component หลัก, BorderLayout, FlowLayout, GridLayout และ null layout, listener ประเภทหลัก ๆ และการวาดเองด้วย paintComponent ตัวอย่างทั้งหมดใช้ Java 17 ขึ้นไป
AWT vs Swing: สอง toolkit นี้เกี่ยวกันอย่างไร
แอป GUI ทำงานแบบ event-driven ไม่ได้รันจากบนลงล่างแล้วจบเหมือนโปรแกรม console แต่จะสร้างหน้าต่างขึ้นมาแล้วรอ event เช่น การคลิก การกดปุ่มคีย์บอร์ด หรือการย่อขยายหน้าต่าง Java มี toolkit ในตัวสองชุดสำหรับงานนี้
AWT (java.awt) | Swing (javax.swing) | |
|---|---|---|
| การวาด | Heavyweight: แต่ละ component ห่อ widget ของ OS | Lightweight: วาดด้วย Java เอง |
| Look and feel | ตาม OS เปลี่ยนไม่ได้ | เปลี่ยนได้ (Metal, Nimbus, system look and feel, theme ภายนอก) |
| ชื่อ component | Frame, Button, Label, TextField | JFrame, JButton, JLabel, JTextField |
| จำนวน component | พื้นฐาน | ครบกว่า: table, tree, tab, split pane, rich text |
| ยังใช้อยู่สำหรับ | event, layout manager, Graphics, สี, ฟอนต์ | ตัว component เอง |
Swing ไม่ได้มาแทน AWT แต่สร้างต่อยอดจากมัน layout manager, คลาส event และ Graphics API ยังอยู่ใน java.awt ไฟล์ Swing ทั่วไปจึง import ทั้งสอง package สิ่งที่ควรเลี่ยงคือการผสม component ของ AWT (Button) กับของ Swing (JButton) ในหน้าต่างเดียวกัน
หน้าต่าง Java Swing แรกและ Event Dispatch Thread
Swing เป็น single-threaded การสร้างและแก้ไข component ทุกครั้งต้องเกิดบน thread เดียว คือ Event Dispatch Thread (EDT) โค้ดใน listener รันบน EDT อยู่แล้ว แต่ main ไม่ได้รันบน EDT สิ่งแรกที่โปรแกรม Swing ต้องทำจึงเป็นการส่งงานสร้าง UI ไปให้ EDT ผ่าน SwingUtilities.invokeLater:
import javax.swing.*;
public class HelloSwing {
public static void main(String[] args) {
SwingUtilities.invokeLater(HelloSwing::createAndShowGui);
}
private static void createAndShowGui() {
var frame = new JFrame("Hello Swing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new JLabel("Running on the EDT", SwingConstants.CENTER));
frame.setSize(400, 200);
frame.setLocationRelativeTo(null); // center on screen
frame.setVisible(true);
}
}ถ้าไม่ตั้ง EXIT_ON_CLOSE การปิดหน้าต่างจะแค่ซ่อนมันไว้ ส่วน JVM ยังรันต่อ และควรเรียก setVisible(true) เป็นลำดับสุดท้าย หลังจาก add component ครบแล้ว
Component หลักของ Swing
ชุดนี้ครอบคลุมฟอร์มและเครื่องมือส่วนใหญ่:
| Component | ใช้ทำอะไร |
|---|---|
JFrame | หน้าต่างระดับบนสุด มี title bar และขอบ |
JPanel | container สำหรับจัดกลุ่ม component แต่ละตัวมี layout ของตัวเองได้ |
JLabel | ข้อความหรือไอคอนแบบอ่านอย่างเดียว |
JButton | ปุ่มกด ยิง ActionEvent |
JTextField / JPasswordField | ช่องกรอกบรรทัดเดียว |
JTextArea | ข้อความหลายบรรทัด ควรห่อด้วย JScrollPane |
JCheckBox, JRadioButton, JComboBox<T> | ตัวเลือก |
JOptionPane | dialog สำเร็จรูปสำหรับแจ้งเตือน ยืนยัน และรับข้อมูล |
ทุก component ยังเปลี่ยน cursor ของเมาส์ได้ด้วย setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)) ตัวเลือกอื่นเช่น WAIT_CURSOR, TEXT_CURSOR และ MOVE_CURSOR
Layout manager ใน Java Swing
Layout manager เป็นตัวกำหนดขนาดและตำแหน่งของทุก component ใน container และจะคำนวณใหม่เมื่อหน้าต่างถูกย่อขยายหรือฟอนต์เปลี่ยน นี่คือเหตุผลที่ควรใช้ layout manager แทนการกำหนดตำแหน่งเป็น pixel ตายตัว
BorderLayout
BorderLayout แบ่ง container เป็นห้าส่วน คือ NORTH, SOUTH, EAST, WEST และ CENTER ส่วนขอบได้ขนาดตาม preferred size ส่วน center ได้พื้นที่ที่เหลือทั้งหมด มันเป็น layout เริ่มต้นของ content pane ใน JFrame นั่นคือเหตุผลที่ frame.add(label) ด้านบนเต็มทั้งหน้าต่าง เพราะถ้าไม่ระบุตำแหน่งจะถือว่าเป็น CENTER แต่ละส่วนใส่ได้หนึ่ง component ถ้าต้องการหลายตัวให้รวมไว้ใน JPanel ก่อน
FlowLayout
FlowLayout วาง component จากซ้ายไปขวาตาม preferred size และขึ้นแถวใหม่เมื่อความกว้างไม่พอ คล้ายการตัดคำในย่อหน้า เป็น layout เริ่มต้นของ JPanel และเหมาะกับแถบปุ่ม: new FlowLayout(FlowLayout.RIGHT) จะจัดปุ่มชิดขวา
GridLayout
GridLayout(rows, cols, hgap, vgap) แบ่ง container เป็นช่องขนาดเท่ากันแล้วเติมทีละแถวตามลำดับ ทุก component ถูกยืดให้เต็มช่อง จึงเหมาะกับแป้นตัวเลขแบบเครื่องคิดเลข (new GridLayout(4, 3, 4, 4)) และฟอร์ม label คู่กับช่องกรอกที่ทุกช่องขนาดเท่ากันได้
Null layout (กำหนดตำแหน่งเอง)
เมื่อ setLayout(null) จะไม่มี layout manager เลย คุณต้องวางแต่ละ component เองด้วย setBounds(x, y, width, height) ซึ่งรวม setLocation กับ setSize ไว้ในคำสั่งเดียว:
var panel = new JPanel(null);
var title = new JLabel("Sign up");
title.setBounds(50, 30, 200, 25);
var email = new JTextField();
email.setBounds(50, 70, 250, 30);
panel.add(title);
panel.add(email);ดูง่าย แต่ไม่มีอะไรปรับตามเมื่อย่อขยายหน้าต่าง ข้อความถูกตัดเมื่อฟอนต์หรือ display scaling เปลี่ยน และทุกครั้งที่แก้หน้าจอต้องคำนวณพิกัดใหม่เอง ควรเก็บไว้ใช้กับกรณีพิเศษ เช่น canvas แบบลากวาง สำหรับฟอร์มที่ซับซ้อนให้ใช้ GridBagLayout, BoxLayout หรือ GroupLayout แทน
ซ้อน panel เพื่อสร้างหน้าจอจริง
หน้าจอจริงมักซ้อน panel หลายชั้น แต่ละชั้นใช้ layout ที่เหมาะกับส่วนของตัวเอง ตัวอย่างนี้คือฟอร์มสมัครสมาชิกที่ใช้สาม layout ร่วมกัน:
import java.awt.*;
import javax.swing.*;
public class SignUpForm {
public static void main(String[] args) {
SwingUtilities.invokeLater(SignUpForm::createAndShow);
}
private static void createAndShow() {
var frame = new JFrame("Sign up");
var emailField = new JTextField(20);
var passwordField = new JPasswordField(20);
var fields = new JPanel(new GridLayout(2, 2, 8, 8));
fields.add(new JLabel("Email"));
fields.add(emailField);
fields.add(new JLabel("Password"));
fields.add(passwordField);
var submit = new JButton("Create account");
var buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttons.add(submit);
var root = new JPanel(new BorderLayout(0, 12));
root.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
root.add(new JLabel("Create your account"), BorderLayout.NORTH);
root.add(fields, BorderLayout.CENTER);
root.add(buttons, BorderLayout.SOUTH);
submit.addActionListener(e -> {
String email = emailField.getText().trim();
char[] password = passwordField.getPassword();
if (email.isEmpty() || password.length == 0) {
JOptionPane.showMessageDialog(frame, "Email and password are required.");
return;
}
JOptionPane.showMessageDialog(frame, "Welcome, " + email);
java.util.Arrays.fill(password, '\0'); // clear the password from memory
});
frame.getRootPane().setDefaultButton(submit); // Enter submits the form
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(root);
frame.pack(); // size the window to fit its contents
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}จัดการ event ด้วย listener ของ Swing
โมเดล event ของ Swing มีสามส่วน คือ source (เช่นปุ่ม), event object (เช่น ActionEvent) และ listener ที่ลงทะเบียนผ่านเมธอด addXxxListener component หนึ่งตัวมีได้หลาย listener และคลาสเดียวก็ implement หลาย listener interface ได้
ActionListener
ActionListener รับ action หลักของ component เช่น การคลิกปุ่ม การกด Enter ใน JTextField หรือการเลือก menu item และ combo box มันมีเมธอดเดียว จึงเขียนเป็น lambda ได้เลย เหมือนในฟอร์มสมัครสมาชิกด้านบน
MouseListener และ MouseMotionListener
Event ของเมาส์แยกอยู่สอง interface MouseListener ดูแล mouseClicked, mousePressed, mouseReleased, mouseEntered และ mouseExited ส่วน MouseMotionListener ดูแล mouseMoved และ mouseDragged ทั้งคู่ใช้ lambda ไม่ได้เพราะมีหลายเมธอด ให้ extend MouseAdapter ซึ่ง implement ทั้งสอง interface ไว้เป็นเมธอดว่าง แล้ว override เฉพาะที่ต้องใช้ แบบเดียวกับ drawing panel ด้านล่าง
บั๊กที่เจอบ่อยคือลงทะเบียน adapter ด้วย addMouseListener อย่างเดียว แล้วสงสัยว่าทำไม mouseDragged ไม่เคยทำงาน ต้องลงทะเบียน object เดียวกันกับ addMouseMotionListener ด้วย
KeyListener และ key binding
KeyListener มี keyPressed, keyReleased และ keyTyped โดยมี KeyAdapter เป็นตัวช่วย มันรับ event ได้เฉพาะตอนที่ component นั้นถือ keyboard focus อยู่ ถ้าใส่ไว้ที่ panel จึงมักดูเหมือนไม่ทำงาน สำหรับ shortcut ให้ใช้ key binding (InputMap คู่กับ ActionMap) ซึ่งเชื่อถือได้กว่า เพราะตั้งให้ทำงานได้ตลอดเวลาที่หน้าต่าง focus อยู่ ตัวอย่าง drawing panel ด้านล่างใช้ key binding กับปุ่ม Escape
ใช้ KeyListener เมื่อ component ที่ถือ focus ต้องการ key event ดิบ ๆ เช่น panel ของเกมที่ต้องรู้ว่าปุ่มลูกศรไหนถูกกดค้างอยู่ อย่าลืมเรียก setFocusable(true) และ requestFocusInWindow() กับ panel นั้น
WindowListener
WindowListener ตอบสนองต่อ lifecycle ของหน้าต่าง กรณีใช้งานที่พบบ่อยคือถามยืนยันก่อนปิด:
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
int choice = JOptionPane.showConfirmDialog(
frame, "Discard unsaved changes?", "Quit", JOptionPane.YES_NO_OPTION);
if (choice == JOptionPane.YES_OPTION) {
frame.dispose();
}
}
});วาดเองด้วย paintComponent
ทุก component ใน Swing วาดตัวเองผ่าน object Graphics ถ้าอยากวาดรูปทรงเอง ให้ extend JPanel แล้ว override paintComponent เมธอดที่ต้องรู้จักมีสามตัว:
paint()คือเมธอดวาดระดับบนสุดของ AWT ใน Swing มันวาด border และ component ลูกด้วย จึงไม่ควร overridepaintComponent(Graphics g)คือที่สำหรับโค้ดวาดของคุณ ต้องเรียกsuper.paintComponent(g)ก่อนเพื่อล้างพื้นหลังrepaint()ขอให้ Swing วาด component ใหม่ในจังหวะถัดไป ห้ามเรียกpaintComponentตรง ๆ
Panel นี้วาดจุดทุกตำแหน่งที่คลิกหรือลาก และล้างทั้งหมดเมื่อกด Escape:
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
class DrawingPanel extends JPanel {
private final List<Point> points = new ArrayList<>();
DrawingPanel() {
setBackground(Color.WHITE);
var mouse = new MouseAdapter() {
@Override public void mousePressed(MouseEvent e) { addPoint(e.getPoint()); }
@Override public void mouseDragged(MouseEvent e) { addPoint(e.getPoint()); }
};
addMouseListener(mouse);
addMouseMotionListener(mouse);
getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ESCAPE"), "clear");
getActionMap().put("clear", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
points.clear();
repaint();
}
});
}
private void addPoint(Point p) {
points.add(p);
repaint(); // schedule a redraw; Swing calls paintComponent for us
}
@Override
public Dimension getPreferredSize() {
return new Dimension(500, 400);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
var g2 = (Graphics2D) g.create();
try {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(0x2563EB));
for (var p : points) {
g2.fillOval(p.x - 4, p.y - 4, 8, 8);
}
} finally {
g2.dispose();
}
}
}ทำให้ paintComponent เร็วและไม่มี side effect เพราะ Swing อาจเรียกมันหลายครั้ง เก็บ state ไว้ใน field เปลี่ยน state ใน listener แล้วค่อยเรียก repaint()
ทำให้ UI ไม่ค้างด้วย SwingWorker
Listener รันบน EDT ถ้ามีงานช้าอยู่ข้างใน หน้าต่างทั้งหมดจะค้าง ไม่ repaint และคลิกอะไรไม่ได้ ให้ย้ายงานอย่าง file I/O, network call หรือ database query ไปไว้ใน SwingWorker แล้วอัปเดต component เฉพาะใน done() หรือ process() ซึ่งกลับมารันบน EDT:
loadButton.addActionListener(e -> {
loadButton.setEnabled(false);
new SwingWorker<String, Void>() {
@Override
protected String doInBackground() throws Exception {
return reportService.fetchReport(); // runs on a background thread
}
@Override
protected void done() { // runs on the EDT
try {
output.setText(get());
} catch (Exception ex) {
output.setText("Failed to load: " + ex.getMessage());
} finally {
loadButton.setEnabled(true);
}
}
}.execute();
});ถ้าต้องการเข้าใจเรื่อง thread, synchronization และ executor นอกบริบท GUI อ่านต่อได้ที่ Java thread และ multithreading
คำถามที่พบบ่อย
Java Swing ยังมีคนใช้อยู่ไหม?
ยังใช้อยู่ Swing มาพร้อม JDK ใน module java.desktop และยังได้รับการดูแลต่อเนื่อง IDE หลายตัว เครื่องมือภายในองค์กร และแอป desktop ระดับ enterprise ที่ใช้งานมานานยังรันบน Swing
Swing ต่างจาก JavaFX อย่างไร?
JavaFX เป็น toolkit ที่ใหม่กว่า รองรับการตกแต่งด้วย CSS, layout แบบ FXML และ scene graph และแยกแจกจ่ายออกจาก JDK ตั้งแต่ Java 11 ส่วน Swing ไม่ต้องเพิ่ม dependency ใด ๆ ทั้งสองฝังเข้าหากันได้ผ่าน JFXPanel และ SwingNode
ทำไม KeyListener ไม่ทำงาน?
เพราะมันรับ event เฉพาะตอนที่ component ถือ keyboard focus และ panel ไม่ได้รับ focus เอง ให้เรียก setFocusable(true) กับ requestFocusInWindow() หรือเปลี่ยนไปใช้ key binding ผ่าน InputMap และ ActionMap
Layout เริ่มต้นของ JFrame และ JPanel คืออะไร?
Content pane ของ JFrame ใช้ BorderLayout ส่วน JPanel ที่สร้างใหม่ใช้ FlowLayout เปลี่ยนได้โดยส่ง layout เข้า constructor ของ JPanel หรือเรียก setLayout
ทำไม component ที่ add ไปไม่แสดง?
ส่วนใหญ่เพราะ add หลังจาก setVisible(true) แล้วไม่ได้เรียก revalidate() และ repaint() หรือเพราะ add สอง component ลงตำแหน่งเดียวกันของ BorderLayout ซึ่งจะแสดงแค่ตัวสุดท้าย
Checklist สำหรับ Swing
- สร้างและอัปเดต UI ทั้งหมดบน EDT เริ่มจาก
SwingUtilities.invokeLater - ใช้ component ตระกูล
Jให้สม่ำเสมอ ไม่ผสม component ของ AWT - ซ้อน
JPanelโดยเลือก layout ให้เหมาะกับแต่ละส่วนของหน้าจอ เลี่ยง null layout สำหรับฟอร์ม - ใช้ lambda กับ
ActionListenerและใช้ adapter กับ listener ของเมาส์ คีย์บอร์ด และหน้าต่าง - ใช้ key binding แทน
KeyListenerสำหรับ shortcut - วาดใน
paintComponentเปลี่ยน state ใน listener แล้วเรียกrepaint() - ย้ายงานที่ช้าไปไว้ใน
SwingWorker
โค้ด Swing คือ Java เชิงวัตถุธรรมดา ถ้าเข้าใจ class, object และ constructor และ inheritance กับเสาหลักอื่นของ OOP ก็จะอ่าน component และ adapter ได้ง่ายขึ้นมาก ถ้าทีมของคุณต้องการเครื่องมือ desktop หรือระบบภายในองค์กร Vectorkub รับพัฒนา custom software
