Swing框架之Renderer(三)

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

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

Swing组件需要能脱离容器组件独立存在;
Swing组件树能在运行时动态地被修改,包括添加和删除。
Swing组件树就像浏览器的HTML DOM树一样,可以通过脚本语言JavaScript进行动态修改,从而达到浏览器页面的局部更新的功能,Swing通过动态的往容器组件中添加或者删除编辑组件实现复合类型组件的in-place editor功能。

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

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

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

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

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

public interface CellEditorListener extends java.util.EventListener {
/*通知编辑器已经停止编辑,编辑效果生效。*/
public void editingStopped(ChangeEvent e);
/*通知编辑器已经取消编辑,编辑效果被取消*/
public void editingCanceled(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-place editor,它的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-place editor的理解。

你可能感兴趣的:(Java)