概述
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。
最终效果


本文介绍了如何通过实现TableCellRenderer和TableCellEditor接口,为JTable创建自定义数据类型的支持,特别是如何使用CheckBox。在设计中,定义了一个MColoredValue类来保存数据和背景色状态。针对不同数据类型,如Boolean和String,实现特定的Renderer和Editor。通过Editor处理用户操作事件,并确保在编辑时正确更新和保存数据模型的状态。
8124

被折叠的 条评论
为什么被折叠?



