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



3 則留言 :

Unknown 提到...

不好意思請問一下 如果我的程式寫成
tabbedPane.addTab("Tab ", new JTable(new DefaultTableModel()));
那我該怎麼addRow進model裡呢??

小狐狸事務所 提到...

我好久沒寫 Java 囉, 您可以先參考這篇看看 :

http://yhhuang1966.blogspot.tw/2014/05/java-swing-jtabbedpane.html

匿名 提到...

TO 李穎琪:

您可以把程式改成這樣

DefaultTableModel dtm = new DefaultTableModel();
JTable jt = new JTable(dtm);

tabbedPane.addTab("Tab ", jt);

這樣一來就可以利用前兩個物件來操作囉~