JTable通過TableCellRenderer和TableCellEditor支持自定義數據類型(checkbox等)

概述

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。

最終效果

在這裏插入圖片描述在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章