ต่อจาก Lab Week 10 (โปรเจกต์ MoogataLab เดิม) · Java Swing + Login + สิทธิ์ผู้จัดการ · ธีมร้านหมูกะทะ BONUS SUKI
somchai / 1234 สำเร็จ
แต่ต้องไปพิมพ์ในโปรแกรม main บนคอนโซลเหมือนนักพัฒนา ไม่ใช่พนักงานหน้าร้าน
ทั้งที่เพิ่มพนักงานใหม่ก็ต้องรู้ชื่อคลาส ชื่อเมธอด แล้วแก้โค้ดเอง — แบบนั้นพนักงานร้านหมูกะทะใช้ไม่ได้แน่จบ codelab นี้คุณจะมี 2 โปรเจกต์ ที่ใช้หลังบ้าน (ฐานข้อมูล/DAO/Service) เหมือนกันทุกบรรทัด ต่างกันแค่หน้าบ้าน:
| โปรเจกต์ 1 — MoogataLab (ส่วนที่ 1) | โปรเจกต์ 2 — MoogataLabDialog (ส่วนที่ 2) | |
|---|---|---|
| โครงหน้าจัดการ | หน้าต่างเดียวจบ: ฟอร์ม input อยู่ด้านบน ตารางอยู่ด้านล่าง เพิ่ม/แก้บนฟอร์มเดียวกัน สลับโหมดด้วยการคลิกแถว/กดล้างฟอร์ม | หน้าหลักมีแต่ตารางกับปุ่ม — เวลาเพิ่ม/แก้จะเด้งฟอร์มเป็น dialog ขึ้นมา บันทึกหรือยกเลิกแล้วค่อยกลับมา |
| เรียนรู้เด่น | การคุมสถานะสองโหมดบนฟอร์มเดียว (editingId) + ListSelectionListener | การแยกฟอร์มเป็น modal dialog + เส้นทางยกเลิก |
ทั้งสองโปรเจกต์มีเส้นทางเดียวกัน: เปิดโปรแกรม → ล็อกอิน → เฉพาะผู้จัดการ (role = 1) เข้าหน้าจัดการพนักงานได้ → เพิ่ม/แก้/ลบ/รีเฟรช
| หน้า | เรื่อง | สิ่งที่จะได้ตอนจบหน้า |
|---|---|---|
| 2 | ทฤษฎีที่ต้องเข้าใจก่อนลงมือ | เข้าใจ event-driven, EDT, modal, model-view |
| 3 | ชุด Component Swing ที่ใช้ | รู้จักหน้าที่/จุดพลาดของ control ทุกตัวที่จะลาก |
| 4–11 | ส่วนที่ 1 (Step 1–8): ต่อยอดใน MoogataLab เดิม สร้างแบบฟอร์มรวม | แอปครบวงจร ผ่านทดสอบ 8 สถานการณ์ |
| 12–15 | ส่วนที่ 2 (Step 9–12): คัดลอกโปรเจกต์ รื้อเป็นแบบ dialog | แอปแบบ dialog ผ่านทดสอบ 10 สถานการณ์ |
| 16–17 | Troubleshooting · เช็คลิสต์ส่งงาน · สรุปเรื่องราว | งานพร้อมส่ง |
TestEmployeeService ได้ Welcome user : somchai (แคชเชียร์)อ่านรอบแรกแบบคร่าว ๆ ก็ได้ — เมื่อถึง Step ไหนที่ใช้เรื่องนั้น จะมีลิงก์ย้อนกลับมาที่นี่ทุกครั้ง ทฤษฎีจะเกาะเมื่อเห็นของจริง
javax.swing.*
ทุกปุ่ม ทุกช่องกรอก ทุกตารางใน lab นี้วาดด้วย Java เอง (lightweight) จึงหน้าตาเหมือนกันทุก OS และ
pom.xml ไม่ต้องเพิ่ม dependency แม้แต่บรรทัดเดียว — ต่างจาก sqlite-jdbc ที่ต้องประกาศเอง
ui (Step 3 เป็นต้นไป)JFrame กำหนดชื่อหัวจอ (title), พฤติกรรมปุ่ม X (defaultCloseOperation), ขนาด
— ส่วนหน้าออกแบบของ NetBeans (Free Design) ที่เราใช้ลากวาง ใช้ layout ชื่อ GroupLayout ที่ NetBeans สร้างโค้ดให้เอง
พฤติกรรมเหมือน "วางพิกัดตรง" คือตำแหน่ง/ขนาดติดหนึบตามที่ลาก ผู้ใช้ย่อหน้าต่างแล้วของไม่ไหลตาม
(ระบบจริงจะใช้ Layout Manager อื่น เช่น BorderLayout/GridBagLayout — หัวข้อของคอร์สถัดไป)
AbsoluteLayout
เข้า pom.xml โดยไม่จำเป็น (ขัดกับที่ lab บอกว่า pom ไม่ต้องเพิ่ม dependency ใด ๆ)
ให้ใช้ Free Design ที่มาพร้อมฟอร์มตั้งแต่ New → JFrame Form เท่านั้นbtnLogin (Step 4), btnSave/btnNew/btnDelete/btnRefresh/btnAdd/btnEdit/btnOK/btnCancel, และ ListSelectionListener ของตาราง (Step 7)java.awt.EventQueue.invokeLater(...) (วิธีเดียวกัน)
ซึ่งจะเห็นใน main() ของ LoginFrame — main เดียวที่เหลือตอนจบ lab
และกติกาสำคัญคือ อย่าทำงานหนักบน EDT เพราะเธรดตัวเดียวติดงานหนักเมื่อไหร่ หน้าจอค้างทั้งบานทันที
SwingWorker)
ซึ่งเป็นหัวข้อต่อยอด — จดไว้ในแบบฝึกหัดได้EmployeeDialog สร้างด้วย super(owner, title, true) — ตัว true คือ modal
และ JOptionPane.showConfirmDialog (ยืนยันลบ) ก็เป็น modal โดยธรรมชาติ
JTable เป็นแค่ "หน้าตา" ส่วนข้อมูลจริง (หัวคอลัมน์ + แถว) อยู่ใน model ชื่อ
DefaultTableModel — หลักการเดียวกับที่เราแยก model/dao มาทั้ง lab 10: แก้ข้อมูลที่ model แล้วหน้าตาวาดใหม่เองservice.getAll() แล้ว setModel() ทิ้งตัวเก่า
ง่าย ไม่มีทาง sync คลาด (แลกกับอ่าน db ซ้ำ — ซึ่งถูกมากสำหรับข้อมูลระดับนี้)showEmployees() (Step 6) — เมธอดเดียวเรียกซ้ำทุกครั้งที่ข้อมูลเปลี่ยน| เมธอด | ใช้เมื่อไหร่ | รูปแบบ |
|---|---|---|
showMessageDialog(..., ERROR_MESSAGE) | ทำไม่สำเร็จ (login ผิด, ชื่อซ้ำ) | ไอคอนแดง ✕ + ปุ่ม OK |
showMessageDialog(..., WARNING_MESSAGE) | ยังไม่ได้เลือก / ไม่มีสิทธิ์ / ข้อมูลไม่ครบ | ไอคอนเหลือง ⚠ + ปุ่ม OK |
showConfirmDialog(..., YES_NO_OPTION) | ถามก่อนทำสิ่งที่กู้ไม่ได้ (ลบ) | คืนค่า JOptionPane.YES_OPTION / NO_OPTION ให้เราเช็ค |
แต่ละการ์ด: หน้าที่ · ใช้ที่หน้าจอไหน (ชื่อตัวแปรที่จะตั้ง) · จุดพลาดประจำตัว — เปิดหน้านี้คู่ไว้ตอนลาก control ได้เลย
JLabel (ข้อความนิ่ง: ① lblHeader ② lblLogin ④ lblPassword) ·
JTextField (③ txtLogin ช่องข้อความปกติ) · JPasswordField (⑤ txtPassword — พิมพ์แล้วเห็นจุดดำ) ·
JButton (⑥ btnLogin ⑦ btnExit) — ทั้งหมดวางอยู่บน JFrame
JComboBox (⑧ cmbRole — dropdown เลือก 1 จากหลายตัวเลือก)
และตัว dialog เองคือ JDialog (หน้าต่างลอยแบบ modal) — ปุ่ม ⑨⑩ คือ JButton ปกติ
JTable (tblEmployee ตารางข้อมูล) ·
JScrollPane (jScrollPane1 — ภาชนะครอบตาราง ให้มี scroll bar และหัวคอลัมน์ติดอยู่) ·
JLabel ตัวใหญ่บนสุด (lblWelcome) · แถว JButton ด้านล่างLoginFrame · EmployeeFrame (สร้างผ่าน New → JFrame Form)setTitle · setDefaultCloseOperation(EXIT_ON_CLOSE / DISPOSE_ON_CLOSE) · setVisible(true) · dispose() ปิดตัวเองEmployeeDialog (New → JDialog Form) — constructor ต้องส่ง super(owner, title, true) = modalsetVisible(true) (โปรแกรมรอตรงนี้จนปิด) · dispose() ปิดตัวเอง · isSaved() (เมธอดที่เราเพิ่มเองเพื่อตอบกลับหน้าหลัก)true ตัวที่สาม → ผู้ใช้คลิกหน้าหลักพลาดได้ระหว่างกรอกlblHeader · lblLogin/lblPassword/... · lblWelcome (ตั้งข้อความด้วยโค้ดตอนเปิดจอ)setText(...) · ตั้ง font ได้ (lblHeader ใช้ Leelawadee UI Bold 18)txtLogin (ชื่อเข้าใช้) · txtPosition (ตำแหน่ง)getText().trim() อ่านค่ามาใช้ · setText("") ล้างช่องtrim() → เว้นวรรคท้ายช่องกลายเป็นชื่อคนละคนtxtPassword ทุกหน้าจอnew String(txtPassword.getPassword()) — ต้องแปลง เพราะ getPassword() คืน char[] ไม่ใช่ StringgetText() กับ JPasswordField (วิธีเก่า เลิกใช้แล้ว) หรือลืมแปลง char[] → คอมไพล์ไม่ผ่านxxxActionPerformed + ผูก listener อัตโนมัติcmbRole — model 2 บรรทัด: พนักงาน (index 0), ผู้จัดการ (index 1)getSelectedIndex() อ่านว่าเลือกบรรทัดไหน · setSelectedIndex(i) ตั้งค่า (ตอนโหมดแก้ไข)tblEmployee — ตั้งเลือกได้ทีละ 1 แถว: setSelectionMode(SINGLE_SELECTION)setModel(model) เติมข้อมูล · getSelectedRow() · getValueAt(row, col) · clearSelection()addListSelectionListener ของตัวเอง (มีเฉพาะ JList) จึงไม่มีเมนู Events → ListSelection ใน designer — ต้องพิมพ์ผูกเองใน constructor: getSelectionModel().addListSelectionListener(...) (Step 7)jScrollPane1 ครอบ tblEmployee — ลาก Table ใน NetBeans แล้วได้มาให้เอง (คงชื่อเดิมไว้ได้)setViewportView(tblEmployee) — NetBeans ใส่ให้แล้วshowEmployees() — สร้างใหม่พร้อมหัวคอลัมน์ → addRow(...) ทีละพนักงาน → tblEmployee.setModel(model)new DefaultTableModel(data, headers) · addRow(Object[]) · setModel(...)showMessageDialog(parent, msg, title, TYPE) · showConfirmDialog(parent, msg, title, YES_NO_OPTION, QUESTION_MESSAGE) คืน int ให้เช็คthis (parent) ผิด → กล่องเด้งไปคนละจอ หรือไม่มี parent ให้ล็อกActionListener กับทุกปุ่ม (ดับเบิลคลิกใน designer ได้) · ListSelectionListener กับตาราง — ต้องพิมพ์ผูกเองใน constructor ผ่าน getSelectionModel().addListSelectionListener(...) (JTable ไม่มีเมนูนี้ใน designer ต่างจาก JList)if (row == -1) return;MoogataLab ของตัวเอง (จาก lab 10)TestEmployeeService.java → ต้องได้ Welcome user : somchai (แคชเชียร์) และ Error, wrong username or password
— พิสูจน์ว่าเลเยอร์ service + dao + ฐานข้อมูลคุยกันได้ครบTestEmployeeDao.java → ต้องเห็นพนักงาน 3 คน (somjai, somchai, malee)
— พิสูจน์ว่า seed data ใน moogata.db ยังอยู่ครบEmployeeServiceEmployeeService เปิดบริการแค่เมธอดเดียวคือ login() เพราะยังไม่มีใครต้องการอย่างอื่น
แต่ต่อจากนี้ หน้าจอ จะมาเป็นลูกค้าตัวใหม่ของ service และหน้าจอต้องการทั้งอ่าน เพิ่ม แก้ ลบ —
เราจึงต้องขยาย "หน้าเคาน์เตอร์" ให้เปิดบริการครบก่อน แล้วหน้าจอที่เขียนในขั้นถัดไปจะไม่ต้องรู้จัก DAO เลยสักนิดเปิดไฟล์ service/EmployeeService.java — เพิ่ม import ด้านบนไฟล์:
import java.util.List;
แล้วเพิ่มเมธอดต่อท้าย login() (ก่อนปิดคลาส):
// ===== เพิ่มใน Lab 11 — เปิดบริการ CRUD ที่ EmployeeDao รองรับอยู่แล้ว ให้หน้าจอใช้ =====
/** พนักงานทุกคน — ใช้โดย EmployeeFrame เพื่อเติมตาราง */
public List<Employee> getAll() {
return employeeDao.getAll();
}
/** พนักงาน 1 คนจาก id — ใช้โดย EmployeeFrame ตอนคลิกเลือกแถวในตาราง */
public Employee get(int id) {
return employeeDao.get(id);
}
/** เพิ่มพนักงานใหม่ — สำเร็จได้ object กลับมาพร้อม id, ล้มเหลว (ชื่อซ้ำ) ได้ null */
public Employee save(Employee employee) {
return employeeDao.save(employee);
}
/** แก้ไขพนักงาน — สำเร็จได้ object กลับมา, ไม่พบแถว/ล้มเหลวได้ null */
public Employee update(Employee employee) {
return employeeDao.update(employee);
}
/** ลบพนักงานตาม id — คืนจำนวนแถวที่ถูกลบ (0 = ไม่พบ, -1 = ผิดพลาด) */
public int delete(int id) {
Employee target = new Employee();
target.setId(id);
return employeeDao.delete(target);
}
EmployeeDao มี CRUD ครบอยู่แล้วตั้งแต่ lab 10
หน้าที่ของ service คือเป็น "หน้าเคาน์เตอร์" ให้ UI ไม่ต้องรู้จัก DAO เลย และวันหน้าถ้ามีกฎธุรกิจใหม่ (เช่น ห้ามลบผู้จัดการคนสุดท้าย)
จะเขียนกฎไว้ที่เคาน์เตอร์นี้จุดเดียว ทุกหน้าจอได้กฎใหม่ทันทีรัน TestEmployeeService ซ้ำ — ผลต้องเหมือนเดิมทุกบรรทัด
(นี่คือแบบฝึกหัดสไตล์นักพัฒนา: แก้โค้ดแล้วต้องรู้ว่าของเดิมไม่พัง — เราจะทำแบบนี้ทุกครั้งที่แตะของเก่า)
git add src/main/java/com/example/moogata/service/EmployeeService.java
git commit -m "lab11(ช่วง 1): เปิดบริการ CRUD ใน EmployeeService สำหรับหน้าจอ"ui + ออกแบบหน้าจอ LoginFramecom.example.moogata → New → Java Package ชื่อ ui
— package ใหม่เพื่อแยก "หน้าจอ" ออกจากโค้ดหลังบ้าน ตามกฎเลเยอร์ที่แผนภาพ lab 10 วางไว้ui → New → JFrame Form (หมวด Swing GUI Forms) ชื่อ LoginFrame
— NetBeans จะเปิดหน้าออกแบบ (Design) ให้ลากวาง control ได้เลย และสลับดูโค้ดได้ที่ปุ่ม Source
lblHeader หัวเรื่องกลางจอ · ②lblLogin คู่กับ ③txtLogin ช่องกรอกชื่อ ·
④lblPassword คู่กับ ⑤txtPassword (ใช้ Password Field ไม่ใช่ Text Field — ให้พิมพ์แล้วเห็นจุดดำ) ·
⑥btnLogin และ ⑦btnExit อยู่แถวล่าง| # | Control | text | Variable Name | เพิ่มเติม |
|---|---|---|---|---|
| 1 | Label | ระบบ POS BONUS SUKI | lblHeader | font: Leelawadee UI, Bold, 18 (กันไทยเป็นกล่อง □□□) |
| 2 | Label | ชื่อเข้าใช้: | lblLogin | |
| 3 | Text Field | — | txtLogin | |
| 4 | Label | รหัสผ่าน: | lblPassword | |
| 5 | Password Field | — | txtPassword | |
| 6 | Button | เข้าสู่ระบบ | btnLogin | |
| 7 | Button | ออก | btnExit |
ตั้งค่าฟอร์ม (คลิกพื้นหลัง → Properties): title เข้าสู่ระบบ — BONUS SUKI POS ·
defaultCloseOperation EXIT_ON_CLOSE (ปิดหน้านี้ = จบโปรแกรม เพราะเป็นหน้าแรก) ·
resizable ออก · ขนาด: ลากขอบฟอร์มใน designer ให้ได้ราว ~380 × 310 (NetBeans จัดการขนาดให้เอง — พิมพ์ตัวเลขใน Properties ไม่มีผล)
และเพิ่มบรรทัดเดียวใน constructor (หลัง initComponents();) เพื่อให้หน้าต่างเปิดกลางจอ ไม่ติดมุมซ้ายบน:
setLocationRelativeTo(null); // null = กึ่งกลางหน้าจอ
lbl/txt/btn?
เพราะหน้าจอใหญ่ขึ้นเรื่อย ๆ มองโค้ดแล้วต้องรู้ทันทีว่าอะไรคือปุ่ม อะไรคือช่องกรอก
— นิสัยนี้จะช่วยตอน EmployeeFrame ที่มี control 14 ตัว · หน้าที่ของแต่ละ component ดูเต็ม ๆ ได้ที่ หน้า 3 (ชุด Component Swing)main
แต่ "รอเหตุการณ์" แล้วรันโค้ดที่เราผูกไว้กับเหตุการณ์นั้น (ทฤษฎีข้อ 3 หน้า 2) เราจะผูก 2 ปุ่ม
และตั้งใจให้ล็อกอินสำเร็จชั่วคราวเป็นแค่ "กล่องทักทาย" ก่อน เพราะหน้าจัดการยังไม่เกิด
— ทำชิ้นเล็กให้รันได้จริงก่อน แล้วค่อยต่อชิ้นถัดไปดับเบิลคลิกปุ่ม btnLogin ใน designer (NetBeans จะสร้างเมธอด event ให้เอง — อย่าพิมพ์เมธอดเองลอย ๆ เพราะต้องมีการผูก event ที่โค้ดส่วน NetBeans ดูแล) แล้ววางโค้ด:
private void btnLoginActionPerformed(...) { ... } พร้อมปีกกาเปิด-ปิดให้แล้ว
(ส่วนหัวล็อกแก้ไม่ได้ ข้างในจะมีบรรทัด // TODO add your handling code here: รออยู่)
— โค้ดใน lab แสดง "เมธอดเต็ม" เพื่อให้เห็นภาพรวมเท่านั้น ตอนวางจริงให้คัดลอก เฉพาะบรรทัดเนื้อความ มาแทนบรรทัด TODO
ถ้าวางทั้งก้อนรวมหัวเมธอด = เมธอดซ้อนเมธอด → compile ไม่ผ่านทันทีprivate void btnLoginActionPerformed(java.awt.event.ActionEvent evt) {
// getPassword() คืน char[] (ไม่ใช่ String) — เพื่อความปลอดภัย ต้องแปลงก่อนใช้
String login = txtLogin.getText().trim();
String password = new String(txtPassword.getPassword());
EmployeeService employeeService = new EmployeeService();
Employee employee = employeeService.login(login, password);
if (employee == null) {
JOptionPane.showMessageDialog(this,
"ชื่อเข้าใช้หรือรหัสผ่านไม่ถูกต้อง",
"เข้าสู่ระบบไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
return;
}
// (Step 6 จะเปลี่ยนข้อความชั่วคราวนี้เป็นการเปิดหน้าจัดการพนักงาน)
JOptionPane.showMessageDialog(this,
"ยินดีต้อนรับ " + employee.getName() + " (" + employee.getPosition() + ")",
"เข้าสู่ระบบสำเร็จ", JOptionPane.INFORMATION_MESSAGE);
}
ดับเบิลคลิก btnExit:
private void btnExitActionPerformed(java.awt.event.ActionEvent evt) {
System.exit(0);
}
main() — NetBeans สร้างมาให้แล้วใน template ของ JFrame Form (อยู่ท้ายคลาส มีโค้ดตั้งค่า Nimbus ยาว ๆ ข้างใน) — ห้ามเพิ่ม main ซ้ำเด็ดขาด (จะ compile ไม่ผ่านเพราะมี main สองตัว) ให้แทนที่เนื้อในด้วยแบบสั้น ๆ ตามด้านล่าง:
/** คำสั่ง start โปรแกรม — รัน LoginFrame บนเธรดของ Swing (EDT) */
public static void main(String args[]) {
java.awt.EventQueue.invokeLater(() -> new LoginFrame().setVisible(true));
}
import com.example.moogata.model.Employee;
import com.example.moogata.service.EmployeeService;
import javax.swing.JOptionPane;
รัน LoginFrame (Shift+F6) แล้วลอง 2 กรณี: พิมพ์ somchai / 0000 กดเข้าสู่ระบบ — ต้องได้กล่องแดง:
จากนั้นลอง somchai / 1234 — ต้องได้กล่อง "ยินดีต้อนรับ สมชาย มานะงาน (แคชเชียร์)"
= login วิ่งลงไปถึงฐานข้อมูลจริงแล้วกลับมาถูก
git add src/main/java/com/example/moogata/ui/LoginFrame.java
git commit -m "lab11(ช่วง 2): หน้าจอ LoginFrame — ล็อกอินด้วย Swing"EmployeeFrame — ฟอร์มบน + ตารางล่างNew → JFrame Form ชื่อ EmployeeFrame ใน package ui แล้วลาก control ตามภาพ:
lblWelcome บอกว่าใครล็อกอินอยู่ (ตั้งค่าในโค้ด) ·
ฟอร์มครึ่งบน: ②lblLogin+③txtLogin กับ ④lblPosition+⑤txtPosition แถวแรก,
⑥lblPassword+⑦txtPassword กับ ⑧lblRole+⑨cmbRole แถวสอง ·
แถวปุ่ม: ⑩btnSave ⑪btnNew ⑫btnDelete ⑬btnRefresh ·
⑭tblEmployee ตารางครึ่งล่าง (ตอนนี้ตารางยังไม่มีข้อมูล — จะเติมด้วยโค้ดใน Step 6)| # | Control | text | Variable Name | เพิ่มเติม |
|---|---|---|---|---|
| 1 | Label | (ตั้งในโค้ด) | lblWelcome | font: Bold, 14 |
| 2 | Label | ชื่อล็อกอิน: | lblLogin | แถวบนของฟอร์ม |
| 3 | Text Field | — | txtLogin | |
| 4 | Label | ตำแหน่ง: | lblPosition | |
| 5 | Text Field | — | txtPosition | |
| 6 | Label | รหัสผ่าน: | lblPassword | แถวล่างของฟอร์ม |
| 7 | Password Field | — | txtPassword | |
| 8 | Label | บทบาท: | lblRole | |
| 9 | Combo Box | — | cmbRole | Properties → model: พนักงาน (บรรทัด 1), ผู้จัดการ (บรรทัด 2) |
| 10 | Button | บันทึก | btnSave | แถวปุ่มใต้ฟอร์ม |
| 11 | Button | ล้างฟอร์ม | btnNew | |
| 12 | Button | ลบ | btnDelete | |
| 13 | Button | รีเฟรช | btnRefresh | |
| 14 | Table | — | tblEmployee | ลาก Table แล้ว NetBeans ครอบด้วย jScrollPane1 ให้เอง (คงชื่อเดิม) |
ตั้งค่าฟอร์ม: title จัดการพนักงานระบบ POS — BONUS SUKI · defaultCloseOperation DISPOSE_ON_CLOSE
(ต่างจากหน้าล็อกอิน — ปิดหน้านี้แค่หน้านี้ปิด โปรแกรมไม่ตาย และหน้าแรกก็ปิดไปแล้วตอนเข้ามา) ·
resizable ออก · ขนาด: ลากขอบฟอร์มใน designer ให้ได้ราว ~656 × 560
สลับไปแท็บ Source ของ EmployeeFrame แล้วแก้ 3 จุด:
(1) field + constructor (แก้ constructor ที่ NetBeans สร้างให้ — เพิ่มพารามิเตอร์ loginUser
เพื่อให้หน้านี้รู้ว่า "ใคร" เข้ามาใช้ จะได้โชว์ชื่อและกันลบบัญชีตัวเองใน Step 7):
main() มาให้ท้ายคลาส
ซึ่งเรียก new EmployeeFrame() แบบไม่มีพารามิเตอร์ — พอเราเปลี่ยน constructor ให้ต้องมีพารามิเตอร์ จะ compile ไม่ผ่านทันที
ให้ลบ main() ทิ้ง (หน้านี้เปิดจาก LoginFrame ไม่ได้รันตรง)/** พนักงานที่ล็อกอินเข้ามา (เก็บไว้โชว์ที่ lblWelcome และกันลบบัญชีตัวเอง) */
private final Employee loginUser;
private final EmployeeService employeeService = new EmployeeService();
/** id ของแถวที่กำลังแก้ไข (-1 = ไม่ได้แก้ไข คือโหมดเพิ่มใหม่) */
private int editingId = -1;
public EmployeeFrame(Employee loginUser) {
this.loginUser = loginUser;
initComponents();
tblEmployee.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
tblEmployee.getTableHeader().setReorderingAllowed(false); // ล็อกลำดับคอลัมน์ — กันลากสลับแล้วโค้ดอ่านค่าผิดคอลัมน์
// (บรรทัดผูก listener "คลิกแถว → เติมฟอร์ม" จะเพิ่มใน Step 7)
lblWelcome.setText("ผู้ใช้งาน: " + loginUser.getName() + " (" + loginUser.getPosition() + ")");
showEmployees();
}
(2) เมธอดเติมตาราง — หัวใจของครึ่งล่าง สังเกตว่าเราสร้าง model ใหม่ทุกครั้ง (หัวคอลัมน์ + แถวจากฐานข้อมูลสด ๆ) ไม่ใช่แก้ทีละแถว — วิธี "ล้างแล้วเติมใหม่ทั้งชุด" ง่ายและไม่มีทางข้อมูลค้าง (ทฤษฎีข้อ 6 หน้า 2) พร้อมตั้งให้ช่องในตาราง แก้ไขไม่ได้: ตารางทำหน้าที่โชว์เท่านั้น ถ้าให้พิมพ์ทับบนจอ ค่าจะเปลี่ยนแต่ไม่ลงฐานข้อมูล (แล้วผู้ใช้จะเข้าใจผิดว่าบันทึกแล้ว):
/** อ่านข้อมูลพนักงานทุกคนจาก service แล้วเติมลงตาราง — เรียกทุกครั้งที่ข้อมูลเปลี่ยน */
private void showEmployees() {
// ตั้งหัวคอลัมน์แล้วเติมแถวจากฐานข้อมูลใหม่ทั้งหมด
// isCellEditable = false: ตารางไว้ "ดู" เท่านั้น — แก้ข้อมูลต้องผ่านฟอร์มด้านบนแล้วบันทึก จึงลงฐานข้อมูลจริง
DefaultTableModel model = new DefaultTableModel(
new Object[][]{},
new String[]{
"รหัส", "ชื่อล็อกอิน", "ตำแหน่ง", "รหัสผ่าน", "บทบาท"
}) {
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
for (Employee emp : employeeService.getAll()) {
model.addRow(new Object[]{
emp.getId(),
emp.getName(),
emp.getPosition(),
emp.getPassword(),
roleName(emp.getRole())
});
}
tblEmployee.setModel(model);
}
(3) เมธอดช่วยแปลงบทบาท — ตารางโชว์ข้อความไทย ("ผู้จัดการ"/"พนักงาน") ไม่ใช่เลข 1/2 — ใส่ไว้เลยเพื่อให้จบ Step 6 แล้ว compile ผ่าน:
/** แปลงเลขบทบาทเป็นข้อความสำหรับคอลัมน์ "บทบาท" ในตาราง */
private String roleName(int role) {
return role == 1 ? "ผู้จัดการ" : "พนักงาน";
}
(4) ปุ่มรีเฟรช + import: ดับเบิลคลิก btnRefresh → วางโค้ด แล้วเพิ่ม import
private void btnRefreshActionPerformed(java.awt.event.ActionEvent evt) {
showEmployees();
}
import com.example.moogata.model.Employee;
import com.example.moogata.service.EmployeeService;
import javax.swing.JOptionPane;
import javax.swing.table.DefaultTableModel;
จากนั้นกลับไปที่ LoginFrame — แทนข้อความชั่วคราวท้าย btnLoginActionPerformed ด้วย "ประตูที่สอง" คือกฎเรื่องสิทธิ์ + การเปิดหน้าจัดการ:
// เดิม (Step 4): JOptionPane.showMessageDialog(this, "ยินดีต้อนรับ ...");
// เปลี่ยนเป็น:
// กฎธุรกิจ: หน้าจัดการพนักงานเปิดเฉพาะผู้จัดการ (role = 1) เท่านั้น
if (employee.getRole() != 1) {
JOptionPane.showMessageDialog(this,
employee.getName() + " ไม่ใช่ผู้จัดการ จึงเข้าหน้าจัดการพนักงานไม่ได้",
"ไม่มีสิทธิ์เข้าใช้", JOptionPane.WARNING_MESSAGE);
return;
}
EmployeeFrame employeeFrame = new EmployeeFrame(employee);
employeeFrame.setLocationRelativeTo(this);
employeeFrame.setVisible(true);
this.dispose();
employee ที่ล็อกอินผ่าน) ไปให้ · จัดกลางจอ · เปิดโชว์ ·
แล้ว dispose() ปิดหน้าล็อกอินทิ้ง — ต่างจาก System.exit(0) ของปุ่มออกที่ปิดทั้งโปรแกรมรัน LoginFrame → login somjai / 1234 → ต้องเห็นหน้าจัดการพนักงาน มีตาราง 3 แถว
ทดลองกดรีเฟรช: เปิด Letos ไปแก้ข้อมูล (เช่นเปลี่ยนตำแหน่ง malee) แล้วกดรีเฟรชในโปรแกรม — ตารางต้องเปลี่ยนตาม
= พิสูจน์ว่าตารางอ่านจากฐานข้อมูลจริงทุกครั้ง ไม่ใช่จำค่าเก่า
git add src/main/java/com/example/moogata/ui/
git commit -m "lab11(ช่วง 3): หน้าจอ EmployeeFrame — ฟอร์มบน ตารางล่าง"editingId ที่ประกาศไว้แล้วใน Step 6(1) คลิกเลือกแถวแล้วค่าขึ้นฟอร์ม — handler ของตารางต่างจากปุ่ม: ไม่มีเมนู Events → ListSelection ให้ใช้
(JTable ไม่มี addListSelectionListener ของตัวเอง — มีเฉพาะ JList) ทำ 2 ขั้น:
ขั้นแรก — ผูก listener ใน constructor (เพิ่มบรรทัดนี้ต่อจาก setSelectionMode ที่เขียนไว้ใน Step 6):
// ผูก listener "คลิกแถว → เติมฟอร์ม"
tblEmployee.getSelectionModel().addListSelectionListener(this::tblEmployeeValueChanged);
ขั้นต่อมา — เขียนเมธอด handler วางใน Source:
/** คลิกเลือกแถวในตาราง → อ่านข้อมูลจากฐานข้อมูลแล้วเติมลงฟอร์มด้านบน (เข้าสู่โหมดแก้ไข) */
private void tblEmployeeValueChanged(javax.swing.event.ListSelectionEvent evt) {
if (evt.getValueIsAdjusting()) {
return; // event นี้ยิง 2 รอบต่อการคลิก 1 ครั้ง — รอบแรก (selection ยังปรับไม่จบ) ข้ามไปก่อน
}
int row = tblEmployee.getSelectedRow();
if (row == -1) {
return; // ช่วงล้างตาราง/ล้างฟอร์ม — ไม่มีแถวถูกเลือก ไม่ต้องทำอะไร
}
int id = (Integer) tblEmployee.getValueAt(row, 0);
Employee emp = employeeService.get(id);
if (emp == null) {
return;
}
editingId = emp.getId();
txtLogin.setText(emp.getName());
txtPosition.setText(emp.getPosition());
txtPassword.setText(emp.getPassword());
setSelectedRole(emp.getRole());
}
employeeService.get(id) ไม่ใช่อ่านจากที่โชว์บนตาราง
— ให้ฟอร์มได้ค่าจาก "แหล่งจริง" เสมอ กรณีคนอื่นแก้ไปก่อน ฟอร์มจะได้ค่าล่าสุด
(2) listener นี้ถูกเรียก ทุกครั้งที่ selection เปลี่ยน รวมถึงตอนโปรแกรมเราเองล้าง selection
จึงต้องมี if (row == -1) return; ดักไว้บนสุด ไม่งั้นโปรแกรมคว่ำตอน clearSelection()
(3) ListSelectionEvent ยิง 2 รอบต่อการคลิก 1 ครั้ง (รอบแรก getValueIsAdjusting() = true คือกำลังปรับ, รอบสอง = จบแล้ว)
— ดักด้วย getValueIsAdjusting() ไว้บรรทัดแรก ไม่งั้นเราจะอ่านฐานข้อมูลซ้ำเปล่า ๆ รอบละ 2 ครั้ง(2) ปุ่มบันทึก (ดับเบิลคลิก btnSave) — ปุ่มเดียวสองพฤติกรรม แยกด้วยสถานะ editingId:
private void btnSaveActionPerformed(java.awt.event.ActionEvent evt) {
String name = txtLogin.getText().trim();
String position = txtPosition.getText().trim();
String password = new String(txtPassword.getPassword());
if (name.isEmpty() || position.isEmpty() || password.isEmpty()) {
JOptionPane.showMessageDialog(this,
"กรอกข้อมูลให้ครบทุกช่องก่อนบันทึก",
"ข้อมูลไม่ครบ", JOptionPane.WARNING_MESSAGE);
return;
}
Employee emp = new Employee(name, position, password, getSelectedRole());
if (editingId > 0) {
// มีแถวถูกเลือกอยู่ = โหมดแก้ไข (ใช้ id ของแถวที่คลิก)
emp.setId(editingId);
if (employeeService.update(emp) == null) {
JOptionPane.showMessageDialog(this,
"แก้ไขไม่สำเร็จ (ชื่อล็อกอินซ้ำกับคนอื่น หรือไม่พบแถวเดิม)",
"แก้ไขไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
return;
}
} else {
// ฟอร์มว่างจากการล้าง = โหมดเพิ่มใหม่
if (employeeService.save(emp) == null) {
JOptionPane.showMessageDialog(this,
"บันทึกไม่สำเร็จ (ชื่อล็อกอินซ้ำกับคนอื่น)",
"บันทึกไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
return;
}
}
clearForm();
showEmployees();
}
(3) ปุ่มล้างฟอร์ม (ดับเบิลคลิก btnNew) — หน้าที่เดียวคือพาฟอร์มกลับสู่โหมดเพิ่ม:
private void btnNewActionPerformed(java.awt.event.ActionEvent evt) {
clearForm();
}
(4) ปุ่มลบ (ดับเบิลคลิก btnDelete) — ลบเป็นการกระทำที่กู้ไม่ได้ จึงมีกันตัว 2 ชั้น:
ห้ามลบบัญชีที่ล็อกอินอยู่ และถามยืนยันก่อนทุกครั้ง:
private void btnDeleteActionPerformed(java.awt.event.ActionEvent evt) {
int row = tblEmployee.getSelectedRow();
if (row == -1) {
JOptionPane.showMessageDialog(this,
"เลือกพนักงานในตารางก่อนกดลบ",
"ยังไม่ได้เลือก", JOptionPane.WARNING_MESSAGE);
return;
}
int id = (Integer) tblEmployee.getValueAt(row, 0);
String selectedName = (String) tblEmployee.getValueAt(row, 1);
// กฎธุรกิจ: ห้ามลบบัญชีที่กำลังล็อกอินอยู่ (กันผู้จัดการลบตัวเองจนเข้าระบบไม่ได้)
if (selectedName.equals(loginUser.getName())) {
JOptionPane.showMessageDialog(this,
"ลบบัญชีที่กำลังใช้งานอยู่ไม่ได้",
"ลบไม่ได้", JOptionPane.WARNING_MESSAGE);
return;
}
int choice = JOptionPane.showConfirmDialog(this,
"ต้องการลบพนักงาน " + selectedName + " ใช่หรือไม่?",
"ยืนยันการลบ", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
if (choice != JOptionPane.YES_OPTION) {
return;
}
int deleted = employeeService.delete(id);
if (deleted > 0) {
clearForm();
showEmployees();
} else {
JOptionPane.showMessageDialog(this,
"ลบไม่สำเร็จ (อาจถูกลบไปก่อนแล้ว)",
"ลบไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
}
}
(5) เมธอดช่วย — เพิ่มท้ายคลาส (roleName() ใส่ไว้แล้วใน Step 6):
/** ล้างฟอร์มกลับสู่โหมดเพิ่มใหม่ (editingId = -1) */
private void clearForm() {
editingId = -1;
tblEmployee.clearSelection(); // จะเรียก valueChanged กลับมาด้วยแถว -1 (มี guard รออยู่)
txtLogin.setText("");
txtPosition.setText("");
txtPassword.setText("");
cmbRole.setSelectedIndex(0);
}
/** แปลงรายการที่เลือกใน cmbRole เป็นเลข role ที่เก็บในฐานข้อมูล */
private int getSelectedRole() {
return cmbRole.getSelectedIndex() == 1 ? 1 : 2;
}
/** ตั้ง cmbRole ให้ตรงกับเลข role ที่เก็บในฐานข้อมูล */
private void setSelectedRole(int role) {
cmbRole.setSelectedIndex(role == 1 ? 1 : 0);
}
ทดสอบชิ้นนี้ทันที: รัน → login somjai → คลิกแถว somchai → ฟอร์มต้องเติมค่าเดิมทันที:
employee → ต้องเหลือเดิม 3 แถว (somjai, somchai, malee)
— ถ้ามีคนอื่นค้างจากตอนลองโค้ด ลบทิ้งให้เหลือ 3 คนนี้ก่อน (ข้อมูลสะอาด = ผลทดสอบเชื่อถือได้)LoginFrame จาก NetBeans แล้ววางหน้าต่าง Letos ไว้ข้าง ๆ — เราจะยืนยัน "ของจริงใน db" คู่กับ "บนจอ" ไปด้วยsomchai / 0000 → เข้าสู่ระบบ → อ่านข้อความ → กด OKsomchai / 1234 → เข้าสู่ระบบ
somjai / 1234 → เข้าสู่ระบบeditingId มีปัญหา (ดู Troubleshooting ข้อ 5)
roleName() ก่อนเติมตารางเช็ค Letos อีกครั้ง — ตารางต้องกลับมา 3 แถวเดิม (somjai/somchai/malee โดย somchai อาจเป็น "หัวหน้ากะ" จากข้อ 6 — จะแก้คืนใน Letos ก็ได้) = ฐานข้อมูลสะอาดเท่าก่อนทดสอบ และถ่ายภาพหน้าจออย่างน้อย 3 ภาพเก็บเป็นหลักฐาน (ข้อ 3, ข้อ 4/7, ข้อ 8)
| ถ้าข้อนี้ไม่ผ่าน | ปัญหาอยู่ที่ | ย้อนไปดู |
|---|---|---|
| 1–2 (login ผิดพฤติกรรม) | เลเยอร์ service / event ปุ่ม login | Step 2, 4 |
| 3 (เข้าหน้าจัดการไม่ได้) | การส่ง object / เปิด frame | Step 6 (จุดเชื่อม LoginFrame) |
| 4–5 (เพิ่ม/ชนซ้ำ) | btnSave + การรีเฟรชตาราง | Step 7 (2) + showEmployees |
| 6 (แก้ไขกลายเป็นเพิ่ม) | สถานะ editingId / listener | Step 7 (1), (2), (5) |
| 7–8 (ลบผิดพฤติกรรม) | กฎธุรกิจ / ยืนยัน / ลบ | Step 7 (4) |
git add src/main/java/com/example/moogata/ui/
git commit -m "lab11(ช่วง 4): CRUD พนักงานครบในหน้าต่างเดียว + สิทธิ์ผู้จัดการ"MoogataLab → Close
(ปิดก่อนคัดลอก — กันไฟล์ถูกล็อกจากโปรแกรมที่กำลังรันอยู่)NetBeansProjects → คลิกขวาโฟลเดอร์ MoogataLab → Copy → Paste → ตั้งชื่อ MoogataLabDialog
(คัดลอกทั้งโฟลเดอร์ = ได้โค้ด + moogata.db ที่มีข้อมูลครบมาด้วย)MoogataLabDialog → แก้ไฟล์ pom.xml 2 จุด:
① เปลี่ยน <artifactId>moogata-lab</artifactId> เป็น <artifactId>moogata-lab-dialog</artifactId>
② เปลี่ยน <name>moogata-lab</name> เป็น <name>moogata-lab-dialog</name>
— จุดที่ 2 สำคัญ: NetBeans แสดงชื่อโปรเจกต์จาก <name> ถ้าไม่แก้จะเห็นสองโปรเจกต์ชื่อเดียวกันจนเปิดผิด).git ติดมาด้วย (ประวัติ commit ของส่วนที่ 1)
— เปิดให้เห็นไฟล์ซ่อนก่อน: File Explorer → View → Show → Hidden items (Windows 11) / แท็บ View ติ๊ก Hidden items (Windows 10)
แล้วลบโฟลเดอร์ .git ใน MoogataLabDialog ทิ้ง แล้ว git init ใหม่
เพื่อให้สองโปรเจกต์มีประวัติแยกกัน (ส่วนที่ 1 อยู่ repo เดิม ส่วนที่ 2 เริ่ม repo ใหม่)MoogataLabDialog → ลบโฟลเดอร์ target/ ทิ้ง (ของเก่าที่คัดลอกมาด้วย) — หรือเปิดแล้วกด Clean and Build หนึ่งรอบMoogataLabDialog → OpenTestEmployeeService ต้องผ่านเหมือนเดิมทันที
(พิสูจน์ว่าคัดลอกมาครบ — รัน LoginFrame ดูหน้าจอก็ได้ ต้องใช้งานได้ปกติ)git add .
git commit -m "lab11(dialog) ช่วง 5: คัดลอกโปรเจกต์จากแบบฟอร์มรวมมาทำแบบ dialog"EmployeeDialog — ฟอร์มพนักงาน 1 คนในกล่องisSaved()New → JDialog Form ชื่อ EmployeeDialog ใน package ui แล้วลาก control ตามภาพ:
lblLogin+②txtLogin ·
③lblPosition+④txtPosition · ⑤lblPassword+⑥txtPassword ·
⑦lblRole+⑧cmbRole (model: พนักงาน/ผู้จัดการ) · ⑨btnOK "บันทึก" และ ⑩btnCancel "ยกเลิก"
— ภาพนี้คือโหมดเพิ่ม (ทุกช่องว่าง) ที่จะเปิดขึ้นมาเมื่อกดปุ่ม "เพิ่มพนักงาน" ใน Step 11ตั้งค่า: resizable ออก · ขนาด: ลากขอบใน designer ราว ~480 × 330 (title ตั้งในโค้ดตอนเปิด)
EmployeeDialog(java.awt.Frame parent, boolean modal) — ลบทิ้ง แล้ววาง 2 constructor ใหม่ของเราแทน
(2) เมธอด main() ที่ template สร้างมาให้ท้ายคลาส — ลบทิ้ง (dialog เปิดจาก EmployeeFrame ไม่ได้รันตรง)
— ถ้าลบ constructor เดิมแต่ลืมลบ main() จะคอมไพล์ไม่ผ่านทันที เพราะ main() ยังเรียก constructor เดิม (new EmployeeDialog(new JFrame(), true)) อยู่ ·
และวางโค้ด event ใหม่ = เฉพาะเนื้อในระหว่างปีกกา (กล่องเตือนใน Step 4)ฟอร์มนี้ใช้ร่วม 2 โหมด (เพิ่ม = ช่องว่าง / แก้ไข = เติมค่าเดิม) — สลับไป Source แล้วแก้ constructor ที่ NetBeans สร้างให้เป็น 2 แบบ + เพิ่ม field/เมธอดช่วย:
private final EmployeeService employeeService = new EmployeeService();
private boolean saved = false;
private int employeeId = -1; // > 0 เมื่อเป็นโหมดแก้ไข
/** โหมดเพิ่มพนักงานใหม่ */
public EmployeeDialog(java.awt.Frame owner, String title) {
super(owner, title, true);
initComponents();
setLocationRelativeTo(owner); // เปิดกลางหน้าต่างหลัก
}
/** โหมดแก้ไข — เติมค่าเดิมของพนักงานที่เลือกให้ทุกช่อง */
public EmployeeDialog(java.awt.Frame owner, String title, Employee editEmployee) {
super(owner, title, true);
initComponents();
setLocationRelativeTo(owner); // เปิดกลางหน้าต่างหลัก
this.employeeId = editEmployee.getId();
txtLogin.setText(editEmployee.getName());
txtPosition.setText(editEmployee.getPosition());
txtPassword.setText(editEmployee.getPassword());
setSelectedRole(editEmployee.getRole());
}
/** EmployeeFrame ใช้ตรวจหลัง dialog ปิด: true = บันทึกสำเร็จ ควรรีเฟรชตาราง */
public boolean isSaved() {
return saved;
}
/** แปลงรายการที่เลือกใน cmbRole เป็นเลข role ที่เก็บในฐานข้อมูล */
private int getSelectedRole() {
return cmbRole.getSelectedIndex() == 1 ? 1 : 2;
}
/** ตั้ง cmbRole ให้ตรงกับเลข role ที่เก็บในฐานข้อมูล */
private void setSelectedRole(int role) {
cmbRole.setSelectedIndex(role == 1 ? 1 : 0);
}
true ตัวที่สามของ super(...) = modal
— เปิดแล้วหน้าต่างหลักถูกล็อกจนกว่า dialog จะปิด ·
constructor overload 2 แบบ (เหมือนบทเรียน constructor ของ Employee ใน lab 10) ให้ผู้เรียกเลือก "โหมดเพิ่ม" หรือ "โหมดแก้ไข" โดยส่งพนักงานมาหรือไม่ ·
isSaved() คือช่องทางที่ dialog "ตอบกลับ" หน้าจอหลักหลังปิดตัวดับเบิลคลิก btnOK:
private void btnOKActionPerformed(java.awt.event.ActionEvent evt) {
String name = txtLogin.getText().trim();
String position = txtPosition.getText().trim();
String password = new String(txtPassword.getPassword());
if (name.isEmpty() || position.isEmpty() || password.isEmpty()) {
JOptionPane.showMessageDialog(this,
"กรอกข้อมูลให้ครบทุกช่องก่อนบันทึก",
"ข้อมูลไม่ครบ", JOptionPane.WARNING_MESSAGE);
return;
}
Employee emp = new Employee(name, position, password, getSelectedRole());
if (employeeId > 0) {
// โหมดแก้ไข — ใส่ id เดิมกลับเข้าไปก่อน update
emp.setId(employeeId);
if (employeeService.update(emp) == null) {
JOptionPane.showMessageDialog(this,
"แก้ไขไม่สำเร็จ (ชื่อล็อกอินซ้ำกับคนอื่น หรือไม่พบแถวเดิม)",
"แก้ไขไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
return;
}
} else {
// โหมดเพิ่ม — save จะคืน object พร้อม id ใหม่ หรือ null ถ้าล้มเหลว
if (employeeService.save(emp) == null) {
JOptionPane.showMessageDialog(this,
"บันทึกไม่สำเร็จ (ชื่อล็อกอินซ้ำกับคนอื่น)",
"บันทึกไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
return;
}
}
saved = true;
dispose();
}
ดับเบิลคลิก btnCancel:
private void btnCancelActionPerformed(java.awt.event.ActionEvent evt) {
dispose();
}
เพิ่ม import: com.example.moogata.model.Employee, com.example.moogata.service.EmployeeService, javax.swing.JOptionPane
(ตอนนี้ dialog ยังไม่มีใครเรียก — โปรแกรมรันได้ปกติ เราจะเชื่อมใน Step 11)
git add src/main/java/com/example/moogata/ui/EmployeeDialog.java
git commit -m "lab11(dialog) ช่วง 6: EmployeeDialog — ฟอร์มเพิ่ม/แก้ไขพนักงาน 1 คน"EmployeeFrame: ลบฟอร์มด้านบน เหลือตาราง + ปุ่ม (เรียก dialog แทน)เป้าหมายของหน้าจอหลังรื้อ (ตารางเต็มหน้าต่าง ปุ่มอยู่แถวล่าง — ไม่มีฟอร์ม):
lblWelcome · ②tblEmployee ตารางเต็มพื้นที่ (สูงขึ้นกว่าเดิมเพราะไม่มีฟอร์มบีบ) ·
③btnAdd "เพิ่มพนักงาน" (ปุ่มใหม่ แทน btnSave/btnNew เดิม) · ④btnEdit · ⑤btnDelete · ⑥btnRefresh
— สังเกตว่าไม่มีช่องกรอกใด ๆ เหลืออยู่แล้วtblEmployee.getSelectionModel().addListSelectionListener(this::tblEmployeeValueChanged); ใน constructor เป็นของเรา (ไม่อยู่ใน guarded block) → ลบบรรทัดนี้และเมธอด tblEmployeeValueChanged เองใน Source.form ของ designer จะได้ตามไปด้วย)lblLogin, txtLogin, lblPosition, txtPosition, lblPassword, txtPassword, lblRole, cmbRole แล้วกด Delete
(ลบใน designer เท่านั้น — อย่าไปลบโค้ดใน guarded block เอง)btnSave และ btnNew ทิ้ง (หน้าที่ของทั้งคู่ย้ายไปอยู่ที่ btnOK ของ dialog แล้ว)เพิ่มพนักงาน → ตั้งชื่อตัวแปร btnAdd · text แก้ไข → ตั้งชื่อตัวแปร btnEdit
(ส่วนที่ 1 ไม่มีปุ่มแก้ไข — เพราะเคยแก้ผ่านฟอร์มบน) · จากนั้น ลากตารางขยายสูงขึ้นกินพื้นที่ว่างของฟอร์มเดิม
และจัดปุ่มเรียงใต้ตาราง: เพิ่มพนักงาน · แก้ไข · ลบ · รีเฟรช (ดูตำแหน่งจากภาพ · ขนาดฟอร์ม: ลากขอบให้ได้ราว ~656 × 470)tblEmployeeValueChanged เองใน Source
จากนั้นลบโค้ดเก่าที่เกี่ยวกับฟอร์มออก (editingId, btnSaveActionPerformed, btnNewActionPerformed, clearForm, getSelectedRole, setSelectedRole)
— ส่วน constructor, showEmployees(), btnRefreshActionPerformed, roleName() คงไว้ (ยังใช้อยู่)
แล้วเพิ่ม handler ใหม่ 2 ตัว (btnAdd, btnEdit — ดับเบิลคลิกสร้างก่อน) และแทนที่เนื้อในของ btnDeleteActionPerformed ที่มีอยู่แล้วจากส่วนที่ 1 (ห้ามสร้างเมธอดซ้ำ) ตามด้านล่างprivate void btnAddActionPerformed(java.awt.event.ActionEvent evt) {
EmployeeDialog dialog = new EmployeeDialog(this, "เพิ่มพนักงานใหม่");
dialog.setVisible(true); // modal — โปรแกรมรอตรงนี้จนกว่า dialog จะถูกปิด
if (dialog.isSaved()) {
showEmployees(); // ข้อมูลเปลี่ยน → อ่านใหม่จากฐานข้อมูล
}
}
private void btnEditActionPerformed(java.awt.event.ActionEvent evt) {
int row = tblEmployee.getSelectedRow();
if (row == -1) {
JOptionPane.showMessageDialog(this,
"เลือกพนักงานในตารางก่อนกดแก้ไข",
"ยังไม่ได้เลือก", JOptionPane.WARNING_MESSAGE);
return;
}
int id = (Integer) tblEmployee.getValueAt(row, 0);
Employee emp = employeeService.get(id);
if (emp == null) {
JOptionPane.showMessageDialog(this,
"หาพนักงานรหัส " + id + " ไม่พบ (อาจถูกลบไปแล้ว)",
"ไม่พบข้อมูล", JOptionPane.ERROR_MESSAGE);
return;
}
EmployeeDialog dialog = new EmployeeDialog(this, "แก้ไขพนักงาน: " + emp.getName(), emp);
dialog.setVisible(true);
if (dialog.isSaved()) {
showEmployees();
}
}
private void btnDeleteActionPerformed(java.awt.event.ActionEvent evt) {
int row = tblEmployee.getSelectedRow();
if (row == -1) {
JOptionPane.showMessageDialog(this,
"เลือกพนักงานในตารางก่อนกดลบ",
"ยังไม่ได้เลือก", JOptionPane.WARNING_MESSAGE);
return;
}
int id = (Integer) tblEmployee.getValueAt(row, 0);
String selectedName = (String) tblEmployee.getValueAt(row, 1);
// กฎธุรกิจ: ห้ามลบบัญชีที่กำลังล็อกอินอยู่ (กันผู้จัดการลบตัวเองจนเข้าระบบไม่ได้)
if (selectedName.equals(loginUser.getName())) {
JOptionPane.showMessageDialog(this,
"ลบบัญชีที่กำลังใช้งานอยู่ไม่ได้",
"ลบไม่ได้", JOptionPane.WARNING_MESSAGE);
return;
}
int choice = JOptionPane.showConfirmDialog(this,
"ต้องการลบพนักงาน " + selectedName + " ใช่หรือไม่?",
"ยืนยันการลบ", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
if (choice != JOptionPane.YES_OPTION) {
return;
}
int deleted = employeeService.delete(id);
if (deleted > 0) {
showEmployees();
} else {
JOptionPane.showMessageDialog(this,
"ลบไม่สำเร็จ (อาจถูกลบไปก่อนแล้ว)",
"ลบไม่สำเร็จ", JOptionPane.ERROR_MESSAGE);
}
}
btnAdd และ btnEdit ใน designer
เพื่อสร้าง btnAddActionPerformed / btnEditActionPerformed ก่อนวางโค้ด — ปุ่มที่สร้างใหม่ยังไม่มี handler จนกว่าจะดับเบิลคลิก
(ส่วน btnDelete/btnRefresh ใช้ตัวเดิมจากส่วนที่ 1 — แค่แก้เนื้อในของ btnDelete) · วางโค้ดใหม่ = เฉพาะเนื้อในระหว่างปีกกา (กล่องเตือนใน Step 4)editingId + listener + clearForm ระวังชนกันเอง ·
แบบ dialog: หน้าจอหลักเหลือ "ตาราง + 4 ปุ่ม" ไม่มีสถานะซ่อนอยู่เลย โหมดเพิ่ม/แก้ไขถูกตัดสินที่ constructor ของ dialog
— โค้ดหน้าหลักสั้นลงแต่แลกมาด้วยการคลิกเพิ่ม 1–2 ครั้งต่อการบันทึกหนึ่งรายการ นี่คือ trade-off ที่คนออกแบบ UI ต้องเลือกจริงgit add src/main/java/com/example/moogata/ui/
git commit -m "lab11(dialog) ช่วง 7: รื้อ EmployeeFrame เป็นตาราง+ปุ่ม เรียก EmployeeDialog"LoginFrame ของโปรเจกต์ MoogataLabDialog (อย่าสับสนกับโปรเจกต์เดิม!) → login somjai / 1234| # | ทำอย่างไร | ต้องเห็น | พิสูจน์อะไร / ต่างจากแบบรวม |
|---|---|---|---|
| 1 | login somchai / 0000 | กล่องแดง ค้างหน้า login | เหมือนเดิม — การยืนยันตัวตนไม่ขึ้นกับรูปแบบหน้าจอ |
| 2 | login somchai / 1234 | กล่องเหลือง "ไม่ใช่ผู้จัดการ..." | เหมือนเดิม — กฎสิทธิ์อยู่ที่ LoginFrame ทั้งสองโปรเจกต์ |
| 3 | login somjai / 1234 | หน้าจัดการ ตาราง 3 แถว ไม่มีฟอร์มด้านบนแล้ว | โครงใหม่ตามที่รื้อใน Step 11 |
| 4 | ไม่เลือกแถว กด "แก้ไข" / "ลบ" ทีละปุ่ม | กล่องเหลือง "เลือกพนักงานในตารางก่อน..." | มีเพิ่มในแบบ dialog — เพราะไม่มีฟอร์มให้พิมพ์แล้ว การเลือกแถวจึงเป็นเงื่อนไขบังคับของทุกปุ่มยกเว้นเพิ่ม |
| 5 | กด "เพิ่มพนักงาน" → พิมพ์ครึ่งหนึ่ง → กด ยกเลิก | dialog ปิด ตารางไม่เปลี่ยน (ยัง 3 แถว) ไม่มีข้อมูลค้างใน db | เส้นทางยกเลิก — isSaved() คืน false จึงไม่รีเฟรช และไม่มีอะไรเข้า db เลย |
| 6 | กด "เพิ่มพนักงาน" → sompong / พนักงานเสิร์ฟ / 1234 / พนักงาน → บันทึก | dialog ปิดเอง ตารางกลายเป็น 4 แถว | บันทึกสำเร็จ → isSaved() true → หน้าหลักรีเฟรชให้ |
| 7 | เพิ่มใหม่ด้วยชื่อ somchai (ซ้ำ) → บันทึก | กล่องแดง "บันทึกไม่สำเร็จ..." และ dialog ยังเปิดค้าง ข้อมูลที่พิมพ์ยังอยู่ | พฤติกรรมสำคัญ: กรอกผิดไม่ปิดทิ้ง ผู้ใช้แก้แล้วบันทึกซ้ำได้ทันที (แก้ชื่อเป็น sompong2 ลองบันทึกต่อได้เลย) |
| 8 | เลือกแถว somchai → "แก้ไข" → เปลี่ยนตำแหน่งเป็น "หัวหน้ากะ" → บันทึก | dialog เปิดมาเติมค่าเดิมพร้อม (ภาพด้านล่าง) บันทึกแล้วตารางอัปเดต | โหมดแก้ไขผ่าน constructor ตัวที่สอง — ไม่มี editingId ให้สับสนแล้ว |
| 9 | เลือกแถว somjai → "ลบ" | กล่องเหลือง "ลบบัญชีที่กำลังใช้งานอยู่ไม่ได้" | กฎธุรกิจเดิมทำงานเหมือนเดิม |
| 10 | เลือกแถว sompong → "ลบ" → Yes | ยืนยัน → ตารางกลับ 3 แถว | ปิดจบข้อมูลทดสอบ db กลับสภาพเดิม |
JOptionPane ตัวเดิม ไม่เกี่ยวกับการรื้อ
เช็ค Letos ต้องเหลือ 3 แถวเดิม · ปิดโปรแกรม · ลองเปิดโปรเจกต์ MoogataLab เดิม รันเทียบ — ทั้งสองโปรเจกต์ต้องยังใช้งานได้ครบ = เรามีระบบ 2 รูปแบบบนหลังบ้านชุดเดียวกัน
| ถ้าข้อนี้ไม่ผ่าน | ปัญหาอยู่ที่ | ย้อนไปดู |
|---|---|---|
| 1–3 | ไม่เกี่ยวกับการรื้อ — สำเนา/หน้า login/กฎสิทธิ์ | Step 9 (คัดลอก) · Step 4 (login) · Step 6 (กฎสิทธิ์) ของส่วนที่ 1 |
| 4 (ไม่เลือกแถวแล้วไม่เตือน) | เงื่อนไข row == -1 ใน btnEdit/btnDelete | Step 11 (โค้ดชุดใหม่) |
| 5 (ยกเลิกแล้วข้อมูลเข้า / ตารางเด้ง) | isSaved() หรือเงื่อนไข if (dialog.isSaved()) | Step 10 (btnOK/btnCancel) · Step 11 (btnAdd) |
| 6–8 (บันทึก/แก้ไข) | btnOK สองโหมด / constructor overload | Step 10 |
| 7 (กรอกผิดแล้ว dialog ปิดทิ้ง) | ลืม return; ในสาย error ของ btnOK | Step 10 |
| 9–10 (ลบ) | กฎธุรกิจ / ยืนยัน | Step 11 (btnDelete) |
| # | อาการ | สาเหตุ / วิธีแก้ |
|---|---|---|
| 1 | no such table: employee | โปรแกรมเปิด db ผิดไฟล์ — moogata.db ต้องอยู่รากโปรเจกต์ข้าง ๆ pom.xml (สำเนา MoogataLabDialog ต้องมีไฟล์ db ของตัวเองด้วย — คัดลอกมาพร้อมโฟลเดอร์แล้ว) |
| 2 | ภาษาไทยบนหน้าจอเป็นกล่อง □□□ | ฟอนต์ไม่รองรับไทย — Windows: ตั้ง Leelawadee UI หรือ Tahoma · macOS: Thonburi · Linux: Noto Sans Thai / Garuda (อย่างน้อย lblHeader ที่ตั้ง font เอง) |
| 3 | คลิกแถวแล้วฟอร์มไม่เติม / ล้างฟอร์มแล้วโปรแกรมคว่ำ (เฉพาะแบบฟอร์มรวม) | ลืมบรรทัดผูก listener ใน constructor (getSelectionModel().addListSelectionListener(this::tblEmployeeValueChanged); — JTable ไม่มีเมนู Events นี้ให้ ต้องพิมพ์ผูกเอง) หรือลืม guard if (row == -1) return; บนสุดของ handler |
| 4 | บันทึก/ลบแล้วตารางไม่อัปเดต | ลืมเรียก showEmployees() หลังบันทึกสำเร็จ / หลัง dialog.isSaved() — ตารางไม่รู้เองว่าฐานข้อมูลเปลี่ยน |
| 5 | แก้ไขแล้วกลายเป็น "เพิ่มแถวใหม่" (เฉพาะแบบฟอร์มรวม) | ลืม editingId — คลิกแถวต้องเก็บ editingId = emp.getId() และตอนบันทึกต้องเช็ค editingId > 0 ก่อนเลือก update/save |
| 6 | UNIQUE constraint failed: employee.employee_name | ชื่อล็อกอินซ้ำ — constraint ทำงานไม่ใช่บั๊ก (save คืน null → หน้าจอโชว์กล่องแดง) |
| 7 | dialog ปิดแล้วหน้าจอหลักค้าง / ไม่ล็อกหน้าจอหลัก | เรื่อง modal — constructor ต้องส่ง true ตัวที่สาม: super(owner, title, true) |
| 8 | กดปุ่มแล้วเงียบ | event ไม่ได้ผูก — ต้องดับเบิลคลิกปุ่มใน designer เพื่อสร้าง handler (ปุ่มใหม่ใน Step 11 คือ btnAdd และ btnEdit ทั้งคู่) |
| 9 | cannot find symbol: class EmployeeDialog/EmployeeFrame | สร้างคลาสคนละ package — ทุกหน้าจอต้องอยู่ com.example.moogata.ui และ import ให้ครบ |
| 10 | เปิด MoogataLabDialog แล้ว NetBeans งงกับโปรเจกต์เดิม / เห็นชื่อเดียวกันสองตัว | ลืมแก้ pom.xml ของสำเนา — ต้องเปลี่ยนทั้ง <artifactId> และ <name> (Step 9 ข้อ 3) |
| 11 | ข้อความไทยในตาราง/ปุ่มเพี้ยนเป็นตัวประหลาด (แต่ไม่ใช่กล่อง □) | คนละปัญหากับข้อ 2 — นี่คือ encoding ของไฟล์: เช็ค pom.xml ต้องมี <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> (โปรเจกต์จาก Lab Week 10 มีให้แล้ว — เจอตอนสร้าง pom ใหม่เอง) |
| 12 | database is locked ตอนเพิ่ม/แก้/ลบจากโปรแกรม | เปิด moogata.db ค้างใน Letos แล้วมี transaction ยังไม่ commit — กลับไปกด Commit (Ctrl+Enter) ในแท็บ Data ของ Letos หรือปิด Letos แล้วรันโปรแกรมใหม่ |
| 13 | รันครั้งแรก (Shift+F6) แล้ว fail หรือค้างที่ดาวน์โหลด | Maven ต้องดาวน์โหลด plugin (เช่น exec-maven-plugin) รอบแรก — ต้องมีอินเทอร์เน็ต ถ้าห้องแล็บติด proxy ให้ตั้งค่า Maven ก่อน หรือรันให้ cache ไว้ที่บ้านมา |
| 14 | ลบ handler แล้ว cannot find symbol หรือ compile ไม่ผ่าน | การผูก event ยังค้างใน guarded block — ถอดผ่าน designer ก่อน (คลิกขวาปุ่ม → Events → เอาติ๊กออก) แล้วค่อยลบเมธอด ดู Step 11 · เปลี่ยนชื่อคลาสให้ใช้ Refactor → Rename เท่านั้น (ไฟล์ .form จะตามไปด้วย) |
git log --oneline ทั้งสอง repo)TestEmployeeDao, TestEmployeeService) ยังผ่านทั้งสองโปรเจกต์เริ่ม lab นี้ด้วย: ระบบที่ทำงานได้แต่ต้องใช้ผ่านคอนโซล — นักพัฒนาใช้ได้ พนักงานร้านใช้ไม่ได้
สิ่งที่เกิดขึ้นตลอด 12 Step: เราแก้ของเก่าแค่ไฟล์เดียว (EmployeeService เปิดบริการ CRUD) แล้วสร้างหน้าจอใหม่ทั้งหมด 2 ชุดบนหลังบ้านชุดเดียวกัน — ชุดแรกให้ทุกอย่างอยู่หน้าต่างเดียว (ฟอร์มบน ตารางล่าง) ต้องคุม "โหมดเพิ่ม/แก้ไข" เองด้วยสถานะและ listener · ชุดที่สองคัดลอกงานเดิมมารื้อ ย้ายฟอร์มไปเป็น dialog ทำให้หน้าหลักเหลือตารางกับปุ่ม ไม่มีสถานะซ่อนอยู่เลย
บทเรียนที่ควรติดตัวออกจาก lab:
service เท่านั้น — จึงสลับรูปแบบหน้าจอได้โดยไม่แตะ SQL เลยสักบรรทัดshowEmployees()) ไม่ใช่แก้ตารางตรง ๆเรื่องต่อไป: ระบบ POS เต็มของร้าน (pos/) ก็ใช้โครงเดียวกันนี้ — เพียงแต่เลเยอร์ UI เป็นหน้าจอเว็บ
และมีตารางจริงหลายตาราง (โต๊ะ บิล โปรโมชัน สมาชิก) ที่ต่อกันด้วย FK ตามที่เคยเรียน — สิ่งที่ฝึกมาทั้ง 2 lab คือเสาหลักของมัน
labs/week11-employee-ui-continue/README.mdlabs/week11-employee-ui-dialog/README.mdlabs/week11-user-ui/README.mdteaching-sequence.md