概述
JTable通過TableCellRenderer和TableCellEditor這兩個接口實現每個單元格的渲染和編輯,這兩個接口都各有一個方法可以返回JComponent的子類,例如BooleanRenderer對應的是Boolean類型的數據,getTableCellRendererComponent返回的是JCheckBox:
class BooleanRenderer extends JCheckBox implements TableCellRenderer, UIResource
{
private final Border noFocusBorder = new EmptyBorder(1, 1, 1, 1);
public BooleanRenderer() {
super();
setHorizontalAlignment(JLabel.CENTER);
setBorderPainted(true);
}
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
if (isSelected) {
setForeground(table.getSelectionForeground());
super.setBackground(table.getSelectionBackground());
}
else {
setForeground(table.getForeground());
setBackground(table.getBackground());
}
setSelected((value != null && ((Boolean)value).booleanValue()));
if (hasFocus) {
setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
} else {
setBorder(noFocusBorder);
}
return this; //返回自己,也就是JCheckBox
}
}
從如上代碼中可以看出,getTableCellRendererComponent方法(TableCellRenderer中的唯一方法)返回的Component就是一個JCheckBox,因此使用個Renderer會在表格的單元格中顯示一個勾選框。
只有Renderer只能顯示靜態數據,如果需要支持對數據進行編輯,那麼必須同時實現Editor,不然點擊單元格進行編輯時會使用默認的Editor,而默認的Editor使用的是JTextField,所以就會發現點完之後變成“true”和“false”了:
如果只實現Editor也不行,會變成只有在點擊的時候顯示成Checkbox,其他時候是文本框。
整體設計
在本博客中,我們要實現通過自定義的類型,讓JTable用不同的UI組件進行顯示,並支持根據數據模型的狀態改變單元格的背景色。
設計思路是定義一個MColoredValue類,該類用於保存數據以及保存是否添加背景色。
public class MColoredValue<T> {
T value;
boolean colored;
public MColoredValue(T v, boolean colored){
this.value = v;
this.colored = colored;
}
public T getValue() {
return value;
}
public boolean isColored() {
return colored;
}
public void setValue(T value) {
this.value = value;
}
public void setColored(boolean colored) {
this.colored = colored;
}
@Override
public String toString() {
return ""+value;
}
}
根據我們的設計,如果isColored爲true,那麼給單元格使用紅色背景色,否則用白色。然後我們的數據又分Boolean類型和String類型,我們希望Boolean類型使用CheckBox的形式來顯示,String就用默認的文本框來顯示。
public class MColoredBoolValue extends MColoredValue<Boolean> {
public MColoredBoolValue(Boolean v, boolean colored){
super(v, colored);
}
@Override
public boolean isColored(){
return this.value;
}
}
public class MColoredStringValue extends MColoredValue<String> {
public MColoredStringValue(String v, boolean colored){
super(v, colored);
}
}
然後我們實現TableCellRenderer和TableCellEditor接口,但只作爲一個殼來使用,在JTable調用getTableCellRendererComponent和getTableCellEditorComponent實例化具體的類,例如如果傳入的是MColoredBoolValue類型,那麼我們就實例化對應的BooleanRenderer和BooleanEditor,否則用默認的Renderer和Editor。
TableCellRenderer
TableCellRenderer接口中只有getTableCellRendererComponent這一個方法,該方法會傳入一個Object類型的參數,該參數就是我們設置到JTable某個單元格里面的值,因此我們可以通過判斷這個參數的類型來選擇使用不同的Renderer,比如Boolean類型可以用勾選框或下拉框來顯示。
import java.awt.Color;
import java.awt.Component;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.UIResource;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
public class MCellRenderer implements TableCellRenderer {
private TableCellRenderer renderer;
//從JTable源碼裏抄過來即可
class BooleanRenderer extends JCheckBox implements TableCellRenderer, UIResource
{
private final Border noFocusBorder = new EmptyBorder(1, 1, 1, 1);
public BooleanRenderer() {
super();
setHorizontalAlignment(JLabel.CENTER);
setBorderPainted(true);
}
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
if (isSelected) {
setForeground(table.getSelectionForeground());
super.setBackground(table.getSelectionBackground());
}
else {
setForeground(table.getForeground());
setBackground(table.getBackground());
}
setSelected((value != null && ((Boolean)value).booleanValue()));
if (hasFocus) {
setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
} else {
setBorder(noFocusBorder);
}
return this;
}
}
@Override
public Component getTableCellRendererComponent(JTable table, Object obj, boolean isSelected, boolean hasFocus, int row,
int column) {
Component component;
if (obj instanceof MColoredBoolValue){
MColoredBoolValue value = (MColoredBoolValue)obj;
renderer = new BooleanRenderer();
component = renderer.getTableCellRendererComponent(table, value.getValue(), isSelected, hasFocus, row, column);
if (value.isColored()){
component.setBackground(Color.RED);
}else{
component.setBackground(Color.WHITE);
}
}else if(obj instanceof MColoredStringValue){
MColoredStringValue value = (MColoredStringValue)obj;
renderer = new DefaultTableCellRenderer();
component = renderer.getTableCellRendererComponent(table, value.getValue(), isSelected, hasFocus, row, column);
if (value.isColored()){
component.setBackground(Color.RED);
}else{
component.setBackground(Color.WHITE);
}
}else if (obj instanceof Boolean){
renderer = new BooleanRenderer();
component = renderer.getTableCellRendererComponent(table, obj, isSelected, hasFocus, row, column);
component.setBackground(Color.WHITE);
}
else{
renderer = new DefaultTableCellRenderer();
component = renderer.getTableCellRendererComponent(table, obj, isSelected, hasFocus, row, column);
component.setBackground(Color.WHITE);
}
return component;
}
}
TableCellEditor
這個接口稍微複雜一點,主要是因爲需要處理用戶的操作事件,例如點擊,結束點擊等,但因爲我們只是實現一個殼,因此這些方法都只需要調用實例化後對象的對應方法即可。
import java.awt.*;
import java.util.EventObject;
import javax.swing.DefaultCellEditor;
import javax.swing.JCheckBox;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.border.LineBorder;
import javax.swing.event.CellEditorListener;
import javax.swing.table.TableCellEditor;
import javax.swing.JComponent;
import sun.reflect.misc.ReflectUtil;
import sun.swing.SwingUtilities2;
public class MCellEditor implements TableCellEditor {
private TableCellEditor editor;
/**
* Default Editors 從JTable源碼裏抄過來即可
*/
class GenericEditor extends DefaultCellEditor {
Class[] argTypes = new Class[]{String.class};
java.lang.reflect.Constructor constructor;
Object value;
public GenericEditor() {
super(new JTextField());
getComponent().setName("Table.editor");
}
public boolean stopCellEditing() {
String s = (String)super.getCellEditorValue();
// Here we are dealing with the case where a user
// has deleted the string value in a cell, possibly
// after a failed validation. Return null, so that
// they have the option to replace the value with
// null or use escape to restore the original.
// For Strings, return "" for backward compatibility.
try {
if ("".equals(s)) {
if (constructor.getDeclaringClass() == String.class) {
value = s;
}
return super.stopCellEditing();
}
SwingUtilities2.checkAccess(constructor.getModifiers());
value = constructor.newInstance(new Object[]{s});
}
catch (Exception e) {
((JComponent)getComponent()).setBorder(new LineBorder(Color.red));
return false;
}
return super.stopCellEditing();
}
public Component getTableCellEditorComponent(JTable table, Object value,
boolean isSelected,
int row, int column) {
this.value = null;
((JComponent)getComponent()).setBorder(new LineBorder(Color.black));
try {
Class<?> type = table.getColumnClass(column);
// Since our obligation is to produce a value which is
// assignable for the required type it is OK to use the
// String constructor for columns which are declared
// to contain Objects. A String is an Object.
if (type == Object.class) {
type = String.class;
}
ReflectUtil.checkPackageAccess(type);
SwingUtilities2.checkAccess(type.getModifiers());
constructor = type.getConstructor(argTypes);
}
catch (Exception e) {
return null;
}
return super.getTableCellEditorComponent(table, value, isSelected, row, column);
}
public Object getCellEditorValue() {
return value;
}
}
class NumberEditor extends GenericEditor {
public NumberEditor() {
((JTextField)getComponent()).setHorizontalAlignment(JTextField.RIGHT);
}
}
class BooleanEditor extends DefaultCellEditor {
public BooleanEditor() {
super(new JCheckBox());
JCheckBox checkBox = (JCheckBox)getComponent();
checkBox.setHorizontalAlignment(JCheckBox.CENTER);
}
}
class ColoredStringEditor extends GenericEditor {
private MColoredStringValue value;
public ColoredStringEditor() {
super();
}
public MColoredStringValue getCellEditorValue() {
String vString = (String)super.getCellEditorValue();
value.setValue(vString);
return value;
}
public Component getTableCellEditorComponent(JTable table, Object value,
boolean isSelected,
int row, int column) {
this.value = (MColoredStringValue)value;
return super.getTableCellEditorComponent(table, this.value.getValue(), isSelected, row, column);
}
}
class ColoredBooleanEditor extends DefaultCellEditor {
private MColoredBoolValue value;
public ColoredBooleanEditor() {
super(new JCheckBox());
JCheckBox checkBox = (JCheckBox)getComponent();
checkBox.setHorizontalAlignment(JCheckBox.CENTER);
}
public MColoredBoolValue getCellEditorValue() {
Boolean vBoolean = (Boolean)super.getCellEditorValue();
value.setValue(vBoolean);
return value;
}
public Component getTableCellEditorComponent(JTable table, Object value,
boolean isSelected,
int row, int column) {
this.value = (MColoredBoolValue)value;
return super.getTableCellEditorComponent(table, this.value.getValue(), isSelected, row, column);
}
}
@Override
public void addCellEditorListener(CellEditorListener arg0) {
// TODO Auto-generated method stub
editor.addCellEditorListener(arg0);
}
@Override
public void cancelCellEditing() {
// TODO Auto-generated method stub
editor.cancelCellEditing();
}
@Override
public Object getCellEditorValue() {
// TODO Auto-generated method stub
return editor.getCellEditorValue();
}
@Override
public boolean isCellEditable(EventObject arg0) {
// TODO Auto-generated method stub
return true;
}
@Override
public void removeCellEditorListener(CellEditorListener arg0) {
// TODO Auto-generated method stub
editor.removeCellEditorListener(arg0);
}
@Override
public boolean shouldSelectCell(EventObject anEvent) {
// TODO Auto-generated method stub
return true;
}
@Override
public boolean stopCellEditing() {
return editor.stopCellEditing();
}
@Override
public Component getTableCellEditorComponent(JTable table, Object obj, boolean isSelected, int row, int column) {
Component component;
if (obj instanceof MColoredBoolValue){
editor = new ColoredBooleanEditor();
component = editor.getTableCellEditorComponent(table, obj, isSelected, row, column);
if (((MColoredBoolValue)obj).isColored()){
component.setBackground(Color.RED);
}else{
component.setBackground(Color.WHITE);
}
}else if(obj instanceof MColoredStringValue){
editor = new ColoredStringEditor();
component = editor.getTableCellEditorComponent(table, obj, isSelected, row, column);
if (((MColoredStringValue)obj).isColored()){
component.setBackground(Color.RED);
}else{
component.setBackground(Color.WHITE);
}
}else if (obj instanceof Boolean){
editor = new BooleanEditor();
component = editor.getTableCellEditorComponent(table, obj, isSelected, row, column);
component.setBackground(Color.WHITE);
}
else{
editor = new GenericEditor();
component = editor.getTableCellEditorComponent(table, obj, isSelected, row, column);
component.setBackground(Color.WHITE);
}
return component;
}
}
上面代碼中的edit屬性就是TableCellEditor的具體實例,除了getTableCellEditorComponent的時候需要根據數據類型找到對應的類進行實例化之外,其他方法都只需調用editor的對應方法即可,其中isCellEditable和shouldSelectCell會在editor被實例化之前就被調用,因此只能我們自己來實現。
其中有一個關鍵點,但用戶通過操作更改了Cell裏的值的時候,我們需要把值存回我們自定義的對象裏面。比如用戶在Checkbox裏勾選了一下,這時Checkbox返回的肯定是true值,但我們需要MColoredBoolValue類型,因爲這個類型裏面保存了背景色,所以我們在ColoredBooleanEditor這個類裏面修改了getCellEditorValue方法,讓這個方法返回MColoredBoolValue類型。爲了記住背景色狀態,我們在getTableCellEditorComponent方法中保存了Object參數到屬性value中,然後在getCellEditorValue時修改了value的值然後返回。
還有一個需要注意的地方,我們在TableCellEditor的getTableCellEditorComponent裏也添加了修改背景色的代碼,如果去掉這些代碼,那麼在用戶對單元格進行編輯的時候背景色會不一致,因爲修改的時候JTable顯示的是TableCellEditor返回的Component,在修改完之後顯示的是TableCellRenderer返回的Component。