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);
    }
  }


此例與前例的效果相同.


5 則留言 :

依冬 提到...
作者已經移除這則留言。
Unknown 提到...
作者已經移除這則留言。
Unknown 提到...

講解得很詳細,讓我節省不少時間。

kolyfish 提到...

感謝您,請問您可以做相關 上傳Excel到 jFrame的功能嗎?

小狐狸事務所 提到...

其實我已經有十年沒寫 Java 了, 但透過 AI 卻輕易寫出上傳功能了, 建議您可以善用 Chateverywhere (當然還是要有 Java 基礎較好) :

https://chateverywhere.app/zh

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import javax.swing.filechooser.FileNameExtensionFilter;

public class ExcelUploader extends JFrame {
private JButton uploadButton;

public ExcelUploader() {
setTitle("Excel Uploader");
setSize(300, 200);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

uploadButton = new JButton("Upload Excel file");
uploadButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JFileChooser fileChooser = new JFileChooser();
FileNameExtensionFilter filter = new FileNameExtensionFilter("Excel 檔案", "xls", "xlsx");
fileChooser.setFileFilter(filter);

int returnValue = fileChooser.showOpenDialog(null);
if (returnValue == JFileChooser.APPROVE_OPTION) {
File selectedFile = fileChooser.getSelectedFile();
// 在這裡處理上傳的 Excel 檔案
// 可以使用 Apache POI 或其他相關函式庫來讀取 Excel 內容
}
}
});

getContentPane().add(uploadButton);
setVisible(true);
}

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new ExcelUploader();
}
});
}
}