JTable: ข้อมูลหลายชนิดใน Column เดียวกัน

สมชัย หลิมศิโรรัตน์
2 พฤษภาคม 2546

7/05/2546 : มอบบทความนี้ให้เว็บ Jarticles.com นำไปเผยแพร่
26/10/2546: ปรับปรุงเป็น v1.3 เพื่อเผยแพร่ที่เว็บ ThaiDev.org เปลี่ยน package เป็น org.thaidev
15/02/2547: ปรับปรุงเป็น v1.4 แก้ไข bug การกดคีย์ Enter เพื่อเลื่อนไปทำงานต่อที่ cell อื่นๆ ให้ทำงานได้เหมือน default ของ JTable

เกริ่นนำ

การใช้ javax.swing.JTable ในภาษา Java เก็บข้อมูลเป็นตารางนั้น นับว่าสะดวกมาก แต่ JTable ที่ทาง Sun ออกแบบไว้นั้นก็มีข้อจำกัด คือ จะต้องใส่ข้อมูลชนิดเดียวกัน ไว้ในคอลลัมน์เดียวกัน ข้อจำกัดอันนี้ ทำให้การสร้างโปรแกรม ที่แสดง Property ของข้อมูล คือ คอลลัมน์ที่แสดงค่าจะมีค่าได้หลากหลายชนิด ทำให้เรานำเอา JTable มาใช้งานตรงๆเลยไม่ได้ เมื่อหลายปีก่อน ผมได้พยายามทำความเข้าใจการทำงานของ JTable และคิดหาวิธีแก้ไขข้อจำกัดอันนี้ขึ้นมา แต่ก็ไม่ได้เผยแพร่ให้ใครได้รู้ จนมาถึงวันนี้ ผมได้มีโอกาสเข้าไปคุยในเว็บบอร์ด หลายแห่ง และพบว่ามีหลายคนที่มีประสพการณ์ดีๆ และมีอุดมการณ์ที่จะเผยแพร่ความรู้ โดยไม่ได้รับค่าตอบแทนใดๆ ผมจึงอยากจะเผยแพร่ความรู้อันนี้ของผมบ้าง แต่ก่อนจะเขียนบทความนี้ขึ้นมา ผมก็ได้สำรวจและพบบทความหนึ่งที่แก้ปัญหานี้ด้วยเหมือนกัน อยู่ที่ JavaWorld แต่วิธีการต่างจากของผม ซึ่งผมก็ได้เข้าไปคุยแลกเปลี่ยนความคิดเห็นในเว็บบอร์ดที่ Narisa.com เพื่อให้หลายๆคนได้วิจารณ์ว่าวิธีการที่ผมใช้ กับวิธีการของ JavaWorld มีข้อดีและข้อเสียอย่างไร ก็ได้รับความเห็นว่า วิธีของ JavaWorld นั้นยังมีข้อจำกัดอยู่ในกรณีที่มีข้อมูลมากๆ หลายๆ row อาจจะทำให้ประสิทธิภาพการทำงานตกลงได้ ซึ่งวิธีการของผม จะไม่มีปัญหาตรงนี้ ผมจึงคิดว่าควรจะเขียนเรื่องนี้เป็นบทความ ให้ทุกคนได้อ่านกัน

พื้นฐานความรู้

เนื่องจากบทความนี้ต้องการเน้นเฉพาะเรื่องการแก้ปัญหา กรณีข้อมูลหลายชนิดใน column เดียวของ JTable เท่านั้น ผู้อ่านควรจะมีพื้นความรู้การใช้งาน JTable เป็นอย่างดีมาก่อนแล้ว หากท่านยังไม่มี แต่สนใจเรื่องนี้ ผมขอแนะนำให้อ่านวิธีการใช้งาน JTable ในหัวข้อ How to use Tables จากหนังสือ The Java Tutorial ซึ่งเป็นการรวมหนังสือ 3 เล่มไว้ที่เดียวกัน และมีไฟล์ให้ดาวโหลดมาอ่านได้ฟรีด้วยครับ

ข้อจำกัด

การใช้งาน JTable นั้น ถูกออกแบบมาในลักษณะของ MVC (Model-View-Controller) ซึ่งมี javax.swing.table.TableModel เป็น interface สำหรับให้เราสร้าง model ของข้อมูลแบบของเราเองได้ ข้อจำกัดของ JTable ก็เกิดจากการที่ TableModel นั้น ถูกออกแบบมาให้มี method getColumnClass(int col) เพื่อใช้ตรวจสอบชนิดของข้อมูลในแต่ละ column แต่ไม่มี method ใดๆสำหรับตรวจสอบชนิดของข้อมูลในแต่ละ cell หรือ row เลย การ implement ใน javax.swing.table.AbstractTableModel ซึ่งเป็นคลาสแม่ของ javax.swing.table.DefalutTableModel นั้น เขียน method getColumnClass ไว้ดังนี้

    public Class getColumnClass(int columnIndex) {
        return Object.class;
    }

เราจะไม่สามารถใช้ method นี้แก้ปัญหาใดๆได้ เพราะ parameter ของ method นี้มีเพียงตัวเดียวคือหมายเลข column หากต้องการระบุว่าเป็นข้อมูลที่ cell ไหน จะตัองระบุทั้ง column และ row

ใน JTable จะมี method getCellEditor(int row, int col) และ getCellRenderer(int row, int col) ซึ่งเป็น method สำหรับหาตัววาด/แก้ไข cell นั้นๆมา ใน method ทั้งสองอันนี้ จะเรียกใช้ method getColumnClass(int col) เพื่อหาชนิดของข้อมูลมา แล้วใช้ method getDefaultEditor/Renderer เพื่อหา Editor/Renderer มา

นี่คือ source code บางส่วนของ JTable คัดลอกมาเฉพาะส่วนที่เกี่ยวข้อง

    public Class getColumnClass(int column) {
        return getModel().getColumnClass(convertColumnIndexToModel(column));
    }

    public TableCellEditor getCellEditor(int row, int column) {
        TableColumn tableColumn = getColumnModel().getColumn(column);
        TableCellEditor editor = tableColumn.getCellEditor();
        if (editor == null) {
            editor = getDefaultEditor(getColumnClass(column));
        }
        return editor;
    }

    public TableCellRenderer getCellRenderer(int row, int column) {
        TableColumn tableColumn = getColumnModel().getColumn(column);
        TableCellRenderer renderer = tableColumn.getCellRenderer();
        if (renderer == null) {
            renderer = getDefaultRenderer(getColumnClass(column));
        }
        return renderer;
    }

ทางแก้

ในบทความของ JavaWorld นั้นแก้ปัญหานี้ด้วยการ override method getCellEditor ของ JTable ให้ไปค้นหา Editor ตาม row ที่กำหนด ซึ่งจะมี RowEditorModel ที่สร้างขึ้นมาใหม่ ให้เป็นตัวจัดการเก็บ Editor ไว้และเพิ่มเติม method สำหรับ set/getRowEditorModel เข้ามา แต่วิธีการของผมนั้น ผมจะเพิ่ม method getCellClass(int row, int column) ซึ่งมี row เพิ่มเข้ามา และใน JTable จะ override ให้ getCellEditor และ getCellRenderer ให้เรียกใช้ getCellClass ตัวนี้แทน ดังนี้

    public class CellIndependentTable extends JTable { 
        // ... constructor

        public Class getCellClass(int row,int col) { 
            Object obj = getModel().getValueAt(row,col); 
if (obj != null) return obj.getClass(); else return Object.class;
}
        public TableCellEditor getCellEditor(int row, int column) { 
TableColumn tableColumn = getColumnModel().getColumn(column);
TableCellEditor editor = tableColumn.getCellEditor();
if (editor == null) {
editor = getDefaultEditor(getCellClass(row,column));
}
return editor;
}
public TableCellRenderer getCellRenderer(int row, int column) { TableColumn tableColumn = getColumnModel().getColumn(column); TableCellRenderer renderer = tableColumn.getCellRenderer(); if (renderer == null) { renderer = getDefaultRenderer(getCellClass(row,column)); } return renderer; } }

จะทำให้ได้ชนิดของข้อมูลจริงๆที่ตำแหน่ง cell นั้นๆเลย และจะทำให้เราสามารถแสดงข้อมูลได้อย่างอิสระ คือ เป็นไปตามชนิดของข้อมูลจริงที่อยู่ใน cell นั้นๆ

ตัวอย่าง

ผมได้ทำตัวอย่างการใช้งานเป็น Property Editor ขึ้นมาซึ่งเป็นเป้าหมายหลักของปัญหานี้ โดย source code ทั้งหมดนั้นประกอบด้วยหลายไฟล์ เพราะผมได้สร้าง Editor/Renderer ของข้อมูลหลายๆแบบไว้ด้วย จึงขอไม่นำมาลงในบทความนะครับ กรุณาดาวโหลดตามลิ้งค์ท้ายบทความ นะครับ

Editor/Renderer ที่สร้างไว้ได้แก่

  • Array (จะใช้ CellIndependentTable อีกตัวหนึ่งแสดง และแก้ไขได้)
  • Boolean (แสดงเป็น Combo Box ให้เลือก True หรือ False เท่านั้น)
  • Color (เปิด JColorChooser เพื่อเลือกสีได้)
  • File Name (เปิด JFileChooser เพื่อเลือกไฟล์ได้)
  • Number (สำหรับข้อมูลที่เป็นตัวเลขชนิดใดก็ได้ ไม่ว่าจะเป็น Integer, Float, Double หรืออื่นๆ)
  • String (สำหรับข้อมูล String ทั่วไป)

หมายเหตุ: เนื่องจากผมคิดว่า การแสดงผล cell ที่แก้ไขไม่ได้ ควรจะแสดงสีที่จางลง ทำให้อาจจะไม่ตรงกับความต้องการของผู้ใช้ในบางกรณีครับ

จากโปรแกรมตัวอย่าง org.vsl.examples.TestCITable การนำ CellIndependentTable มาใช้งานก็ง่ายๆคือ สร้าง TableModel สำหรับข้อมูลที่ต้องการ แล้วส่งให้ตอนที่สร้างตาราง เช่น

    table = new CellIndependentTable(new DefaultTableModel(data1,colname1));

ตัวแปร data1 และ colname1 นั้นเก็บข้อมูล และชื่อคอลลัมน์ตามที่ต้องการ ซึ่งภาษา Java มีความสามารถสูงมากตรงที่เราสามารถสร้าง array ของ Object ใดๆได้ ผลลัพธ์ที่ได้ ดังภาพแรก เป็นการแสดงให้เห็นว่าสามารถใช้ Renderer แสดงของข้อมูลต่างชนิดกันได้ ซึ่งในตัวอย่างนี้มีข้อมูลชนิดต่างๆคือ

  • ID เป็น Integer
  • Weight เป็น Float
  • File เป็น class ที่ผมสร้างขึ้นเองชื่อ FileName ไว้สำหรับเก็บชื่อไฟล์
  • Single เป็น Boolean
  • Hair Color เป็น Color
  • Name และ Family Name เป็น String

เมื่อคลิกเพื่อแก้ไขข้อมูล ก็จะมีตัว Editor มาโต้ตอบกับเรา ขึ้นอยู่กับชนิดของข้อมูลในช่องนั้นๆ เช่น ภาพที่สองเลือกแก้ไข field ข้อมูล Single ซึ่งเป็นข้อมูลแบบ Boolean ผมได้ใช้ JComboBox ให้เลือกคำว่า True/False

ภาพที่สาม เลือกแก้ไข File ซึ่งเป็นข้อมูลแบบที่ผมสร้างเอง คือ class FileName โดย Editor จะผสมระหว่าง JTextField กับ JButton คือจะป้อนชื่อไฟล์เองหรือ กดที่ปุ่มเพื่อเปิดหน้าต่าง JFileChooser ก็ได้นะครับ

ภาพสุดท้าย จะเป็นการแสดงให้เห็นว่า เราสามารถใช้ table ตัวเดิม รองรับข้อมูลแบบอื่นๆได้ทันที เพียงแค่สั่ง setTableModel() ใหม่ให้กับตัว table นั้น ซึ่งผมได้ทำปุ่ม "Change Data" ไว้ เมื่อกด ก็จะเปลี่ยนข้อมูลที่ผมเตรียมไว้ แสดงให้เห็นว่า ข้อมูลจะเป็นอะไรก็ได้ อยู่ที่ cell ไหนก็ได้ เหมือน Spread Sheet เลย

API Document

นอกจากนี้ ผมได้พยายามเขียน API Document เป็นภาษาไทย เพื่อให้ท่านอ่านและทำความเข้าใจได้สะดวกขึ้นด้วย

ขอขอบคุณ

  1. คุณวีรศักดิ์ วิทวัสกุล หรือ นายข้าวโพดหวาน (oop guy) ผู้ดูแลเว็บบอร์ด ในส่วนของ Java & Object Oriented Developmemt ของเว็บ Narisa.com ที่ปรับปรุงโปรแกรมตัวอย่างรุ่นแรกให้เป็น jar ไฟล์ที่พร้อมรันได้เลย
  2. คุณ pharter สมาชิกท่านหนึ่งของ Narisa.com ที่รายงาน bug การป้อนข้อมูล ที่เมื่อป้อนเสร็จ กดปุ่ม Enter ก็จะกระโดดไปทำงานที่ cell ถัดไป

สรุป

เทคนิคของผมนี้ จะช่วยให้เราสร้างโปรแกรมที่ทำงานกับข้อมูลแบบตารางได้อย่างไร้ขีดจำกัดอีกต่อไป ซึ่งอาจจะนำไปปรับปรุงทำเป็นโปรแกรม Spread Sheet ก็ได้ นอกจากนี้ในโปรแกรมตัวอย่างนี้ยังมีเทคนิคการสร้าง Editor/Renderer แบบต่างๆอีกมากมายนะครับ

ไฟล์

  1. CITable1.4.jar - แก้ไข bug การกดคีย์ Enter เพื่อเลื่อนไป cell อื่นๆ ให้ทำงานได้เหมือน default ของ JTable
  2. CITable1.3.jar - ปรับปรุงโดยตัด CellIndependentTableModel ออก, จัด package ใหม่ ให้อยู่ใน org.thaidev.examples.CITable และ org.thaidev.lib.swing.table และปรับปรุงส่วนอื่นๆ
  3. CITable1.2.jar - jar package และ source code ที่ปรับปรุงโดยคุณวีรศักดิ์ วิทวัสกุล
  4. CITableTest.zip - source code ต้นฉบับ version 1.1

since September 2002
Web Counter by http://www.digits.com
Last updated : Saturday, 27 March, 2004 1:28

Copyright © 2002-2004 Somchai LIMSIRORATANA. All rights reserved.