Building a Professional Swing JTable
- Making the Table Editable
- The Cell Renderer
- The Cell Editor
- Conclusion
The API is extremely powerful and flexible, but along with that flexibility comes complexity. Although this complexity can be overwhelming, after you have mastered part of it, the rest falls into place rather easily.
I will start with the following code, which was created in my last article. This class is a custom table model that I created to avoid using the DefaultTableModel and all of the inherent difficulties that come with it:
class MyTableModel extends AbstractTableModel { private ArrayList datalist = new ArrayList(); private String[] columns = {"Name", "Value", "Location", "Quantity"}; public Widget getWidgetAt(int row) { return (Widget)datalist.get(row); } public Widget removeWidgetAt(int row) { return (Widget)datalist.remove(row); } public void addWidget(Widget w) { datalist.add(w); fireTableDataChanged(); } public void addWidgetList(List l) { datalist.addAll(l); fireTableDataChanged(); } public MyTableModel(List l) { datalist.addAll(l); } public MyTableModel() { } public int getRowCount() { return datalist.size(); } public String getColumnName(int i) { return columns[i]; } public int getColumnCount() { return columns.length; } public Object getValueAt(int row, int col) { Widget widget = (Widget) datalist.get(row); switch (col) { case 0: return widget.getName(); case 1: return String.valueOf(widget.getValue()); case 2: return widget.getLocation(); case 3: return String.valueOf(widget.getQuantity()); default: return null; } } }
Making the Table Editable
The next thing is to make the table that displays the data editable. A JTable determines whether a cell is editable by asking the Table Model. The method, isCellEditable(int row, int col), answers this question for the JTable. The AbstractTableModel that I extended already implemented this method for me and always returns false. This "default" behavior automatically makes the entire table non-editable. To change this behavior I will overload a few methods. First, let me show the code; then I will walk you through each method:
public boolean isCellEditable(int row, int col) { switch (col) { case 0: //Name return false; case 1: //value return true; case 2: //location return true; case 3: //quantity return true; default: return false; } } public Class getColumnClass(int col) { switch (col) { case 0: //Name return String.class; case 1: //value return Double.class; case 2: //location return String.class; case 3: //quantity return Integer.class; default: return null; } } public void setValueAt(Object value, int row, int col) { Widget w = (Widget)datalist.get(row); switch (col) { case 0: //Name w.setName(value.toString()); break; case 1: //value Double _value = (Double)value; w.setValue(_value.doubleValue()); break; case 2: //location w.setLocation(value.toString()); break; case 3: //quantity Integer _quantity = (Integer)value; w.setQuantity(_quantity.intValue()); break; } }
The first method that I am overloading, isCellEditable(),merely returns true because I want my entire table to be editable. I could just as easily do a switch statement inside of the method and choose specific columns and/or rows to make editable. This one method decides exactly which cells are editable.
The second method, getColumnClass(), tells the view which type of object will be displayed. This allows the JTable to display the data in a way that is most appropriate for the type of object that exists in that row. For instance, if I had a boolean value stored in my Widget class and informed my view that I would be returning a Boolean object to it, the JTable would display that boolean value as a check box instead of the word true or false.
What is returned from getColumnClass() does not match up with what is returned from the getValueAt() method. (This was originally done for clarity in my other article.) So to avoid any class casting exceptions, I correct the getValueAt() method:
public Object getValueAt(int row, int col) { Widget widget = (Widget) datalist.get(row); switch (col) { case 0: return widget.getName(); case 1: return new Double(widget.getValue()); case 2: return widget.getLocation(); case 3: return new Integer(widget.getQuantity()); default: return null; } }
The primary difference between this version of the getValueAt() method and the previous version is that the primitive values are now wrapped in their immutable classes instead of being wrapped in a String.
The final method, setValueAt(), handles changing the underlying values. This method passes in a generic object and lets me know which row and column the object is to be assigned. Knowing this, I can grab the correct widget from my ArrayList and drop it into a switch statement. Because I have already informed the JTable what type of object is in each column, I know that the JTable will pass the correct object type into this method. Therefore, it is safe to cast the value object to the object type that I require.
I can take this example one step further. If my Widgets were in fact stored in a database, I could also add the database code into the setValueAt() method. If I do this, however, I would want to shunt the database code off to a separate thread because this method is being called from the Event Thread.
These simple changes allow my table model to be editable via a JTable. Additionally, I can add other methods into my table model so I can access this data without having to cast it out of the getValueAt() method. I can easily make the table model into a type of persistent storage if I want. In fact, in an EJB environment, the Widget objects could very easily be remote EJB references which would guarantee correct persistence.