2014年5月31日 星期六

經方與溫病

所謂經方是指中醫裡以東漢張仲景的傷寒論與金匱要略等著作為處方根據所產生的派別, 數年前由於旅美的倪海廈大力宏揚而獲得許多中醫學習者崇拜. 我讀過其網站部份文章, 感覺其人甚為狂傲, 對溫病學派不屑一顧, 但批評西醫部份我倒是頗為認同.

今日看到下面這篇 :

點到倪海廈經方家(經方派)就滿足,真是很可惜

此君看法值得參考, 蓋疾病有多種樣態, 無法一劑治百病. 我以前也認為葉天士的溫病學派所處之方藥效柔弱, 造成近世之人對中醫有著效果遲緩不彰的印象. 經方敢用剛強威猛之劑 (例如附子), 故若對症的話, 藥效速, 驗而有徵. 但若誤用, 那可會要命. 況且溫病派的諸醫家就是用經方治溫病無效或加重其病, 才發展出溫病學的不是嗎?

仰望蒼穹, 知人類之渺小. 即使是醫宗聖手, 亦無法起死回生. 人雖為萬物之靈, 其智亦有所限. 知知之無涯, 故為學應以謙卑為懷. 真正學有專長者皆虛懷若谷.


2014年5月28日 星期三

關於自然語言處理 (NLP)

計算語言學與自然語言處理 (Natural Language Processing) 息息相關, 機器翻譯沒有它也是枉然. 目前做 NLP 套件支援最好的是 Python, 歐萊里 (2009) 出版了一本 "Natural Language Processing with Python", 使用 NLTK 函式庫與語料庫, 是不可多得的好書.

Java 也有 NLP 套件, 例如 OpenNLP 就不錯.

我書櫃中與語音分析, 句法分析自動化技術有關的書有下列幾本 :
  1. 計算語言學與漢語自動分析 (侯敏, 北京廣播學院出版社 1999)
  2. Visual Basic 與語音辨識 (楊鎮光, 松崗 2002)
  3. 語言, 語音與技術 (王士元, 彭剛, 香港城市大學出版社 2007)
  4. 機器翻譯研究 (馮志偉, 中國對外翻譯出版社 2004)
下列幾本原文書非常棒 :
  1. SPEECH and LANGUAGE PROCESSING (2nd) (MIT 教科書)
  2. Foundations of Statistical Natural Language Processing (1999)
  3. Text Processing in Python 
參考 :

http://www.nltk.org/
统计自然语言理解的两本书
用Python也能輕鬆玩自然語言處理(序言+1.1)
Is there a good natural language processing library
# MIT : Natural Language Processing
Python Big Data Analytics & Stock Algorithmic (PART 1)
# Python Big Data Analytics & Stock Algorithmic (PART 2)


Big Data 與計算語言學

上週跟市圖借了一本巨量資料分析的書 "Big Data-讓你看見真實慾望 (悅知文化出版)", 這是南韓人寫的書, 全書主要內容集中於觀念於看法, 技術方面大概只有第一章末尾談到巨量資料分析的過程有提到, 其實也就是兩張關於句法分析的流程圖而已, 但這兩張圖卻重燃我對計算語言學的興趣.

計算語言學屬於邊緣學科, 牽涉範圍比聲學語音學還深還廣, 除了以傳統語言學中的構詞句法學, 語意學, 形式語言學等為基礎外, 現在主要發展動力來自於 IT 技術的突破, 特別是人工智慧與類神經系統理論與實作技術的躍進.

前陣子在 Youtube 上看到會聊天的導航機, 雖然爆笑的對話很可能是人為操控的, 但這讓我想起以前讀書的時候很受歡迎的霹靂遊俠影集, 那台霹靂車上的電腦 "夥計" 彷彿是一個虛擬智慧, 不但會自動操控主人的霹靂車, 還像朋友一樣會跟主人聊天出主意.

[東森新聞]超爆笑導航 ! 能對話還是蔡小虎粉絲

其實在人工智慧領域早就有 Chatbot/Chatterbot 這個構想了, 被奉為 "人工智慧之父" 的英國數學家涂林 (Turing) 於 1950 年代提出了一個論點, 認為電腦若能跟人對話, 表示具有思考能力 (涂林測試). 涂林對現代電腦科學與人工智慧貢獻極大, 二戰時也協助盟軍破解德軍密碼, 對扭轉戰局居功厥偉. 可惜英年早逝, 享年 41 歲.  美國企業家勒布納還設立了勒布納人工智慧大賞來促進此技術的發展. 獲得 2013 年勒布納大賞的網路聊天機器人水谷 (Mitsuku) 就是其中的佼佼者.

我實際測試了水谷, 發現她比 Cleverbot 反應快, 而且回答的內容比較擬真. 但是還不夠理想, 還感覺得出機器味, 例如我重複問她 : "Are you married? Can you be my girl friend?", 她都千篇一律回答 "No, I am single. No, we are just friends. I am happy being sigle." 這不像人類女孩的回答啊! 她至少應該說 "You have asked this question twice." 才對. 畢竟還是程式運作的結果, 我理想中的人工智慧機器人是具有意識引擎的, 也就是具有記憶, 情緒, 與思考能力的, 這樣才能稱為智慧呀.

當兩個聊天機器人進行對話時... 「沒輸沒贏,笑到不行」
Mess with your friend using an AOL Instant Messenger "bot"
# Wiki : Chatterbot
從聊天機器人到「分析黑洞」
http://www.cleverbot.com/
# 聊天機器人水谷 會答各種怪問題
http://www.mitsuku.com/ (水谷)


2014年5月27日 星期二

Java Swing 測試 : 表格 JTable (續)

Swing 的 JTable 要測試的項目有點多, 加上我為了保存測試資料, 以便往後寫專案參考, 原始碼全部貼上去, 所以文章實在太長, 只好切割為續集, 前一篇文章參考 :

# Java Swing 測試 : 表格 JTable
# http://docs.oracle.com/javase/7/docs/api/

此篇要繼續紀錄我的 JTable 測試, 這些測試主要根據 Oracle 的 Java Tutorial 以及下面幾本書的範例綜合改寫而成 :
  1. 精通 Java Swing 程式設計 (林智揚, 金禾 2001)
  2. Java Swing 進階篇 (歐萊里)
  3. JFC Swing 教學手冊第二版 (碁峰)
  4. JBuilder 8.0 JFC and Swing programming
關於 JTable 的選取模型 SelectionModel 如上一篇所述, 在 JTable 上選取資料的模式有三種 : 單選, 單一連續區間, 多重連續區間共三種選取模式, 由 ListSelectionModel 介面的三個靜態屬性定義 (整數常數) :
  1. ListSelectionModel.SINGLE_SELECTION (=0)
  2. ListSelectionModel.SINGLE_INTERVAL_SELECTION (=1)
  3. ListSelectionModel.MULTIPLE_INTERVAL_SELECTION (=2, 預設)
此介面也定義了十幾個方法以操作項目之選取. JTable 有一個 selectionModel 屬性用來記錄使用者的選取動作. 而呼叫 getSelectionModel() 方法則會傳回 JTable 所實作的 SelectionModel 物件. 欲更改 JTable 的選取模式, 就必須呼叫此 SelectionModel 物件的 setSelectionMode() 方法, 傳入上述的選取模式常數即可. 如果要攔截表格的選取動作, 則必須將此 ListSelectionModel 物件加入 ListSelectionListener 加以監聽, 監聽者必須實作 valueChanged() 方法.

JTable 中與選取有關的方法如下 :
  1. setCellSelectionEnabled() / getCellSelectionEnabled() :
    設定/傳回儲存格是否為可選取狀態 (boolean), 預設為 false.
  2. setRowSelectionAllowed() / setColumnSelectionAllowed() :
    設定列 / 行 (欄) 為可選取狀態 (boolean).
  3. getRowSelectionAllowed() / getColumnSelectionAllowed() :
    設定列 / 行 (欄) 為可選取狀態 (boolean)
  4. setSelectionMode() / getSelectionMode() :
    設定/傳回表格的選取模式 (int), 預設為多重區間選取.
  5. setSelectionModel()  / getSelectionModel() :
    設定 / 傳回表格的 ListSelectionModel 物件. 
  6. getSelectedRows() / getSelectedColumns() :
    傳回選取之列元素 / 行元素 (欄) 索引陣列 (int[])
  7. getSelectedRow() / getSelectedColumn() :
    傳回選取之第一個列元素 / 行元素 (欄) 索引 (int), 若未選取傳回 -1. 
  8. getSelectedRowCount() / getSelectedColumnCount() :
    傳回選取之列元素 / 行元素 (欄) 個數 (int).
  9. setSelectionBackground() / getSelectionBackground()
    設定 / 傳回表格的選取背景色 (Color 物件). 
  10. setSelectionForeground() / getSelectionForeground()
    設定 / 傳回表格的選取前景色 (Color 物件).
在下面範例中, 我把 SelectionMode, CellSelection, RowSelection, ColumnSelection 的設定做成選項功能表, 並在視窗下方 (SOUTH) 放置 JLabel 來顯示設定值. 在視窗上方則放置顯示選取儲存格內之值 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class JTable9 implements ActionListener,ListSelectionListener {
  JFrame f;
  JTable jt;
  ListSelectionModel lsm;
  JLabel lb_mode;
  JLabel lb_cell;
  JLabel lb_row;
  JLabel lb_column;
  JLabel lb_selected;
  public static void main(String argv[]) {
    new JTable9();
    }
  public JTable9() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    MyTableModel mtm=new MyTableModel();
    jt=new JTable(mtm);
    jt.setPreferredScrollableViewportSize(f.getSize()); //與 JFrame 相同大小
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    //Add menu bar
    JMenuBar mb=new JMenuBar();
    JMenu jtable=new JMenu("JTable");
    //selection mode 製作功能表
    JMenu selectionMode=new JMenu("SelectionMode");
    JRadioButtonMenuItem[] mode=new JRadioButtonMenuItem[3];
    mode[0]=new JRadioButtonMenuItem("SINGLE_SELECTION(0)");
    mode[1]=new JRadioButtonMenuItem("SINGLE_INTERVAL_SELECTION(1)");
    mode[2]=new JRadioButtonMenuItem("MULTIPLE_INTERVAL_SELECTION(2)");
    ButtonGroup modegroup=new ButtonGroup();
    for (int i=0; i<mode.length; i++) {
      selectionMode.add(mode[i]);
      mode[i].addActionListener(this);
      if (i==2) {mode[i].setSelected(true);} //預設顯示 MULTIPLE
      }
    jtable.add(selectionMode);
    //cell selection
    JMenu cellSelection=new JMenu("Cell Selection");
    JRadioButtonMenuItem[] cell=new JRadioButtonMenuItem[2];
    cell[0]=new JRadioButtonMenuItem("setCellSelectionEnabled(true)");
    cell[1]=new JRadioButtonMenuItem("setCellSelectionEnabled(false)");
    ButtonGroup cellgroup=new ButtonGroup();

    for (int i=0; i<cell.length; i++) {
      cellSelection.add(cell[i]);
      cell[i].addActionListener(this);
      if (i==1) {cell[i].setSelected(true);}
      }
    jtable.add(cellSelection);
    //row selection
    JMenu rowSelection=new JMenu("Row Selection");
    JRadioButtonMenuItem[] row=new JRadioButtonMenuItem[2];
    row[0]=new JRadioButtonMenuItem("setRowSelectionAllowed(true)");
    row[1]=new JRadioButtonMenuItem("setRowSelectionAllowed(false)");
    ButtonGroup rowgroup=new ButtonGroup();

    for (int i=0; i<row.length; i++) {
      rowSelection.add(row[i]);
      row[i].addActionListener(this);
      if (i==1) {row[i].setSelected(true);} //顯示預設值
      }
    jtable.add(rowSelection);
    //row selection
    JMenu columnSelection=new JMenu("Column Selection");
    JRadioButtonMenuItem[] column=new JRadioButtonMenuItem[2];
    column[0]=new JRadioButtonMenuItem("setColumnSelectionAllowed(true)");
    column[1]=new JRadioButtonMenuItem("setColumnSelectionAllowed(false)");
    ButtonGroup columngroup=new ButtonGroup();

    for (int i=0; i<column.length; i++) {
      columnSelection.add(column[i]);
      column[i].addActionListener(this);
      if (i==1) {column[i].setSelected(true);}
      }
    jtable.add(columnSelection);
    mb.add(jtable);
    f.setJMenuBar(mb);
    //set status
    lsm=jt.getSelectionModel();
    lsm.addListSelectionListener(this);  //選項模式物件向本物件註冊監聽
    JPanel status=new JPanel(new GridLayout(1,4));
    lb_mode=new JLabel("mode=" + lsm.getSelectionMode());
    lb_cell=new JLabel("cell=" + jt.getCellSelectionEnabled());
    lb_row=new JLabel("row=" + jt.getRowSelectionAllowed());
    lb_column=new JLabel("column=" + jt.getColumnSelectionAllowed());
    status.add(lb_mode);
    status.add(lb_cell);
    status.add(lb_row);
    status.add(lb_column);
    cp.add(status,BorderLayout.SOUTH);
    lb_selected=new JLabel("Selected : ");
    cp.add(lb_selected,BorderLayout.NORTH);
    f.setVisible(true);

    //Close JFrame      
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f));
    }
  public void actionPerformed(ActionEvent e) {  //設定模式與屬性
    String cmd=e.getActionCommand();
    if (cmd.equals("SINGLE_SELECTION(0)")) {
      lsm.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
      lb_mode.setText("mode=" + lsm.getSelectionMode());
      }
    if (cmd.equals("SINGLE_INTERVAL_SELECTION(1)")) {
      lsm.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
      lb_mode.setText("mode=" + lsm.getSelectionMode());
      }
    if (cmd.equals("MULTIPLE_INTERVAL_SELECTION(2)")) {
      lsm.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
      lb_mode.setText("mode=" + lsm.getSelectionMode());
      }
    if (cmd.equals("setCellSelectionEnabled(true)")) {
      jt.setCellSelectionEnabled(true);
      lb_cell.setText("cell=true");
      }
    if (cmd.equals("setCellSelectionEnabled(false)")) {
      jt.setCellSelectionEnabled(false);
      lb_cell.setText("cell=false");
      }
    if (cmd.equals("setRowSelectionAllowed(true)")) {
      jt.setRowSelectionAllowed(true);
      lb_row.setText("row=true");
      }
    if (cmd.equals("setRowSelectionAllowed(false)")) {
      jt.setRowSelectionAllowed(false);
      lb_row.setText("row=false");
      }
    if (cmd.equals("setColumnSelectionAllowed(true)")) {
      jt.setColumnSelectionAllowed(true);
      lb_column.setText("column=true");
      }
    if (cmd.equals("setColumnSelectionAllowed(false)")) {
      jt.setColumnSelectionAllowed(false);
      lb_column.setText("column=false");
      }
    }
  public void valueChanged(ListSelectionEvent e) { //觸發選取事件後執行
    int[] rows=jt.getSelectedRows();  //取得選取列上之儲存格
    int[] columns=jt.getSelectedColumns();  //取得選取欄上之儲存格
    StringBuilder msg=new StringBuilder("Selected : ");
    for (int i=0; i<rows.length; i++) {
      for (int j=0; j<columns.length; j++) {
        msg.append(jt.getValueAt(rows[i],columns[j]).toString());
        }
      }
    lb_selected.setText(msg.toString());
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }  
  }
class MyTableModel extends AbstractTableModel {
  Object[][] data={
    {"Kelly","Female",new Integer(16),false,"kelly@gmail.com"},
    {"Peter","Male",new Integer(14),false,"peter@gmail.com"},
    {"Amy","Female",new Integer(12),false,"amy@gmail.com"},
    {"Tony","Male",new Integer(49),true,"tony@gmail.com"},
    {"John","Male",new Integer(23),true,"john@gmail.com"},
    {"Eva","Female",new Integer(19),false,"eva@gmail.com"},
    {"Rebeca","Female",new Integer(9),false,"rebeca@gmail.com"}};
  String[] columns={"Name","Gender","Age","Vegetarian","E-mail"};
  public int getColumnCount() {return columns.length;}
  public int getRowCount() {return data.length;}
  public Object getValueAt(int row, int col) {return data[row][col];}
  public String getColumnName(int col) {return columns[col];}
  public Class getColumnClass(int col) {
    return getValueAt(0,col).getClass();
    }
  public boolean isCellEditable(int row,int col) {return true;}
  public void setValueAt(Object value,int row,int col) {
    data[row][col]=value;
    fireTableCellUpdated(row,col);
    }
  }



可見在不同選取模式下, 儲存格與行列的選取行為會不同, 選取的儲存格值會串接後顯示在上方 JLabel 內. 此例實作了 ActionListener 與 ListSelectionListener, 當選取表格內的行, 列或儲存格時, 就會觸發 ListSelectionEvent, 執行 valueChanged() 方法. 為了在 actionPerformed() 與 valueChanged() 中設定上下兩個狀態欄, JLabel 均宣告為類別成員.

接下來是測試要如何在 JTable 上新增與刪除行與列? 這要用到表格預設模型 DefaultTableModel 與 TableColumn 物件. 在 DefaultTableModel 類別中定義了下列方法來操作新增行列以及刪除列 :
  1. addColumn(Object columnName) :
    增加一欄到表格模型尾端, 但只有欄名.
  2. addColumn(Object columnName, Object[] columnData) :
    增加一欄到表格模型尾端, 包括欄名與資料.
  3. addColumn(Object columnName, Vector columnData)
    增加一欄到表格模型中, 包括欄名與資料 (使用 Vector).
  4. addRow(Object[] rowData)
    增加一列資料到表格模型的尾端.
  5. addRow(Vector rowData)
    增加一列資料到表格模型尾端 (使用 Vector).
  6. insertRow(int row, Object[] rowData)
    將資料插入指定的列.
  7. insertRow(int row, Vector rowData)
    將資料插入指定的列 (使用 Vector).
  8. removeRow(int row):
    刪除指定列資料.
  9. moveRow(int start, int end, int to)
    將連續數列資料移動到指定列.
可知刪除列較簡單, 呼叫 TableModel 物件的 removeRow() 即可. 但刪除欄較麻煩, 要先取得欄物件. DefaultTableModel 類別並未定義刪除欄的方法, 因為這是 TableColumn 介面所管理的. 此介面定義了 removeColumn() 方法來刪除欄. 呼叫 JTable 的 getColumnModel() 會傳回實作 TableColumnModel 介面之物件, 預設為 DeafaultTableColumnModel 物件. 呼叫 TableColumnModel 物件的 getColumn() 就會傳回 TableColumn 欄位物件, 就可以使用其 removeColumn() 方法來刪除欄了. 增加行列時 JTable 會自動管理行列數目, 但刪除行列時則必須自行管理 TableModel 的行列數, 範例如下 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import java.util.*;

public class JTable10 implements ActionListener {
  JFrame f;
  JTable jt;
  DefaultTableModel dtm;
  JLabel status;
  int cid=0;
  public static void main(String argv[]) {
    new JTable10();
    }
  public JTable10() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    dtm=new DefaultTableModel();
    jt=new JTable(dtm);
    jt.setPreferredScrollableViewportSize(f.getSize());
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    //setup button
    JPanel panel=new JPanel(new GridLayout(1,4));
    JButton addColumn=new JButton("增加行");
    addColumn.addActionListener(this);
    JButton removeColumn=new JButton("移除行");
    removeColumn.addActionListener(this);
    JButton addRow=new JButton("增加列");
    addRow.addActionListener(this);
    JButton removeRow=new JButton("移除列");
    removeRow.addActionListener(this);
    panel.add(addColumn);
    panel.add(removeColumn);
    panel.add(addRow);
    panel.add(removeRow);
    cp.add(panel,BorderLayout.NORTH);
    status=new JLabel(" ");
    cp.add(status,BorderLayout.SOUTH);
    f.setVisible(true);

    //Close JFrame      
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f));
    }
  public void actionPerformed(ActionEvent e) {
    String cmd=e.getActionCommand();
    if (cmd.equals("增加行")) {
      dtm.addColumn("行 " + cid);
      status.setText("已新增一行, 總行數=" + dtm.getColumnCount());
      ++cid;
      }
    if (cmd.equals("增加列")) {
      dtm.addRow(new Vector());  //增加空列 (Vector)
      status.setText("已新增一列, 總列數=" + dtm.getRowCount());
      }
    if (cmd.equals("移除行")) {
      int lastColumnID=dtm.getColumnCount()-1;
      if (lastColumnID >= 0) {
        TableColumnModel columnModel=jt.getColumnModel();  //取得表格欄模型
        TableColumn column=columnModel.getColumn(lastColumnID); //取得欄物件
        columnModel.removeColumn(column); //刪除欄物件
        dtm.setColumnCount(lastColumnID);  //更新表格模型之總欄數
        status.setText("已刪除最後一行, 總行數=" + dtm.getColumnCount());
        }
      }
    if (cmd.equals("移除列")) {
      int lastRowID=dtm.getRowCount()-1;
      if (lastRowID >= 0) {
        dtm.removeRow(lastRowID);
        dtm.setRowCount(lastRowID);
        status.setText("已刪除最後一列, 總列數=" + dtm.getRowCount());
        }
      }
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }
  }

此例中, 新增行或列時會在最後一行或列添加新行或新列. 這裡我們用上面編號五的 addRow() 方法, 加入一個空的 Vector 物件即可, Vector 類別放在 java.util 套件下, 所以必須匯入. 注意, 當沒有任何欄位時, 新增列雖然不會顯示儲存格, 但實際上有加入到模型中, 只要新增一行即顯現.

上例中, 新增或刪除行列時, 是新增刪除最後一行或最後一列, 是否可以新增於選取行列之後, 刪除選取之行列呢? 範例程式如下 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import java.util.*;

public class JTable11 implements ActionListener,ListSelectionListener {
  JFrame f;
  JTable jt;
  DefaultTableModel dtm;
  JLabel status;
  int cid=0;
  int rid=0;
  public static void main(String argv[]) {
    new JTable11();
    }
  public JTable11() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    dtm=new DefaultTableModel();
    jt=new JTable(dtm);
    jt.setPreferredScrollableViewportSize(f.getSize());
    jt.setCellSelectionEnabled(true);
    jt.setRowSelectionAllowed(true);
    jt.setColumnSelectionAllowed(true);
    ListSelectionModel lsm=jt.getSelectionModel();
    lsm.addListSelectionListener(this);
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    //setup button
    JPanel panel=new JPanel(new GridLayout(1,4));
    JButton addColumn=new JButton("增加行");
    addColumn.addActionListener(this);
    JButton removeColumn=new JButton("移除行");
    removeColumn.addActionListener(this);
    JButton addRow=new JButton("增加列");
    addRow.addActionListener(this);
    JButton removeRow=new JButton("移除列");
    removeRow.addActionListener(this);
    panel.add(addColumn);
    panel.add(removeColumn);
    panel.add(addRow);
    panel.add(removeRow);
    cp.add(panel,BorderLayout.NORTH);
    status=new JLabel(" ");
    cp.add(status,BorderLayout.SOUTH);
    f.setVisible(true);

    //Close JFrame      
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f));
    }
  public void actionPerformed(ActionEvent e) {
    String cmd=e.getActionCommand();
    if (cmd.equals("增加行")) {
      int selectedColumnID=jt.getSelectedColumn();
      dtm.addColumn("行 " + cid);  //於尾端增加一行
      int columnCount=dtm.getColumnCount();  //新總行數
      if (selectedColumnID != -1) {  //有選取行 : 將新增之最後行移到選取行
        TableColumnModel columnModel=jt.getColumnModel();
        columnModel.moveColumn(columnCount-1,selectedColumnID);
        }
      status.setText("已新增一行, 總行數=" + columnCount);
      ++cid;
      }
    if (cmd.equals("增加列")) {
      int selectedRowID=jt.getSelectedRow();
      int rowCount=dtm.getRowCount();
      if (selectedRowID != -1) { //有選取 : 在選取列插入新列
        dtm.insertRow(selectedRowID,new Vector());
        }
      else {dtm.addRow(new Vector());} //沒有選取 : 在尾端加一列
      dtm.setValueAt("列 " + rid, rowCount, 0);  //在第一儲存格顯示列號
      status.setText("已新增一列, 總列數=" + dtm.getRowCount());
      ++rid;
      }
    if (cmd.equals("移除行")) {
      int selectedColumnID=jt.getSelectedColumn();
      int columnCount=dtm.getColumnCount();
      int lastColumnID=columnCount-1;
      if (selectedColumnID != -1) { //one column selected
        TableColumnModel columnModel=jt.getColumnModel();
        TableColumn column=columnModel.getColumn(selectedColumnID);
        columnModel.removeColumn(column);
        dtm.setColumnCount(columnCount-1);
        String msg="已刪除行 id=" + selectedColumnID + ", 總行數=" +
          dtm.getColumnCount();
        status.setText(msg);
        }
      else { //no column selected
        TableColumnModel columnModel=jt.getColumnModel();
        TableColumn column=columnModel.getColumn(lastColumnID);
        columnModel.removeColumn(column);
        dtm.setColumnCount(columnCount-1);
        String msg="已刪除行 id=" + lastColumnID + ", 總行數=" +
          dtm.getColumnCount();
        status.setText(msg);
        }
      }
    if (cmd.equals("移除列")) {
      int selectedRowID=jt.getSelectedRow();
      int rowCount=dtm.getRowCount();
      int lastRowID=rowCount-1;
      if (selectedRowID != -1) { //有選取 : 刪除指定列
        dtm.removeRow(selectedRowID);
        dtm.setRowCount(rowCount-1);
        String msg="已刪除列 id=" + selectedRowID + ", 總列數=" +
          dtm.getRowCount();
        status.setText(msg);
        }
      else { //無選取 : 刪除最後一列
        dtm.removeRow(lastRowID);
        dtm.setRowCount(rowCount-1);
        String msg="已刪除列 id=" + lastRowID + ", 總列數=" +
          dtm.getRowCount();
        status.setText(msg);
        }
      }
    }
  public void valueChanged(ListSelectionEvent e) {
    String msg="選取列 " + jt.getSelectedRow() + " 行 " +
      jt.getSelectedColumn();
    status.setText(msg);
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }
  }

此例中我增加了選取動作的監聽, 以便在下方的 JLabel 中顯示選取了哪一列哪一行. 在增加行或列時, 要判斷是否有選取動作, DefaultTableModel 的 getSelectedRow() 與 getSelectedColumn() 在沒有選取時會傳回 -1, 有選取時則傳回選取的 (第一個) 行或列之索引, 故可藉此來判定是要新增行列於選取之位置還是末尾. 此外, 我也在新增列時, 在第一個儲存格加入列號, 以方便與下方 JLabel 之顯示相對照.

比較特別的是, 插入列有 JTable 的 insertRow() 可用, 但插入行卻沒有 insertColumn() 可用, 變通辦法是利用 TableColumnModel 物件的 moveColumn() 方法, 先用 DefaultTableModel 的 addColumn() 新增一行於 Model 尾端 (變成新的最後一行), 再呼叫 JTable 的 getColumnModel() 取得 TableColumnModel 物件, 就可以使用其 moveColumn() 方法將此新增行從尾端移往選取行了.

刪除列可以用 DefaultTableModel 的 removeRow() 方法;  刪除行則要用 TableColumnModel 的 removeColumn() 方法. 奇怪的是, 即使傳入選取的行索引給 TableColumnModel 物件的 getColumn() 以取得 TableColumn 物件, 但刪除時卻跟無選取時一樣, 都刪除最後一行, why?  再研究.

接下來要測試 JTable 的排序功能, 這要用到 TableRowSorter 類別以及 JTable 的 setRowSorter() 方法, 其 API 如下 :

http://docs.oracle.com/javase/7/docs/api/javax/swing/table/TableRowSorter.html

TableRowSorter 繼承自 RowSorter 與 DefaultRowSorter 類別, 要讓表格具有排序功能很簡單, 只要傳入 TableModel 物件當參數給 TableRowSorter 的建構子建立一個排序物件, 再將此排序物件傳給 JTable 的 setRowSorter() 方法即可. 注意, TableRowSorter 使用泛型宣告, 因此建立排序物件時必須使用泛型並指定 TableModel 型態, 否則會出現編譯錯誤 :

    TableRowSorter sorter=new TableRowSorter(tablemodel);
    jtable.setRowSorter(sorter);

也可以先用 TableRowSorter 無參數的建構子, 再呼叫 setModel() 方法亦可 :

    TableRowSorter sorter=new TableRowSorter();
    sorter.setModel(tablemodel);
    jtable.setRowSorter(sorter);

參考 : java JTable排序和过滤

範例程式如下 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class JTable12 implements ActionListener {
  JFrame f;
  JTable jt;
  public static void main(String argv[]) {
    new JTable12();
    }
  public JTable12() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    MyTableModel mtm=new MyTableModel();
    jt=new JTable(mtm);
    TableRowSorter sorter=new TableRowSorter(mtm);
    jt.setRowSorter(sorter);
    jt.setPreferredScrollableViewportSize(f.getSize());
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    f.setVisible(true);

    //Close JFrame      
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f));
    }
  public void actionPerformed(ActionEvent e) {
    String cmd=e.getActionCommand();
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }  
  }
class MyTableModel extends AbstractTableModel {
  Object[][] data={
    {"Kelly","Female",new Integer(16),false,"kelly@gmail.com"},
    {"Peter","Male",new Integer(14),false,"peter@gmail.com"},
    {"Amy","Female",new Integer(12),false,"amy@gmail.com"},
    {"Tony","Male",new Integer(49),true,"tony@gmail.com"},
    {"John","Male",new Integer(23),true,"john@gmail.com"},
    {"Eva","Female",new Integer(19),false,"eva@gmail.com"},
    {"Rebeca","Female",new Integer(9),false,"rebeca@gmail.com"}};
  String[] columns={"Name","Gender","Age","Vegetarian","E-mail"};
  public int getColumnCount() {return columns.length;}
  public int getRowCount() {return data.length;}
  public Object getValueAt(int row, int col) {return data[row][col];}
  public String getColumnName(int col) {return columns[col];}
  public Class getColumnClass(int col) {
    return getValueAt(0,col).getClass();
    }
  public boolean isCellEditable(int row,int col) {return true;}
  public void setValueAt(Object value,int row,int col) {
    data[row][col]=value;
    fireTableCellUpdated(row,col);
    }
  }

當點選欄位標題時, 就會以該欄之數據對列進行排序 (故取名為 RowSorter), 標題上會出現一個三角標誌, 向上為遞增排序, 向下為遞降排序.

接下來要測試此行最重要的一個測試, 即如何將資料庫中擷取出來的資料呈現於 JTable 中. 最簡單的方法是利用 DefaultTableModel 的 addRow() 方法, 此方法有兩個多載 :
  1. void addRow(Object[] rowData)
  2. void addRow(Vector rowData)
此方法會在表格模型尾端加入一列資料. 由於從資料表中擷取出來的資料欄位有各種型態, 所以宜使用第一個 addRow(), 將各欄位以物件陣列傳入. 但除了 addRow() 增添列資料外, 還必須利用 DefaultTableModel 的 setColumnIdentifiers(Object[] columnNames) 方法來處理欄位標題, 否則資料列不會顯示 :

    String[] columnNames={"Name","Gender","Age","Vegetarian","E-mail"};
    dtm.setColumnIdentifiers(columnNames);

當然, 為了簡化資料庫存取, 我使用了之前開發的迷你 Java 套件 JT.java, 參考下列文章 :

# http://yhhuang1966.blogspot.tw/2014/04/java-access.html

範例如下 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import java.sql.*;

public class JTable13 {
  JFrame f;
  JTable jt;
  public static void main(String argv[]) {
    new JTable13();
    }
  public JTable13() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    DefaultTableModel dtm=new DefaultTableModel();
    jt=new JTable(dtm);

    String[] columnNames={"Name","Gender","Age","Vegetarian","E-mail"};
    dtm.setColumnIdentifiers(columnNames);

    TableRowSorter sorter=new TableRowSorter(dtm);
    jt.setRowSorter(sorter); 
    //connect ACCESS DB
    JT.connectMDB("testdb");  //連線 ACCESS 資料庫
    try {
      ResultSet rs=JT.runSQL("SELECT * FROM users");  //執行 SQL 查詢
      while (rs.next()) {  //拜訪紀錄集的每一列
        dtm.addRow(new Object[]{
          rs.getObject("Name"),
          rs.getObject("Gender"),
          rs.getObject("Age"),          
          rs.getObject("Vegetarian"),          
          rs.getObject("E-mail")         
          });

        }
      rs.close(); //關閉紀錄集
      rs=null;
      }
    catch (Exception e) {System.out.println(e);}
    JT.closeDB(); //關閉資料庫連線
    jt.setPreferredScrollableViewportSize(f.getSize());
    jt.setCellSelectionEnabled(true);
    jt.setRowSelectionAllowed(true);
    jt.setColumnSelectionAllowed(true);
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    f.setVisible(true);

    //Close JFrame      
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f));
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }
  }


執行此程式前, 必須先建立一個資料庫 testdb.mdb, 裡面有一個資料表 users, 內容如下 :


除了 Vegetarian 為 YES/NO 型態, Age 為 INT 型態外, 其餘均為 VARCHAR 型態. 一般我們讀取 ResultSet 物件裡的資料時, 通常呼叫其 getString() 方法, 對於 YES/NO 型態的欄位會傳回 0/1 而非 true/false.  如果要顯示 true/false 就要用 Boolean 包裹物件的 valueOf() 方法轉換 :

Boolean.valueOf(rs.getString("Vegetarian"))


這倒不如直接使用 getObject() 來得方便.

參考資料 :

# http://tips4java.wordpress.com/2009/03/12/table-from-database/ 
# http://www.roseindia.net/java/example/java/swing/jtable-display-database-data.shtml
# Simple show database data in JTable

除了上面使用 DefautTableModel 的 addRow() + setColumnIdentifiers() 兩個方法來加入 JTable 內容外, 還可以使用 JTable 的最後一個建構子, 以 Vector 類別來製作 JTable 的欄位標題以及每一列內容 :

JTable(Vector rowData, Vector columnNames)

Vector 類別用來存放各類物件, 與陣列一樣用整數索引來存取元素, 其元素長度可擴展與縮減 (可變), 不像陣列是型態一致與長度固定不可變. 其常用方法如下 :

  1. add(E e) : 加入元素
  2. add(int index, E element) : 於指定索引加入元素
  3. addElement(E obj) : 加入元素
  4. contains(Object o) : 是否含有指定元素
  5. elementAt(int index) : 傳回指定索引的元素
  6. indexOf(Object o) : 傳回指定元素的索引 (無傳回 -1)
  7. remove(int index) : 移除指定索引元素
  8. remove(Object o) : 移除指定元素
  9. removeElementAt(int index) : 移除指定索引元素
  10. size() : 傳回元素個數
  11. toArray() : 轉成陣列傳回

這裡我們只會用到其 add() 方法來添加元素. Vector 放在 java.util 套件, 故須匯入此類別庫. 詳細 API 參見 :

# http://docs.oracle.com/javase/7/docs/api/java/util/Vector.html 

程式碼如下 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import java.sql.*;
import java.util.*;

public class JTable14 {
  JFrame f;
  JTable jt;
  public static void main(String argv[]) {
    new JTable14();
    }
  public JTable14() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    Vector<String> columnNames=new Vector<String>(); //儲存欄位標題
    columnNames.add("Name");
    columnNames.add("Gender");
    columnNames.add("Age");
    columnNames.add("Vegetarian");
    columnNames.add("E-mail");
    //connect ACCESS DB
    JT.connectMDB("testdb");  
    Vector<Vector<Object>> data=new Vector<Vector<Object>>(); //store a row
    try {
      ResultSet rs=JT.runSQL("SELECT * FROM users");
      while (rs.next()) {
        Vector<Object> row=new Vector<Object>(); //store each cell of a row
        row.add(rs.getObject("Name")); //把欄位加入 row 的 Vector 中
        row.add(rs.getObject("Gender"));
        row.add(rs.getObject("Age"));
        row.add(rs.getObject("Vegetarian"));
        row.add(rs.getObject("E-mail"));
        data.add(row);
        }
      rs.close();
      rs=null;
      }
    catch (Exception e) {System.out.println(e);}
    JT.closeDB();
    DefaultTableModel dtm=new DefaultTableModel(data, columnNames);
    jt=new JTable(dtm);
    dtm.setColumnIdentifiers(columnNames);
    TableRowSorter<TableModel> sorter=new TableRowSorter<TableModel>(dtm);
    jt.setRowSorter(sorter);
    jt.setPreferredScrollableViewportSize(f.getSize());
    jt.setCellSelectionEnabled(true);
    jt.setRowSelectionAllowed(true);
    jt.setColumnSelectionAllowed(true);
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    f.setVisible(true);

    //Close JFrame      
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f));
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }
  }

執行結果與上例一樣, 故不抓圖. 這裡使用了兩個 Vector 來分別儲存欄位標題 columnNames 與表格內容 data (列資料). 注意, Vector 必須使用泛型宣告指定元素類型, 其中欄位標題只要字串即可, 而列則為二維物件資料, 因此使用兩層泛型, 型態為 Object.

參考 :

http://stackoverflow.com/questions/18921211/transferring-data-from-database-to-jtable

上面從資料庫擷取資料表, 並將內容描繪到 JTable 的兩種方法都是使用 DefaultTableModel, 即使使用 getObject() 擷取, 描繪時都會轉成字串 (例如 YES/NO 欄位變成 true/false, 用 getString() 則變成 0/1), 無法以物件在 Swing 的對應型態描繪 (例如 YES/NO 以 checkbox 呈現). 另外預設表格模型的儲存格也是編輯無效的 (可編輯, 但無效). 如果要讓表格模型客製化, 應該繼承 AbstractTableModel 或 DefaultTableModel, 並覆寫想要客製化的方法才行.

在下面範例中, 我繼承了 AbstractTableModel 類別, 並改用 ArrayList 來儲存列資料 (當然用 Vector 也是可以的), 程式碼如下 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import java.sql.*;
import java.util.*;

public class JTable15 {
  JFrame f;
  JTable jt;
  public static void main(String argv[]) {
    new JTable15();
    }
  public JTable15() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    MyTableModel mtm=new MyTableModel();
    jt=new JTable(mtm);
    TableRowSorter<TableModel> sorter=new TableRowSorter<TableModel>(mtm);
    jt.setRowSorter(sorter);
    jt.setPreferredScrollableViewportSize(f.getSize());
    jt.setCellSelectionEnabled(true);
    jt.setRowSelectionAllowed(true);
    jt.setColumnSelectionAllowed(true);
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    f.setVisible(true);

    //Close JFrame      
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f));
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }
  }
class MyTableModel extends AbstractTableModel {
  String[] columns={"Name","Gender","Age","Vegetarian","E-mail"};
  ArrayList<ArrayList<Object>> data=new ArrayList<ArrayList<Object>>();
  public MyTableModel() {  //在建構子中描繪各列資料
    //connect ACCESS DB
    JT.connectMDB("testdb");
    try {
      ResultSet rs=JT.runSQL("SELECT * FROM users"); 
      while (rs.next()) {
        ArrayList<Object> row=new ArrayList<Object>(); //儲存每列之各欄資料
        row.add(rs.getObject("Name"));
        row.add(rs.getObject("Gender"));
        row.add(rs.getObject("Age"));
        row.add(rs.getObject("Vegetarian"));
        row.add(rs.getObject("E-mail"));
        data.add(row);
        }
      rs.close();
      rs=null;
      }
    catch (Exception e) {System.out.println(e);}
    JT.closeDB();
    }
  public int getColumnCount() {return columns.length;}
  public int getRowCount() {return data.size();} //列數=ArrayList 的大小
  public Object getValueAt(int row, int col) {
    ArrayList<Object> rowdata=data.get(row); //取得列物件
    return rowdata.get(col);  //傳回指定欄位之儲存格內容
    }
  public String getColumnName(int col) {return columns[col];}
  public Class getColumnClass(int col) {
    return getValueAt(0,col).getClass();
    }
  public boolean isCellEditable(int row,int col) {return true;} //可編輯
  public void setValueAt(Object value,int row,int col) { //使編輯有效之處理
    ArrayList<Object> rowdata=data.get(row);  //取得列物件
    rowdata.set(col,value);  //將編輯後資料更新列資料物件之指定欄
    fireTableCellUpdated(row,col); //更新表格顯示
    }
  }

此例中讓儲存格編輯有效的關鍵是我們覆寫了 isCellEditable() 與 setValueAt() 這兩個方法, 前者讓儲存格可編輯, 後者則是讓編輯結果生效, 也就是更新了表格的資料來源 (ArrayList 中的元素). 先呼叫 ArrayList 的 get() 方法以所編輯之列索引取得列物件, 再用 set() 方法更新此列物件在所編輯欄之值, 最後呼叫 AbstractTableModel 的 fireTableCellUpdated() 方法觸發 JTable 重新描繪該儲存格, 這樣便達到有效編輯的目的. 不過這只是更改描繪結果, 與資料庫無關, users 資料表並未被更改.

參考 :

# http://www.users.csbsju.edu/~lziegler/CS317/NetProgramming/JTable.html



2014年5月24日 星期六

Java Swing 測試 : 表格 JTable

表格乃是呈現二維資料最常用的一種方式, 例如顯示從資料庫中擷取的資料表紀錄. Swing 提供了以 JTable 類別為首的一系列完整表格處理類別, 收錄在 javax.swing.table 套件中, JTable 仰賴這些協助類別讓表格的運用變得非常容易與具有彈性. 相關的類別與介面 API 如下 :

# http://docs.oracle.com/javase/7/docs/api/
# JTable 類別
# TableModel 介面 
# AbstractTableModel 類別
# DefaultTableModel 類別
# TableColumnModel 介面
# DefaultTableColumnModel 類別
# ListSelectionModel 介面
# DefaultListSelectionModel 類別
# ListSelectionListener 介面
# TableColumn 類別

在使用 JTable 之前, 必須先對它運作的原理與結構做個理解. 如同 Excel 試算表一樣, 表格的二維資料由兩個部份組成, 一是資料集合, 由多筆紀錄組成 (Y 軸); 二是欄位資訊 (X 軸), 包括各欄位標題, 各欄資料型態等等, 這些表格資料稱為資料模型 (Data Model), 包含了資料結構, 資料內容, 以及存取方法. 在 Swing 中, JTable 類別只負責呈現資料, 本身並不包含任何資料. 亦即, 使用 JTable 必須餵給它資料來源, 此外, 還必須提供此表格含有幾欄幾列等資訊, 也要提供存取資料的方法, 這些合起來就是一個資料模型. JTable 用到的資料模型有三種 :
  1. TableModel (表格模型) : 負責處理整個表格, 主要是針對列
  2. TableColumnModel (表格欄模型) : 負責處理欄
  3. SelectionModel (選取模型) : 負責處理資料選取
Swing 的表格模型定義在 TableModel 介面中, 欲製作資料來源必須實做此介面. 此介面定義了 9 個方法, 例如取得欄位名稱的 getColumnName(); 取得欄列數目的 getColumnCount() 與 getRowCount(); 存取儲存格的 getValueAt() 與 setValue() 等等. Swing 中提供了 AbstractTableModel 這個實做大部分 TableModel 方法的抽象類別 (主要是幫我們搞定了 TableModelEvent 的事件監聽), 只保留了三個重要的方法給使用者改寫 :
  1. getColumnCount()
  2. getRowCount()
  3. getValueAt()
如果想要建立自己的表格模型, 可以繼承 AbstractTableModel 後覆寫這三個方法即可, 當然, 若要覆寫其他方法也是可以的, 例如 getColumnName(). 但要注意的是, AbstractTableModel 類別放在 javax.swing.table 類別庫, 因此須用 import 匯入. 自己建立模型的好處是較有彈性, 可以客製化表格呈現方式. 如果不想這麼麻煩, 那就使用 DefaultTableModel 類別, 它是 AbstractTableModel 的子類別, 覆寫了上面三個它所保留的方法, JTable 預設就是使用這個資料模型.

除了表格模型 TableModel 外, 還有一個資料模型是使用 JTable 時必須了解的, 那便是表格欄模型, 這是專門用來處理欄的, 因為表格模型主要的處理對象是列, 所牽涉的欄是這列裡面的某欄, 它無法處理整欄的資料 (例如刪除某欄). 如果要對欄進行操控, 那麼就必須提供表格欄模型, Swing 是定義在 TableColumnModel 這個介面裡, 它定義了 19 個方法來操控表格的欄, 例如 addColumn() 方法可以在表格中新增一個資料欄. 欲處理資料欄必須實做 TableColumnModel 介面, 不過 Swing 提供了 DefaultTableColumnModel 類別可供使用, 此類別實做了全部 TableColumnModel 介面的方法. JTable 預設就是使用此類別. 利用 JTable 的 getColumnModel() 方法可以取得 TableColumnModel 的物件以進行整欄的操作, 例如將某欄設定為無法編輯.

第三個模型為選取模型. 這跟 JList 的項目選取是完全一樣的, 亦即在 JTable 表格中選取資料時可以做單選, 單一連續區間, 多重連續區間這三種選取動作. 如果要對 JTable 的選取模式進行操控, 就必須實做 ListSelectionModel 介面. 此介面定義了三個靜態屬性可用來控制選取模式 :
  1. ListSelectionModel.SINGLE_SELECTION
  2. ListSelectionModel.SINGLE_INTERVAL_SELECTION
  3. ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
Swing 提供了 DefaultListSelectionModel 類別, 它實作了 ListSelectionModel 介面的全部方法. JTable 預設就是使用此類別. 利用 JTable 的 getSelectionModel() 方法就可以取得 ListSelectionModel 物件, 然後再呼叫其 setSelectionMode() 方法就可以更改選取模式了. 另外, 如果對 JTable 表格的資料欄位進行選取動作, 就會觸發 ListSelectionEvent 事件. 若要處理此事件, 必須實做 ListSelectionListener 介面, 並實做 valueChanged() 方法.

以上對 JTable 的資料模型有初步了解後, 就比較能看懂 JTable 的建構子了.

JTable 的建構子有七個 :
  1. JTable()
  2. JTable(int numRows, int numColumns)
  3. JTable(Object[][] rowData, Object[] columnNames)  (固定資料)
  4. JTable(TableModel dm)
  5. JTable(TableModel dm, TableColumnModel cm)
  6. JTable(TableModel dm, TableColumnModel cm, ListSelectionModel sm)
  7. JTable(Vector rowData, Vector columnNames)  (可變資料)
首先就來測試一下最常用的第三個建構子, 第一個參數是要顯示的資料, 型態為二維物件陣列; 第二個參數是欄標題, 型態為物件陣列. 我們使用最常見的陣列來表示資料來源. 此建構子使用預設的資料模型 (DefaultTableModel, DefaultTableColumnModel, DefaultListSelectionModel). 範例程式如下 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JTable1 {
  JFrame f;
  public static void main(String argv[]) {
    new JTable1(); 
    }
  public JTable1() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();

    //Build Elements
    Object[][] data={
      {"Kelly","Female",new Integer(16),false,"kelly@gmail.com"},
      {"Peter","Male",new Integer(14),false,"peter@gmail.com"},
      {"Amy","Female",new Integer(12),false,"amy@gmail.com"},
      {"Tony","Male",new Integer(49),true,"tony@gmail.com"}};
    String[] columns={"Name","Gender","Age","Vegetarian","E-mail"};
    JTable jt=new JTable(data,columns);
    jt.setPreferredScrollableViewportSize(new Dimension(400,300));
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    f.setVisible(true);

    //Close JFrame       
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f)); 
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }   
  }


注意, JTable 必須呼叫 setPreferredViewportSize() 方法設定顯示區域大小, 否則什麼也看不到. 其次, 此處我們先將 JTable 物件先放進 JScrollPane 裡, 再加入內容面板中. 這樣有兩個好處, 一是當資料列數超過視窗高度時, 會自動出現垂直捲軸, 二是會 JScrollPane 會自動解讀欄位標題. 如果修改上述程式, 直接將 JTable 放進內容面板的話, 欄位標題就不會顯示了, 而且當資料長度超過視窗高度時也無法看到後面的資料 :

cp.add(jt,BorderLayout.CENTER); //JTable 直接放進內容面板


可見只顯示表格內容, 標題不見了. 所以使用 JTable 時通常都用 JScrollPane 包起來.

此範例也顯示 JTable 的欄位寬度是可調整的, 只要滑鼠放在欄位分隔線上拖曳, 就可調整欄位所佔寬度. JTable 有一個 autoResizeMode 屬性用來設定五種不同的自動調整寬度模式, 而且提供五個靜態屬性用來設定此autoResizeMode 屬性 :
  1. JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS (預設)
    只有後面的欄位會同步調整寬度
  2. JTable.AUTO_RESIZE_ALL_COLUMNS
    全部欄位都會同步調整寬度
  3. JTable.AUTO_RESIZE_OFF
    關閉同步調整 (只有目前欄位調整寬度)
  4. JTable.AUTO_RESIZE_NEXT_COLUMN
    只有後面那個欄位同步調整寬度
  5. JTable.AUTO_RESIZE_LAST_COLUMN
    只有最後面那個欄位同步調整寬度
我修改了上面範例, 在下方加上五個按鈕來設定同步調整寬度模式, 如下所示 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

public class JTable2 implements ActionListener {
  JFrame f;
  JTable jt;
  public static void main(String argv[]) {
    new JTable2(); 
    }
  public JTable2() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    Object[][] data={
      {"Kelly","Female",new Integer(16),false,"kelly@gmail.com"},
      {"Peter","Male",new Integer(14),false,"peter@gmail.com"},
      {"Amy","Female",new Integer(12),false,"amy@gmail.com"},
      {"Tony","Male",new Integer(49),true,"tony@gmail.com"}};
    String[] columns={"Name","Gender","Age","Vegetarian","E-mail"};
    jt=new JTable(data,columns);
    jt.setPreferredScrollableViewportSize(new Dimension(400,300));
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    //Add resize button
    JPanel panel=new JPanel(new GridLayout(5,1));
    JButton btn=new JButton("AUTO_RESIZE_SUBSEQUENT_COLUMNS");
    btn.addActionListener(this);
    panel.add(btn);
    btn=new JButton("AUTO_RESIZE_ALL_COLUMNS");
    btn.addActionListener(this);
    panel.add(btn);
    btn=new JButton("AUTO_RESIZE_OFF");
    btn.addActionListener(this);
    panel.add(btn);
    btn=new JButton("AUTO_RESIZE_NEXT_COLUMN");
    btn.addActionListener(this);
    panel.add(btn);
    btn=new JButton("AUTO_RESIZE_LAST_COLUMN");
    btn.addActionListener(this);
    panel.add(btn);
    cp.add(panel,BorderLayout.SOUTH);
    f.setVisible(true);

    //Close JFrame       
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f)); 
    }
  public void actionPerformed(ActionEvent e) {
    String cmd=e.getActionCommand();
    if (cmd.equals("AUTO_RESIZE_SUBSEQUENT_COLUMNS")) {
      jt.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
      }
    if (cmd.equals("AUTO_RESIZE_ALL_COLUMNS")) {
      jt.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
      }
    if (cmd.equals("AUTO_RESIZE_OFF")) {
      jt.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
      }
    if (cmd.equals("AUTO_RESIZE_NEXT_COLUMN")) {
      jt.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN);
      }
    if (cmd.equals("AUTO_RESIZE_LAST_COLUMN")) {
      jt.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
      }
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }   
  }

上圖是設定 AUTO_RESIZE_OFF 後將 Age 欄位擴大, 寬度超過視窗, 結果出現水平捲軸. JTable 也只有在這種模式下才會出現水平捲軸, 因為其他四種模式至少有一個欄位會同步調整寬度, 因此不會讓總寬度超過容器的寬度. 注意, 這裡因為要在動作事件t處理中存取 JTable, 因此需宣告為類別屬性.

上面範例使用預設的資料模型有如下的缺點 :
  1. 每一個欄位寬度相等, 無法設定.
  2. 每一個資料都是可編輯, 會被使用者更改已顯示出來的資料. 
  3. 每一個欄位資料均為字串, 故 Vegetarian 欄位顯示為 "true", "false", 而非核取方塊.
其中欄位寬度這可以呼叫 TableColumn 類別所提供的 setPreferredWidth() 來修改所想要的寬度, 此類別可以由 TableColumnModel 物件的 getColumn() 方法取得, 它放在 javax.swing.table 類別庫裡面, 因此必須特別加以匯入. 如下範例所示 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class JTable3 {
  JFrame f;
  public static void main(String argv[]) {
    new JTable3(); 
    }
  public JTable3() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    Object[][] data={
      {"Kelly","Female",new Integer(16),false,"kelly@gmail.com"},
      {"Peter","Male",new Integer(14),false,"peter@gmail.com"},
      {"Amy","Female",new Integer(12),false,"amy@gmail.com"},
      {"Tony","Male",new Integer(49),true,"tony@gmail.com"}};
    String[] columns={"Name","Gender","Age","Vegetarian","E-mail"};
    JTable jt=new JTable(data,columns);
    jt.setPreferredScrollableViewportSize(new Dimension(400,300));
    TableColumn column=jt.getColumnModel().getColumn(4);
    column.setPreferredWidth(150);
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    f.setVisible(true);

    //Close JFrame       
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f)); 
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }   
  }

可見 E-mail 欄位 (索引 4) 的預設寬度已經放大了, 但這是有限度的, 不是設多大就多大, 把其他欄位都擠扁, 而是 preferred, 盡量滿足我們所要求的. 注意這裡我們首先呼叫了 JTable 的 getColumnModel() 方法, 它會傳回一個 TableColumnModel 物件, 再呼叫其 getColumn() 方法, 傳回一個 TableColumn 物件, 最後呼叫其 setPreferredWidth() 方法來設定欄位寬度.

至於後面兩個缺點, 則必須繼承 AbstractTableModel 來自訂資料模型才能解決. 如下範例所示 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class JTable4 {
  JFrame f;
  public static void main(String argv[]) {
    new JTable4(); 
    }
  public JTable4() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    MyTableModel mtm=new MyTableModel();
    JTable jt=new JTable(mtm);
    jt.setPreferredScrollableViewportSize(new Dimension(400,300));
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    f.setVisible(true);

    //Close JFrame       
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f)); 
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }   
  }
class MyTableModel extends AbstractTableModel {
  Object[][] data={
    {"Kelly","Female",new Integer(16),false,"kelly@gmail.com"},
    {"Peter","Male",new Integer(14),false,"peter@gmail.com"},
    {"Amy","Female",new Integer(12),false,"amy@gmail.com"},
    {"Tony","Male",new Integer(49),true,"tony@gmail.com"}};
  String[] columns={"Name","Gender","Age","Vegetarian","E-mail"};
  public int getColumnCount() {return columns.length;}
  public int getRowCount() {return data.length;}
  public Object getValueAt(int row, int col) {return data[row][col];}
  }


此例中我們繼承 AbstractTableModel 建立一個自訂的表格模型, 實作了三個方法. 注意, 每一個欄位都無法編輯, 這是因為 AbstractTableModel 類別實作 isColumnEditable() 方法時, 一律傳回 false 之故. 自訂模型帶來新的缺點, 即欄位標題被改成像 EXCEL 試算表的 A,B,C ... 了. 這是因為 AbstractTableModel 類別在實作 getColumnName() 方法時, 以 A,B,C, ... 當作預設欄位標題之故. 必須覆寫此方法, 並傳回欄位標題才會用新的標題覆蓋掉 A,B,C, ...

public String getColumnName(int col) {return columns[col];} 

其次, 第三個缺點依然存在, 資料仍然一律視為 String 類型, 這可以覆寫 getColumnClass() 方法, 傳回第 0 列各欄的類別即可 :

public Class getColumnClass(int col) {return getValueAt(0, col).getClass();}

最後, 如果要讓被 AbstractTableModel 禁掉的可編輯功能復活, 可以覆寫 isColumnEditable() 方法並傳回 true 即可 :

public boolean isCellEditable(int row,int col) {return true;}

範例程式如下 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class JTable5 {
  JFrame f;
  public static void main(String argv[]) {
    new JTable5(); 
    }
  public JTable5() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    MyTableModel mtm=new MyTableModel();
    JTable jt=new JTable(mtm);
    jt.setPreferredScrollableViewportSize(new Dimension(400,300));
    TableColumn column=jt.getColumnModel().getColumn(4);
    column.setPreferredWidth(150);
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    f.setVisible(true);

    //Close JFrame       
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f)); 
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }   
  }
class MyTableModel extends AbstractTableModel {
  Object[][] data={
    {"Kelly","Female",new Integer(16),false,"kelly@gmail.com"},
    {"Peter","Male",new Integer(14),false,"peter@gmail.com"},
    {"Amy","Female",new Integer(12),false,"amy@gmail.com"},
    {"Tony","Male",new Integer(49),true,"tony@gmail.com"}};
  String[] columns={"Name","Gender","Age","Vegetarian","E-mail"};
  public int getColumnCount() {return columns.length;}
  public int getRowCount() {return data.length;}
  public Object getValueAt(int row, int col) {return data[row][col];}
  public String getColumnName(int col) {return columns[col];}
  public Class getColumnClass(int col) {
    return getValueAt(0,col).getClass();
    }
  public boolean isCellEditable(int row,int col) {return true;}

  }

可見欄位標題回來了, 而且實作 getColumnClass() 後有了大改變, 不僅 true/false 變成以核取方塊顯示, 而且字串資料會自動向左對齊, 數字則向右對齊. 但奇怪的是, 實作 isColumnEditable() 傳回 true 後, 雖然文字與數字欄位又恢復可編輯狀態, 但是滑鼠一離開, 馬上恢復原狀, 而且 true/false 的核取方塊仍然無法編輯, why?

原因是, 光是覆寫 isColumnEditable() 是沒有用的, 我們還需要真正去修改資料來源的內容才行, 這可以透過覆寫 setValueAt() 方法解決 :

  public void setValueAt(Object value,int row,int col) {
    data[row][col]=value;  //更新資料來源
    fireTableCellUpdated(row,col);   //觸發 JTable 更新顯示元件

    }

這裡除了以傳入的值更新資料來源外, 還需呼叫 AbstractTableModel 類別的 fireTableCellUpdated() 方法去觸發 TableEvent 事件, JTable 才會更新顯示的元件.

範例程式碼如下 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class JTable6 {
  JFrame f;
  public static void main(String argv[]) {
    new JTable6(); 
    }
  public JTable6() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    MyTableModel mtm=new MyTableModel();
    JTable jt=new JTable(mtm);
    jt.setPreferredScrollableViewportSize(new Dimension(400,300));
    JComboBox jcb=new JComboBox();
    jcb.addItem("O");
    jcb.addItem("A");
    jcb.addItem("B");
    jcb.addItem("AB");
    TableColumn column=jt.getColumnModel().getColumn(5);

    column.setCellEditor(new DefaultCellEditor(jcb));
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    f.setVisible(true);

    //Close JFrame       
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f)); 
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }   
  }
class MyTableModel extends AbstractTableModel {
  Object[][] data={
    {"Kelly","Female",new Integer(16),false,"kelly@gmail.com","O"},
    {"Peter","Male",new Integer(14),false,"peter@gmail.com","O"},
    {"Amy","Female",new Integer(12),false,"amy@gmail.com","O"},
    {"Tony","Male",new Integer(49),true,"tony@gmail.com","O"}};
  String[] columns={"Name","Gender","Age","Vegetarian","E-mail","Blood Type"};
  public int getColumnCount() {return columns.length;}
  public int getRowCount() {return data.length;}
  public Object getValueAt(int row, int col) {return data[row][col];}
  public String getColumnName(int col) {return columns[col];}
  public Class getColumnClass(int col) {
    return getValueAt(0,col).getClass();
    }
  public boolean isCellEditable(int row,int col) {return true;}
  public void setValueAt(Object value,int row,int col) {
    data[row][col]=value;
    fireTableCellUpdated(row,col);
    }

  }

可見經此修改後, 每一個欄位均可編輯了. 上面讓 JTable 以相應元件顯示原本資料型態的 getColumnClass() 方法, 其實還有另外一種覆寫方式, 就是傳回相應的 Class 物件, 如下範例所示 :

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class JTable7 {
  JFrame f;
  public static void main(String argv[]) {
    new JTable7(); 
    }
  public JTable7() {
    //Setup JFrame
    JFrame.setDefaultLookAndFeelDecorated(true);
    JDialog.setDefaultLookAndFeelDecorated(true);
    f=new JFrame("JTable Test");
    f.setSize(400,300);
    f.setLocationRelativeTo(null);
    Container cp=f.getContentPane();
    //cp.setLayout(null);

    //Build Elements
    MyTableModel mtm=new MyTableModel();
    JTable jt=new JTable(mtm);
    jt.setPreferredScrollableViewportSize(new Dimension(400,300));
    JComboBox jcb=new JComboBox();
    jcb.addItem("O");
    jcb.addItem("A");
    jcb.addItem("B");
    jcb.addItem("AB");
    TableColumn column=jt.getColumnModel().getColumn(5);
    column.setCellEditor(new DefaultCellEditor(jcb));
    cp.add(new JScrollPane(jt),BorderLayout.CENTER);
    f.setVisible(true);

    //Close JFrame       
    f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    f.addWindowListener(new WindowHandler(f)); 
    }
  }
class WindowHandler extends WindowAdapter {
  JFrame f;
  public WindowHandler(JFrame f) {this.f=f;}
  public void windowClosing(WindowEvent e) {
    int result=JOptionPane.showConfirmDialog(f,
               "確定要結束程式嗎?",
               "確認訊息",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.WARNING_MESSAGE);
    if (result==JOptionPane.YES_OPTION) {System.exit(0);}
    }   
  }
class MyTableModel extends AbstractTableModel {
  Object[][] data={
    {"Kelly","Female",new Integer(16),false,"kelly@gmail.com","O"},
    {"Peter","Male",new Integer(14),false,"peter@gmail.com","O"},
    {"Amy","Female",new Integer(12),false,"amy@gmail.com","O"},
    {"Tony","Male",new Integer(49),true,"tony@gmail.com","O"}};
  String[] columns={"Name","Gender","Age","Vegetarian","E-mail","Blood Type"};
  Class[] types={String.class,String.class,Number.class,Boolean.class,
    String.class,String.class
};
  //定義各欄位資料型態之 Class 物件 
  public int getColumnCount() {return columns.length;}
  public int getRowCount() {return data.length;}
  public Object getValueAt(int row, int col) {return data[row][col];}
  public String getColumnName(int col) {return columns[col];}
  public Class getColumnClass(int col) {
    return types[col];  //傳回資料型態的 Class 物件
    }

  public boolean isCellEditable(int row,int col) {return true;}
  public void setValueAt(Object value,int row,int col) {
    data[row][col]=value;
    fireTableCellUpdated(row,col);
    }
  }


此例與前例的效果相同.