Swing框架之Renderer

Swing组件根据其所操作的数据类型分为两种,一种是标量数据类型的组件,一类是复合数据类型的组件。标量数据类型的组件操作的是基本类型的数据,如字符串、布尔、数字等,此类型组件包括JTextField、JCheckBox、JLabel、JButton等。复合数据类型的组件操作的是诸如向量、矩形和非线形等类型的数据。向量数据类型的组件有JComboBox、JList,矩形数据类型的组件有JTable,非线形数据类型的组件如JTree。

       为更形象地展现各种类型的数据,复合数据类型的组件往往采用标量数据类型组件来表现每种数据元素。比如JTable的某一列数据是字符串类型,那么该列的单元格往往用JLabel方式展现每个字符串;如果一列数据是布尔类型,那么该列的单元格往往用JCheckBox方式展现每个布尔值。

       如何实现复合数据类型的组件的渲染呢?最直接的是在paint方法中一个一个地根据数据类型画出每一个组件,但这种方法很显然代码复用率很低,大量重复了相应标量型组件的代码,代码的维护和同步会非常困难,也不容易实现皮肤切换。

       为解决此问题,Swing体系中提出了所谓渲染器(Renderer)的概念,其核心思想是使用接口,封装和复用已有标量型组件的渲染代码,降低代码重复率,提高组件的可扩展性。

       渲染器是如何工作的呢?我们以JTable的渲染代码来演示渲染器的工作原理。下面代码是从JTable的UI类javax.swing.plaf.basic.BasicTableUI中摘出的部分代码(JDK6):

  //注:为清晰期间,只显示了关键代码
    publicvoid paint(Graphics g, JComponent c) {
      ......
      //计算当前可显示区域的行列范围
      ......
       // 画出表格的分割线
       paintGrid(g, rMin, rMax, cMin, cMax);

       // 画出表格单元格的内容
   paintCells(g, rMin, rMax, cMin, cMax);

       //如果正在拖动列,画出正在被拖动的线
       paintDropLines(g);
   }

       paintCells实现所有单元格的渲染过程:

    privatevoid paintCells(Graphics g, int rMin, int rMax, int cMin, int cMax){
          ......
       for(int row = rMin; row <= rMax; row++) {
       cellRect = table.getCellRect(row, cMin, false);
               for(int column = cMin; column <= cMax; column++){
                  //遍历表格的每一个单元格
                   .......
                   paintCell(g, cellRect, row, column);
                   cellRect.x += columnWidth;
              }
           }
         ......
   }

        渲染单元格的方法paintCell的实现:

    privatevoid paintCell(Graphics g, Rectangle cellRect, int row, int column){
       if (如果当前格是正在编辑的) {
            //重新设置编辑器的边框
            ......
       }
       else {
            //获取当前格的渲染器
           TableCellRenderer renderer = table.getCellRenderer(row,column);
           //配置当前的渲染器
           Component component = table.prepareRenderer(renderer, row,column);
           //使用渲染器来渲染当前表格
           rendererPane.paintComponent(g, component, table, cellRect.x,cellRect.y,
                                       cellRect.width, cellRect.height, true);
       }
   }

    publicComponent prepareRenderer(TableCellRenderer renderer, int row, intcolumn) {
       Object value = getValueAt(row, column);
       ......
       //调用渲染器的配置方法配置并获取合适的渲染组件
    returnrenderer.getTableCellRendererComponent(this,value,
                                                 isSelected, hasFocus,
                                                 row, column);
   }

       如何使用渲染器返回的组件渲染当前的单元格呢?JTable在自己内部隐藏了一个所谓的CellRendererPane组件,该组件是一个“零实现”的容器组件。虽然被添加到JTable上,但它是不可见的,其paint和update方法都为空,仅仅作为临时容纳渲染组件的容器,目的是将渲染组件粘合到JTable组件树上,使得渲染组件有效化,以便使它们达到渲染前的正确状态。下面代码演示了CellRendererPane的概要结构:

publicclass CellRendererPane extends Container implementsAccessible
{
   //构造函数
    publicCellRendererPane() {
   super();
   //注意CellRendererPane的布局管理器为空,后面渲染时有用!
   setLayout(null);
   //不可见,使之不被显示在JTable上
   setVisible(false);
    }
   //零实现
    public voidinvalidate() { }
   //零实现
    public voidpaint(Graphics g) { }
   //零实现
    public voidupdate(Graphics g) { }
   ......
  //下面是CellRendererPane的paintComponent方法:
    public voidpaintComponent(Graphics g, Component c, Container p, int x, int y,int w, int h, boolean shouldValidate) {
         ......
    if(c.getParent() != this){//如果渲染组件c还没有添加当前CellRendererPane中
        //添加进去
       this.add(c);
    }
//构造函数将布局管理器设置为空,setBounds将渲染组件设置成相应的位置和尺寸


   c.setBounds(x, y, w, h);
//将渲染组件在JTable的组件树上有效化,目的是如果渲染组件是一个有内部结构的复合组件,比如一个JTextField和一个JButton构成的一个复合框组件,该过程将会使内部组件进行布局,并递归此过程,使得该组件达到正常显示应该达到的效果。
   if(shouldValidate) {
       c.validate();
    }
//下面主要处理双缓冲问题,可略去
 ......
   //准备图形对象
    Graphics cg= g.create(x, y, w, h);
    try {
//调用渲染组件的重画方法渲染,注意由于cg其实是JTable的图形对象,因此其效果是将该组件渲染到JTable上相应单元格,从而达到了代码复用。
       c.paint(cg);

    }
          ......
    }
    ......
}

       渲染器的核心思想都体现在上面红色代码标注的部分。将JTable的图形对象传递给组件的paint的方法,产生的结果是将组件画到了JTable上。其实Swing打印的原理也大抵如此,只不过这儿的图形对象变成了打印机的图形对象。虽然大部分Swing组件都专门对打印进行了专门的处理(主要是因为有一些图形元素不希望被打印的,比如填充的内容往往不希望打印,可能是太耗墨了),但基本过程是一样的。

       渲染器的思想很像是摄像机、镜子等成像原理。作个比喻,如果你想获取某人的图像,一种方法是将此人一点点用笔画出来。另种方法是通过光线将此人照到镜子里或用照相机拍摄下来。其好处是不管是什么物体,都可以映射出来,具有很强的可扩展性。比如JTable中,表格中不仅可以使用JLabel、JCheckBox、JComboBox等简单组件作为渲染器,而且可以使用其它任何的Swing组件进行渲染,包括复杂的组件JTable(比如实现表格套表的风格)、自定义的组件(比如嵌入图片)。渲染器方法带来的好处不仅仅是组件代码的复用,更带来了无限的可扩展性!

       渲染器思想在Swing中有着广泛的应用。除利用它们实现JTable、JList、JTree和JComboBox等标准组件,还可以实现界面设计工具中属性页、类似UML设计图、类似于MSExcel风格的电子表格等更为复杂的界面组件,甚至IDE中常见的界面设计工具也是利用了渲染器的思想,它把整个组件树当作一个大渲染器,渲染出当前图形用户界面的设计效果。

       渲染器是Swing展现复杂数据结构的利器。但是Swing组件不仅仅被用作展现数据,通常还是编辑数据的地方。实际上纯粹展现数据的Swing组件很少,标准组件中也许只有JLabel。复合数据类型的组件往往使用渲染器原理实现组件的渲染,使用所谓in-placeeditor实现组件的编辑。渲染器Renderer和Editor的结合赋予了Swing强大的灵活性,JTable、等组件这两种原理结合的代表。


Swing的API具有很强的灵活性和可扩展性,比如标准复合数据型组件一般不需要进行渲染器扩展,就可以实现许多应用,但是当遇到需要自定义扩展的需求时,Swing的高度抽象灵活的MVC框架也可以优雅从容的完成。Swing的这一特色典型的体现在其渲染器扩展思想上。那么如何使用渲染器呢?如何自定义渲染器来扩展组件呢?如何将渲染器思想应用到自定义组件上呢?

  复合数据类型的组件如JTable、JTree、JList以及JComboBox都定义适合自己类型的渲染器接口,它们与渲染器接口之间的映射关系如下表所示:

 组件  渲染器
 JTable  TableCellRenderer
 JTree  TreeCellRenderer
 JList  ListCellRenderer
 JComboBox  ListCellRenderer

  TableCellRenderer接口定义了JTable渲染器接口:

public interface TableCellRenderer{
/*返回渲染表格的组件,使用该方法在渲染之前配置渲染器。
 *参数table:请求渲染器渲染的表,可以为空。
 *参数value:要渲染的单元格的值,由渲染器来决定如何解释和渲染该值。比如如果value的值为字符串“true”,渲染器可以渲染成字符串,也可以渲染成一个checkbox。该值可以为空。
 *参数isSelected:当前表格是否被选中,渲染器应据此决定是否应该高亮显示。
 *参数hasFocus:当前表格是否拥有焦点,渲染器应据此进行特殊渲染,比如画一个虚线框。
 *参数row:当前表格的行号。如果渲染的是表头,该值为-1。
 *参数column:当前表格的列号。
 */
    ComponentgetTableCellRendererComponent(JTable table, Object value,
        boolean isSelected, boolean hasFocus,
        int row, int column);
}

  TreeCellRenderer定义了JTree的渲染器接口。

public interface TreeCellRenderer{
/*将树当前节点的值赋给value,如果isSelected是true,当前节点要被渲染成选中状态,如果expanded是true,当前节点是处于打开状态,如果leaf是true,当前代表的是一个叶子节点,如果hasFocus是true,表示当前节点拥有焦点。tree是需要渲染的树。改方法返回一渲染组件渲染当前树的节点。*/
   Component getTreeCellRendererComponent(JTree tree, Objectvalue,
      boolean selected, boolean expanded,
      boolean leaf, int row, boolean hasFocus);
}

 ListCellRenderer是JList、JComboBox的渲染器接口。

public interfaceListCellRenderer

{
   /*返回一渲染组件显示列表中的某个选项。参数list是正在渲染的列表,value是列表中当前正在渲染的项,index是当前正在渲染的项的索引,isSelected是当前项是否选中,cellHasFocus是指当前项是否拥有焦点。*/
   Component getListCellRendererComponent(
       JList list,
       Object value,
       int index,
       boolean isSelected,
       boolean cellHasFocus);
}

 如何使用这些渲染器接口来实现对这些组件的扩展呢?Jtable中许多方法来实现渲染器的设置,比如方法:

public voidsetDefaultRenderer(Class columnClass, TableCellRendererrenderer);

 如果在TableColumn中没有设置渲染器的话,该方法设置对于数据类型columnClass应该使用的渲染器。也可以使用
jTable.getColumnModel().getColumn(columnIndex).setCellRenderer(newMyTableCellRenderer())设置columnIndex列的渲染器。最直接的方法是继承JTable,覆盖publicTableCellRenderer getCellRenderer(int row, int column)方法,返回任何位置的TableCellRenderer。该方法使我们能轻易实现类似于NetBeans界面设计工具的属性表:

   

 在CSDN上有人问如何实现表套表,实际如果清楚了渲染器的工作原理可以很容易实现这个功能,下面是一个很短的代码,它实现了在表的2,3表格中插入一表:

public class TableOfTable extendsJTable{
   public TableOfTable() {
       //添加一个缺省model,实际中可以根据自己需求定制。
       setModel(new DefaultTableModel(...... ));
       //将第二行高度设置宽一些,使嵌入的表格显示起来好看些。
       setRowHeight(1,super.getRowHeight()*4);
   }
   //重载getCellRenderer提供自己的TableCellRenderer
   public TableCellRenderer getCellRenderer(int row, int column){
       if(row == 1 &&column==2){//在第二行、第三列提供一个子表的渲染器
           return new TableCellRenderer(){
               //子表,可以自己定制子表的内容。
               JTable subTable=new JTable(newDefaultTableModel(......));
               //实现TableCellRenderer的方法,提供该子表作为渲染器
               public Component getTableCellRendererComponent(JTable table, Objectvalue, boolean isSelected, boolean hasFocus, int row, int column){
                   return subTable;
               }
           };
       }else//如果是其他地方的表格,沿用父类中提供的渲染器
           return super.getCellRenderer(row, column);
   }
}

  短短几行代码就可以实现如下图所示的界面:

   

  当然你可以根据需求具体定制更为复杂表格的实现,但大体原理就是这样的。

  Jtable、JList、JTree以及JComboBox可以接受的渲染组件不仅仅可以是标准组件,也可以是自定义的组件。比如想实现如下所示的颜色选择器:


   

  图中的渲染器可以从基本的BasicComboBoxRenderer继承,它实际上是继承JLabel的一个类,你可以设置该JLabel的图标,让它显示当前的颜色。为了提供这样一个显示颜色的图标,可以实现这样一个ColorIcon:

public class ColorIcon implementsIcon{
static final int BOX=10;
private Color color;
public ColorIcon(Color c){
color=c;
}
public void paintIcon(Component c,Graphics g, int x, int y) {
Color old = g.getColor();
g.setColor(color);
g.fillRect(x,y,BOX,BOX);
g.setColor(Color.black);
g.drawRect(x,y,BOX,BOX);
g.setColor(old);
}

public int getIconWidth() {
return BOX;
}

public int getIconHeight() {
return BOX;
}
}

  然后自定义一个渲染器:

public class ColorRendererextends BasicComboBoxRenderer {
private JComboBox combo;
public ColorRenderer(JComboBox cb){
combo=cb;
setFont(combo.getFont());
}
public ComponentgetListCellRendererCompo nent(
JList list,
Object value,
int index,
boolean isSelected,
boolean cellHasFocus) {
super.getListCellRendererCompo nent(list, value, index, isSelected,cellHasFocus);
JLabel lbl=(JLabel)this;
if(value!=null){
Color c=(Color)value;
lbl.setText(ColorIcon.getColorName(c));
lbl.setIcon(newColorIcon(c));
}
return this;
}
}

  最后,在程序中使用它:

JcomboBox colorCombo=newJComboBox();
colorCombo.setRenderer(newColorRenderer(this));
addItem(Color.red);
addItem(Color.orange);
......

 其实,渲染器不仅仅可以用在标准组件JTable、JList、JTree和JComboBox,也可以在自己定制的组件中使用渲染器的思想实现复杂的界面,比如UML图、工作流图、电路图,模拟JTable实现类似于MSExcel的电子表格控件,甚至可以实现自己的用户界面设计工具。前面文章中曾经提到过的数据库设计插件和报表设计插件就是根据渲染器原理自定义出的组件。

 因此,熟悉了Swing的结构尤其是渲染器的思想,加上一些额外的知识,比如doublebuffering、glass pane、robot、swing threading、colormodel、java2d等等,可以做出许多事情来。人有多大胆,地有多大产。但深入学习和了解Swing的基本结构,这是前提。今天的文章主要是以实例演示了这些渲染器的应用,文中的例子只是演示作用,加深你对渲染器的印象。但是真正吃透渲染器的各种技术,还需要自己深入的学习和实践。 


前面文章提到过,许多复合数据型组件不仅仅需要展现数据,还需要编辑数据。比如JTable的某些单元格可能需要编辑,JComboBox除了选择外还可以直接编辑数据,有些JTree有时也需要直接编辑节点。Swing中解决此类问题的方案叫做所谓的in-placeeditor。Swing综合in-placeeditor和Renderer原理赋予了Swing扩展复杂组件功能的能力。注意in-placeeditor其实只是普通的Swing组件,并不是新的Swing元素,它们往往结合Renderer完成组件的扩展,所以我把它们和Renderer放在一起讲了。

  Swing可以使用In-placeeditor的两个前提条件是:

  1. Swing组件需要能脱离容器组件独立存在;
  2. Swing组件树能在运行时动态地被修改,包括添加和删除。

  Swing组件树就像浏览器的HTMLDOM树一样,可以通过脚本语言JavaScript进行动态修改,从而达到浏览器页面的局部更新的功能,Swing通过动态的往容器组件中添加或者删除编辑组件实现复合类型组件的in-placeeditor功能。

 所有Swing组件都是直接或者间接继承自JComponent,而JComponent是Container类的一个子类,因此所有的Swing组件都可看作一个容器组件。复合数据类型的组件也是如此,它们内部往往拥有自己的子结构、布局管理器、为实现某种功能而隐藏了的子组件(比如前篇文章中所提到的CellRendererPane)等。复合数据类型的组件通过动态修改自己内部组件树结构实现in-placeeditor。

  In-placeeditor的生命周期包括激活、编辑、停止编辑、取消编辑、删除编辑器等。下面以JTable为例详细了解In-placeeditor的工作过程。

  首先要了解JTable的in-placeeditor接口TableCellEditor的定义:

public interface TableCellEditor extendsCellEditor {
   /*该方法返回JTable当前表格的编辑组件,该组件根据提供的参数table、value、isSelected、row、column进行配置。注意,对该组件的调用有可能会导致停止编辑,丢失尚未编辑完的数据。该组件将作为JTable的子组件添加到组件树上,之后就可以接受用户的输入了。*/
    ComponentgetTableCellEditorComponent(JTable table, Object value,
      boolean isSelected,
      int row, int column);
}

public interface CellEditor {
/*返回编辑器内的值,必须是编辑后有效的值,用在JTable中作为获取编辑结果用*/
    publicObject getCellEditorValue();
/*让编辑器判断anEvent是否能激活编辑动作,anEvent使用的是JTable组件坐标系,这儿编辑器不能假设getCellEditorComponent返回的组件已经安装在JTable中。如果当前事件触发编辑行为,则返回true,否则false。*/
    publicboolean isCellEditable(EventObject anEvent);
/*该方法同前一方法类似,只不过是判断该事件是否允许选中当前格。大多数情况下,这儿直接返回true,因为一般如果可编辑,就应该允许被选中。然而有些情况下编辑行为发生时,并不想改变表格的选中状态。比如,用户可能想改变checkbox编辑器的值,却并不想因为要点击checkbox而改变JTable当前选中的其他行。*/
    publicboolean shouldSelectCell(EventObject anEvent);
/*停止编辑器的编辑,接受部分编辑的值作为编辑结果值。如果编辑不能停止,则返回false,这对于编辑器检查正在编辑的值是否有效,是否能接受无效值时很有效。*/
    publicboolean stopCellEditing();
/*告诉编辑器取消编辑,不要使用任何部分编辑过的值*/
    public voidcancelCellEditing();
/*添加CellEditorListener,当编辑器停止或者取消编辑行为时触发该处理器*/
    public voidaddCellEditorListener(CellEditorListener l);
/*删除CellEditorListener*/
    public voidremoveCellEditorListener(CellEditorListener l);
}

  再看一下CellEditorListener的定义:

public interface CellEditorListener extendsjava.util.EventListener {
   /*通知编辑器已经停止编辑,编辑效果生效。*/
    public voideditingStopped(ChangeEvent e);
   /*通知编辑器已经取消编辑,编辑效果被取消*/
    public voideditingCanceled(ChangeEvent e);
}

  当鼠标双击之类引起编辑的事件发生时,JTable的激活编辑器的过程可用下面的图来示意:

 首先检查当前是否正在编辑。如果正在编辑,则调用TableCellEditor的stopCellEditing来结束编辑。stopCellEditing如果不能正常结束,则返回false,这时便把焦点重新定位给该编辑器,并结束。如果当前不是正在编辑,或者虽然正在编辑,但是stopCellEditing正常结束了,则判断当前的鼠标事件是否是鼠标拖动行为。如果是则调用mousePressedDND方法处理。否则说明该事件有可能引发编辑功能。接着调用TableModel.isCellEditable判断该当前单元格是否允许编辑。如果允许编辑,则获得当前单元格的TableCellEditor,调用其isCellEditable判断当前鼠标事件是否意味着激活编辑(比如规定双击意味着激活编辑、单击并不激活等)。如果是激活编辑事件,则从TableCellEditor.getTableCellEditorComponent获取编辑组件,最后将该组件添加到组件树上,添加适当的处理器、有效化之后等待用户输入。

 TableCellEditor的stopCellEditing方法通常会删除当前编辑器。上面过程中,如果JTable正在处于编辑中,鼠标的点击则会导致调用TableCellEditor的stopCellEditing方法删除编辑器,并开始新的编辑器的安装过程。stopCellEditing在这个过程中获取编辑器的值并把它赋予JTable。其工作过程如下:

 tableCellEditing的stopCellEditing(左边示意图)先检查目前编辑组件的值是否有效(比如输入的整数是否越界)。如无效则(有时需要提示用户)返回false。如果有效则将该值作为editor的最终值存储起来,以备JTable通过editor.getCellEditorValue调用。然后触发编辑停止事件fireEditingStopped。由于JTable在安装编辑器时,总把自己注册为它的CellEditorListener处理器,因此当JTable能接到该通知。之后JTable就在其editingStopped(右边示意图)方法中处理该事件。JTable首先使用editor.getCellEditorValue获得该editor编辑好的值,并调用setValueAt将该值更新到当前单元格,最后调用removeEditor删除该编辑器。控制返回到TableCellEditor的stopCellEditing(左边示意图图)后,stopCellEditing返回true结束整个过程。

   TableCellEditor还会发出另一个事件fireEditingCanceled。这经常出现在编辑器本身有所谓取消功能编辑组件上。比如想在按下ESC键时取消当前正在编辑的值,就可以调用TableCellEditor的cancelEditing来取消。cancelEditing的工作过程比较简单,往往是直接通过fireEditingCanceled触发取消动作,侦听的JTable会在其editingCanceled方法中简单将编辑器删除去,继续保留以前的值。

 理解TableCellEditor接口的方法之间的关系对于编写自己的Editor很重要,它们之间除了上面注释所描述的含义外,还有一些暗示的关系编写程序时需要注意:

1.在自定义编辑器的stopCellEditing中要检查编辑组件正在编辑数据的有效性。如有效,要通过fireEditingStopped之类方法通知注册在此编辑器上的接口。如果没有触发,JTable将不能正常工作,编辑的值也会被丢失。最后要返回true作为成功的标识;如果数据没有效,一般做法是提示用户错误,用户确认错误提示后要要把焦点重新定位到编辑组件上,并返回false作为失败的标识。

2.注意在fireEditingStopped调用之前,getCellEditorValue返回的值一定要是当前有效的值。因为JTable会紧跟其后,调用该方法将编辑好的值填入表中并删除当前编辑器。

3.cancelCellEditing一般什么都不做,简单的fireEditingCanceled就行了。JTable响应该事件仅仅是简单的删除编辑器。

4.要实现方法addCellEditorListener和removeCellEditorListener,而不能空着它们,并且要定义fireEditingStopped和fireEditingCanceled两个方法以便在stopCellEditing和cancelCellEditing方法中使用它们触发事件。

5.getTableCellEditorComponent返回的组件需要添加适当的事件处理器,该处理器在用户编辑确认时(比如JTextField在敲回车引发ActionPerformed时),应该stopCellEditing以此来通知这次编辑过程已经完成,需要更新到JTable中。

 TableCellEditor接口暗含许多内部关系,如果不能正确建立它们之间的关系,实现往往并不能复合你的需求。

    JTree的TreeCellEditor接口和TableCellEditor除了获取编辑组件的方法的参数有所不同外,其他完全相同,其工作过程也类似。

  除了JTable和JTree外,JComboBox也有in-placeeditor,它的editor接口相对简单一些:

public interface ComboBoxEditor {
/*返回编辑的组件*/
public Component getEditorComponent();
/*设置要编辑的值,如果需要终止正在编辑的其他值*/
public void setItem(Object anObject);
/*返回当前正在编辑的值*/
public Object getItem();
/*全选*/
public void selectAll();
/*当编辑的值得到确认时或者发生变化时将会调用注册上面的ActionListener,改方法添加ActionListener*/
public void addActionListener(ActionListener l);
/*删除ActionListener*/
public void removeActionListener(ActionListener l);
}

 由于JComboBox只有一处可以编辑,因此的编辑过程相当简单。JComboBox将ComboBoxEditor 提供组件作为编辑器添加到组件树上,自己注册为该ComboBoxEditor的ActionListener。当编辑组件的值发生变化,或用户确认编辑结果时,通知JComboBox。JComboBox发出适当的事件通知JComboBox的外部事件侦听者。

 当用户从列表中选择某项进行编辑时,JComboBox使用ComboBoxEditor.setItem设置编辑器的初始值。当用户请求JComboBox获取当前编辑值时,JComboBox调用ComboBoxEditor.getItem获取正在编辑的值,另外JComboBox可以通过ComboBoxEditor的selectAll来请求全选操作,这时允许如JTextField为编辑组件的编辑器实现全选。

 ComboBoxEditor接口的方法也暗含几个关系:

1.setItem、getItem、selectAll的操作对象应该是getEditorComponent返回的同一组件。

2.同样要实现addActionListener和removeActionListener,一般要实现fireActionPerformed,要侦听编辑组件的事件,使用fireActionPerformed通知JComboBox做出响应。

 编辑器工作过程相对比Renderer要复杂一些,主要是因为编辑过程是和用户进行交互的过程,不像Renderer那样只是简单的渲染过程。这个过程是非过程性的,所以它们的接口也就复杂的多,接口方法之间也暗含这一定的关系。

  但要正确理解也不难,关键要清楚JTable、JTree和JComboBox编辑器工作的过程。知道这些过程并根据以及前所学的事件及事件器处理模型知识,就应该很容易理解各个接口方法的含义以及它们之间暗含的关系,也就不会在实现自己的Editor时一头雾水,无所适从。由于篇幅的原因,今天文章就到此为止了。明天的文章将举一些具体例子加深对in-placeeditor的理解。


本文举一例子,实现如下图所示、类似于IDE界面设计工具的属性表。此例将向你演示如何自定义扩展TableCellEditor、TableCellRenderer以及TableModel,以便将这几天讲的知识串一下。

  此例中属性表区别于普通表的特征包括:
1.属性值一列不同行显示不同类型的数据。
2.属性值单元格采用不同的渲染组件和编辑组件。
3.属性值单元格的编辑器和渲染组件是同一种组件。
  该例子包括如下图所示八个文件:
 
  TableCellSupport实现了TableCellEditor和TableCellRenderer两个接口,是扩展自定义渲染器及编辑器的基类。该类实现了TableCellEditor和TableCellRenderer的所有方法,封装了TableCellEditor所暗含的关系,并假定负责渲染的组件和负责编辑的组件为同一组件。org.dyno.test.impl包下的类继承该TableCellSupport类,分别实现了CheckBox、ComboBox、Spinner以及TextField常见类型的渲染器编辑器。要实现其他类型的自定义渲染器编辑器,可继承TableCellSupport进行扩展。
    BeanProperty只是简单封装了某JavaBean属性以及渲染编辑器的对象。
    BeanPropertyTable继承JTable,以BeanProperty数组作为数据源。
      PropertyDemo是该例子的主类,是一个JFrame。
    TableCellSupport是实现该属性表编辑器的核心类,下面是TableCellSupport的实现:
public abstract classTableCellSupport
       implements TableCellEditor, TableCellRenderer {
   //编辑器、渲染器缺省的前后背景
    static ColorBACKGROUND=UIManager.getColor("TextField.background");
    static ColorFOREGROUND=UIManager.getColor("TextField.foreground");
    static ColorSELECTION_BACKGROUND=UIManager.getColor("TextField.selectionBackground");
    static ColorSELECTION_FOREGROUND=UIManager.getColor("TextField.selectionForeground");
   //渲染器、编辑器的组件,使用同一个
    protected Tcomponent;
   //CellEditorListener的容器,使用WeakReference放置内存泄漏
    privateArrayList> listeners
           =new ArrayList>();
   //构造函数
    publicTableCellSupport(T component) {
       this.component=component;
       //如果是JComponent类组件,为了美观把边框去掉
       if(component instanceof JComponent)
           ((JComponent)component).setBorder(null);
    }
   //获取并配置编辑组件
    publicComponent getTableCellEditorComponent(JTable table,
           Object value, boolean isSelected, int row, int column) {
       //将value值设置给component,这儿调用了一个子类需要实现的方法setValueTo
       setValueTo(component, value);
       //设置前后景、字体
       component.setBackground(BACKGROUND);
       component.setForeground(FOREGROUND);
       component.setFont(table.getFont());
       return component;
    }
   //获取当前编辑器的值,以component中的值为准
    publicObject getCellEditorValue() {
       //调用了一个子类需要实现的方法getValueFrom从component获取当前正在编辑的值
       return getValueFrom(component);
    }
   //根据事件anEvent判断是否可编辑,直接返回true,如有特殊需求子类可以覆盖改变
    publicboolean isCellEditable(EventObject anEvent) {
       return true;
    }
   //根据事件anEvent判断是否可选,直接返回true,如有特殊需求子类可以覆盖改变
    publicboolean shouldSelectCell(EventObject anEvent) {
       return true;
    }
   //停止编辑
    publicboolean stopCellEditing() {
       try{
           //调用通常子类需要覆盖的方法:checkComponentValue,该方法通过抛出异常来声明发生何中错误
           checkComponentValue(component);
           //通过检查,说明有效,触发事件通知编辑停止事件
           fireEditingStopped();
           //返回true标识成功
           return true;
       }catch(Exception e){
           //说明有错,错误信息被包含在Exception的message中,显示该信息。
           JOptionPane.showMessageDialog(component,
                   e.getMessage(), "Error Input", JoptionPane.ERROR_MESSAGE);
           //返回false标识失败
           return false;
       }
    }
   //取消编辑
    public voidcancelCellEditing() {
       //通常直接发出通知即可
       fireEditingCanceled();
    }
   //添加CellEditorListener
    public voidaddCellEditorListener(CellEditorListener l){       
       listeners.add(new WeakReference(l));
    }
   //删除CellEditorListener
    public voidremoveCellEditorListener(CellEditorListener l) {
       listeners.remove(new WeakReference(l));
    }
   //获取并配置渲染组件
    publicComponent getTableCellRendererComponent(
           JTable table, Object value, boolean isSelected,
           boolean hasFocus, int row, int column) {
       //设置组件的值
       setValueTo(component, value);
       //设置字体、前后背景
       component.setFont(table.getFont());
       if(isSelected){
           component.setBackground(SELECTION_BACKGROUND);
           component.setForeground(SELECTION_FOREGROUND);
       }else{
           component.setBackground(BACKGROUND);
           component.setForeground(FOREGROUND);
       }
       //返回该组件
       return component;
    }
   //触发编辑停止操作事件,注意这儿是protected方法,允许子类调用
    protectedvoid fireEditingStopped(){
       ChangeEvent e=new ChangeEvent(component);
       for(WeakReference ref:listeners){
           CellEditorListener l=ref.get();
           l.editingStopped(e);
       }
    }
   //触发编辑取消操作,允许子类调用
    protectedvoid fireEditingCanceled(){
       ChangeEvent e=new ChangeEvent(component);
       for(WeakReference ref:listeners){
           CellEditorListener l=ref.get();
           l.editingCanceled(e);
       }
    }
   //检查编辑器组件component内的值是否有效,对于希望检查有效性的需要覆盖此方法
    protectedvoid checkComponentValue(T component)throws Exception{
    }
   //将value设置到编辑组件component内,子类必须实现的抽像方法
    protectedabstract void setValueTo(T component, Object value);
   //从编辑组件component内获取正在编辑的值,子类必须实现的抽象方法
    protectedabstract Object getValueFrom(T component);
}
    通过封装,TableCellSupport实现了大部分自定义渲染器和编辑器的功能,继承TableCellSupport的类要实现以下方法:
1.带有以组件为参数的构造函数,还负责可能的事件注册,注册可能导致编辑完成事件的处理器。
2.setValueTo:将给定的值设置到给定的组件内
3.getValueFrom:从指定组件内获取当前编辑的值
3.如需要判断编辑器的值是否有效,还要覆盖checkComponentValue,该项为可选做项
 
  现在它们之间不在暗含什么关系,需要实现和覆盖的方法的含义也非常清晰了。


我们仅举CheckBoxCell这个类的例子简单说明一下如何使用TableCellSupport进行自定义扩展。CheckBoxCell就是定义一个JCheckBox为编辑器和渲染器的实现:
public class CheckBoxCell extends TableCellSupport{
    public CheckBoxCell(JCheckBox cb){        
        super(cb);
        cb.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                //被选中时需要触发编辑停止事件,一般直接调用父类的stopCellEditing即可,那儿已经负责了有效性检查,事件触发。
                stopCellEditing();
            }
        });
    }    
    protected void setValueTo(JCheckBox component, Object value) {
        //认为value值是Boolean类型的,注意空值的处理
        component.setSelected(value==null?false:((Boolean)value).booleanValue());
    }
    protected Object getValueFrom(JCheckBox component) {
        //返回当前选中状态的布尔值,用Boolean封装
        return new Boolean(component.isSelected());
    }
}
   BeanProperty是一个简单的封装类,目的是打包一个属性的描述,包括它的名称、值、渲染器和编辑器。它的代码如下:
public class BeanProperty {
    //属性显示名称,属性表格第一列显示名
    private String displayName;
    //属性的值,属性表格第二列的值
    private Object value;
    //渲染和编辑属性值用的渲染编辑器(Support类)
    private TableCellSupport support;
    public BeanProperty(String displayName, TableCellSupport support) {
        this.displayName=displayName;
        this.support=support;
    }
    public String getDisplayName() {...}
    public Object getValue() {...}
    public void setValue(Object value) {...}
    public TableCellSupport getSupport() {...}
}
  BeanPropertyTable继承了JTable类,它覆盖了JTable的两方法getCellRenderer和getCellEditor来提供自定义的渲染器和编辑器,并使用基于BeanProperty数组的数据模型BeanModel:
public class BeanPropertyTable extends JTable {
    ...
    private ArrayList properties;
    public BeanPropertyTable() {
        properties=new ArrayList();
        ...
    }
    public void setProperties(ArrayList properties){
        if(properties!=null){
          this.properties=properties;
          setModel(new BeanModel());
        }
    }
    //自定义的TableModel
    private class BeanModel extends AbstractTableModel{
        public int getRowCount() {
            //属性表的行数
            return properties.size();
        }
        public int getColumnCount() {
            //属性表的列数
            return 2;
        }
        public String getColumnName(int columnIndex) {
            //属性表的列名:property, value
            return columnIndex==0?"property":"value";
        }
        public boolean isCellEditable(int rowIndex, int columnIndex) {
            //第二列属性值可编辑
            return columnIndex==1;
        }
        public Object getValueAt(int rowIndex, int columnIndex) {
            //获取值,第一列用属性显示名,第二列用属性值
            BeanProperty property=properties.get(rowIndex);
            return columnIndex==0?property.getDisplayName():property.getValue();
        }
        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
            if(columnIndex==1)//只有第二列可编辑,设置第二列到属性值
                properties.get(rowIndex).setValue(aValue);
        }
    }
    //覆盖父类的getCellRenderer提供个性化的渲染器
    public TableCellRenderer getCellRenderer(int row, int column) {
        if(column==0)//第一列使用继承的渲染器
            return super.getCellRenderer(row, column);
        else//第二列使用属性对象自己提供的渲染器,注意Support类实现了TableCellRenderer
            return properties.get(row).getSupport();
    }
    //覆盖父类的getCellEditor提供个性化的编辑器
    public TableCellEditor getCellEditor(int row, int column) {
        if(column==0)//第一列使用继承的编辑器
            return super.getCellEditor(row,column);
        else//第二列使用属性对象自己提供的编辑器,注意Support类实现了TableCellEditor
            return properties.get(row).getSupport();
    }
}
  最后看看主类PropertyDemo.java的实现,非常简单,非常直接:
public class PropertyDemo extends JFrame{
    public PropertyDemo() {        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        BeanPropertyTable table=new BeanPropertyTable();
        ArrayList props=new ArrayList();
        for(int i=0;i<2;i++){
            //添加一个textfield属性,其渲染编辑组件是JTextField
            props.add(new BeanProperty("textfield"+i,new TextFieldCell(new JTextField())));
            //添加一个combo属性,其渲染编辑组件是JComboBox
            JComboBox cb=new JComboBox();
            cb.addItem("true");
            cb.addItem("false");
            props.add(new BeanProperty("combobox"+i,new ComboBoxCell(cb)));
            //添加一个checkbox属性,其渲染编辑组件是 JCheckBox 
            props.add(new BeanProperty("checkbox"+i,new CheckBoxCell(new JcheckBox())));
            //添加一个spinner属性,其渲染编辑组件是 JSpinner 
            props.add(new BeanProperty("spinner"+i, new SpinnerCell(new JSpinner())));
        }
        //设置这些属性数组到属性表
        table.setProperties(props);
        add(new JScrollPane(table), BorderLayout.CENTER);
    }
    public static void main(String args[]) {
        try {
            //设置外观,这儿设置成系统的,也可以设置成其他的外观
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                //显示属性表的窗口
                PropertyDemo demo=new PropertyDemo();
                demo.setSize(200,300);
                demo.setVisible(true);
            }
        });
    }
}
  另外,在SpinnerCell类中继承并覆盖了checkComponentValue,检查整型值是否小于零,小于零就报错:
public class SpinnerCell extends TableCellSupport {
    ...
    protected void checkComponentValue(JSpinner component) throws Exception {
        Integer i=(Integer)component.getValue();
        if(i.intValue()<0)
            throw new Exception("Cannot be negative!");
    }    
}
   当编辑spinner属性时,如果输入值小于零,便会提示用户出错,要求用户重新输入:
 本例中的TableCellSupport封装了最常见的渲染和编辑逻辑,它把暗含的接口关系都已经实现了,用户的自定义扩展只需要实现上文提到的四个方法便可满足大部分需求,因此TableCellSupport类可以作为一个比较通用的基类,来简化自定义JTable的开发。


来源:http://blog.sina.com.cn/s/blog_4b6047bc010007qz.html

你可能感兴趣的:(Java,swing,框架,jcomponent,object,exception,properties)