树(二)

17.4 编辑树节点

除了支持单个的树单元渲染器,JTree组件还可以是可编辑的,从而以许用户修改树的节点的内容。默认情况下,树是只读的。要使得树成为可编辑的,只需要将editable属性设置修改为true即可:

aTree.setEditable(true);

默认情况下,编辑器是一个文本域。同时对由组合框或是复选框中选择也具有内建支持。如果我们喜欢,我们可以为树创建一个自定义的编辑器,就像我们可以创建一个自定义单元渲染器一样。

注意,不幸的是,内建的复选框编辑器在表格中的表现要优于树中的表现,其中列标签是名字而值是单元。

图17-12显示了一个使用默认编辑器的树。要打开编辑器,选择一个节点,然后双击。如果节点不是叶子节点,选择该节点同时会显示或隐藏其子节点。


有一系列的类支持编辑树节点。许多是为JLabel组件所共享的,因为他们都支持可编辑单元。CellEditor接口形成了TreeCellEditor接口的基础。JTree的任何编辑器实现必须实现TreeCellEditor接口。DefaultCellEditor(其扩展了AbstractCellEditor)提供了一个这样的实现,而DefaultTreeCellEditor提供了另一个实现。下面我们详细探讨这些接口与类。

17.4.1 CellEditor接口

CellEditor接口定义了JTree或JTable以及需要编辑器的第三方组件所用的编辑器的基础。除了定义如何管理CellEditorListener对象列表,接口描述了如何确定一个节点或是单元是否是可编辑的及编辑器在修改了其值以后其新值是什么。

public interface CellEditor {
  // Properties
  public Object getCellEditorValue();
  // Listeners
  public void addCellEditorListener(CellEditorListener l);
  public void removeCellEditorListener(CellEditorListener l);
  // Other methods
  public void cancelCellEditing();
  public boolean isCellEditable(EventObject event);
  public boolean shouldSelectCell(EventObject event);
  public boolean stopCellEditing();
}


17.4.2 TreeCellEditor接口

TreeCellEditor接口的作用类似于TreeCellRenderer接口。然而,getXXXComponent()方法并没有分辨编辑器是否具有输入焦点的参数,因为在编辑器的情况下,他必须具有焦点。任何实现了TreeCellEditor接口的类都可以作为我们的JTree所用的编辑器。

public interface TreeCellEditor implements CellEditor {
  public Component getTreeCellEditorComponent(JTree tree, Object value,
    boolean isSelected, boolean expanded, boolean leaf, int row);
}


17.4.3 DefaultCellEditor类

DefaultCellEditor类用作树节点与表格单元的编辑器。这个类可以使得我们很容易提供文本编辑器,组合框编辑器,或是复选框编辑器来修改节点或是单元的内容。

下面将要描述DefaultTreeCellEditor类,使用这个类可以为自定义的文本域提供一个编辑器,维护基于TreeCellRenderer的相应节点类型图标。

创建DefaultCellEditor

当我们创建DefaultCellEditor实例时,我们提供JTextField,JComboBox或是JCheckBox来用作编辑器。

public DefaultCellEditor(JTextField editor)
JTextField textField = new JTextField();
TreeCellEditor editor = new DefaultCellEditor(textField);
public DefaultCellEditor(JComboBox editor)
public static void main (String args[]) {
  JComboBox comboBox = new JComboBox(args);
  TreeCellEditor editor = new DefaultCellEditor(comboBox);
  ...
}
public DefaultCellEditor(JCheckBox editor)
JCheckBox checkBox = new JCheckBox();
TreeCellEditor editor = new DefaultCellEditor(checkBox);

对于JTree,如果我们需要一个JTextField编辑器,我们应该使用DefaultTreeCellEditor。这个文本域将会共享相同的字体并且使用树的相应的编辑器边框。当JCheckBox被用作编辑器时,树的节点应该是一个Boolean值或是一个可以转换为Boolean的String。(如果我们不熟悉String到Boolean的转换,可以参考接收String的Boolean构造函数的Javadoc。)

在创建了编辑器之后,我们使用类似的tree.setCellEditor(editor)来使用这个编辑器。并且不要忘记使用tree.setEditable(true)来使得树可编辑。例如,如果我们希望一个可编辑器的组合框作为我们的编辑器,下面的代码可以实现相应的目的:

JTree tree = new JTree(...);
tree.setEditable(true);
String elements[] = { "Root", "chartreuse", "rugby", "sushi"} ;
JComboBox comboBox = new JComboBox(elements);
comboBox.setEditable(true);
TreeCellEditor editor = new DefaultCellEditor(comboBox);
tree.setCellEditor(editor);

上面的代码将会产生如图17-13所示的编辑basketball节点时的屏幕。注意,树并没有图标来标识正在被编辑的节点的类型。这可以通过DefaultTreeCellEditor类来修正。DefalutCellEditor最初是用于JTable的,而不是用于JTree。


注意,当我们使用一个不可编辑的JComboBox作为单元编辑器时,如果选项集合不包括原始的代码设置,一旦节点的值发生变化,他就有可能回到原始的设置。

要了解当使用DefaultCellEditor作为TreeCellEditor时,JCheckBox笨拙的外观,可以参看图17-14。


图17-14使用下面的代码:

Object array[] =
  {Boolean.TRUE, Boolean.FALSE, "Hello"}; // Hello will map to false
JTree tree = new JTree(array);
tree.setEditable(true);
tree.setRootVisible(true);
JCheckBox checkBox = new JCheckBox();
TreeCellEditor editor = new DefaultCellEditor(checkBox);
tree.setCellEditor(editor);

JCheckBox编辑器的笨拙以及DefaultTreeCellEditor中自定义的文本域编辑器使得JComboBox成为我们可以由DefaultCellEditor中获得的唯一的TreeCellEditor。然而,也许我们仍然希望将组合框编辑器放在DefaultTreeCellEditor中来显示与图标紧邻的相应的类型图标。

DefaultCellEditor属性

DefaultCellEditor只有三个属性,如表17-4所示。编辑器可以是任意的AWT组件,而不仅仅是轻量级的Swing组件。记住,如果我们确实要选择使用一个重量级组件作为编辑器,我们就要承担混合重量级组件与轻量级组件的风险。如查我们在确定编辑器组件的当前设置是什么,我们可以请求cellEditorValue属性设置。


17.4.4 DefaultTreeCellEditor类

当我们使得树成为可编辑的,但是并没有向树关联编辑器时,JTree自动所用的TreeCellEditor就是DefaultTreeCellEditor。DefaultTreeCellEditor组合了TreeCellRenderer的图标与TreeCellEditor来返回一个组合的编辑器。

编辑器所用的默认组件是JTextField。文本编辑器比较特殊,因为他会尝试将其高度限制为原始的单元渲染器并且使用树的字体,从而不会出现不合适的显示。编辑器使用两个公开的内联类来实现在这一目的:DefaultTreeCellEditor.EidtorContainer与DefaultTreeCellEditor.DefaultTextField。

DefaultTreeCellEditor有两个构造函数。通常我们并不需要调用第一个构造函数,因为他是当确定节点为可编辑器的时由用户界面自动为我们创建的。然而,如果我们希望以某种方式自定义默认编辑器时,第一个构造函数则是必需的。

public DefaultTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer)
JTree tree = new JTree(...);
DefaultTreeCellRenderer renderer = (DefaultTreeCellRenderer)tree.getCellRenderer();
TreeCellEditor editor = new DefaultTreeCellEditor(tree, renderer);
public DefaultTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer,
  TreeCellEditor editor)
public static void main (String args[]) {
  JTree tree = new JTree(...);
  DefaultTreeCellRenderer renderer = 
    (DefaultTreeCellRenderer)tree.getCellRenderer();
  JComboBox comboBox = new JComboBox(args);
  TreeCellEditor comboEditor = new DefaultCellEditor(comboBox);
  TreeCellEditor editor = new DefaultTreeCellEditor(tree, renderer, comboEditor);
  ...
}

17.4.5 为树创建合适的组合框编辑器

如图17-13所示,通过DefaultCellEditor使用JComboBox作为TreeCellEditor并不会在编辑器旁边放置合适的代码类型图标。如果我们希望显示图标,我们需要组合DefaultCellEditor与DefaultTreeCellEditor来获得一个同时具有图标与编辑器的编辑器。事实上他并没有听起来这样困难。他只涉及到两个额外的步骤:获得树的渲染器(由其获得图标),然后组合图标与编辑器从而获得一个新的编辑器。下面的代码演示了这一操作:

JTree tree = new JTree();
tree.setEditable(true);
DefaultTreeCellRenderer renderer = (DefaultTreeCellRenderer)tree.getCellRenderer();
String elements[] = { "Root", "chartreuse", "rugby", "sushi"} ;
JComboBox comboBox = new JComboBox(elements);
comboBox.setEditable(true);
TreeCellEditor comboEditor = new DefaultCellEditor(comboBox);
TreeCellEditor editor = new DefaultTreeCellEditor(tree, renderer, comboEditor);
tree.setCellEditor(editor);

改进的输入如图17-15所示。


17.4.6 只为叶节点创建编辑器

在某些情况下,我们希望只有叶子节点是可以编辑的。由getTreeCellEditorComponent()请求返回null等效于使得一个节点不可以编辑。不幸的是,这会使得用户界面类抛出NullPointerException。

不能返回null,我们可以覆盖public boolean isCellEditable(EventObject object)方法的默认行为,他是CellEditor接口的一部分。如果原始的返回值为true,我们可以进行额外的检测以确定所选中的树的节点是否是叶子节点。树的节点实现了TreeNode接口(在本章稍后进行描述)。这个接口恰好具有方法public boolean isLeaf(),该方法可以为我们提供所寻求的答案。叶子节点的单元编辑器的类定义显示在列表17-8中。

package swingstudy.ch17;
 
import java.util.EventObject;
 
import javax.swing.JTree;
import javax.swing.tree.DefaultTreeCellEditor;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeNode;
 
public class LeafCellEditor extends DefaultTreeCellEditor {
 
	public LeafCellEditor(JTree tree, DefaultTreeCellRenderer renderer) {
		super(tree, renderer);
	}
 
	public LeafCellEditor(JTree tree, DefaultTreeCellRenderer renderer, TreeCellEditor editor) {
		super(tree, renderer, editor);
	}
 
	public boolean isCellEditable(EventObject event) {
		// Get initial setting
		boolean returnValue = super.isCellEditable(event);
		// If still possible, check if current tree nod is a leaf
		if(returnValue) {
			Object node = tree.getLastSelectedPathComponent();
			if((node != null) && (node instanceof TreeNode)) {
				TreeNode treeNode = (TreeNode)node;
				returnValue = treeNode.isLeaf();
			}
		}
		return returnValue;
	}
}


我们使用LeafCellEditor的方式类似于DefaultTreeCellRenderer。其构造函数要求JTree与DefaultTreeCellRenderer。另外,他支持一个额外的TreeCellEditor。如果没有提供,则JTextField会被用作编辑器。

JTree tree = new JTree();
tree.setEditable(true);
DefaultTreeCellRenderer renderer = (DefaultTreeCellRenderer)tree.getCellRenderer();
TreeCellEditor editor = new LeafCellEditor(tree, renderer);
tree.setCellEditor(editor);


17.4.7 CellEditorListener接口与ChangeEvent类

在探讨完整的TreeCellEditor创建之前,我们先来了解一下CellEditorListener接口定义。这个接口包含CellEditor所用的两个方法。

public interface CellEditorListener implements EventListener {
  public void editingCanceled(ChangeEvent changeEvent);
  public void editingStopped(ChangeEvent changeEvent);
}

编辑器调用所注册的监听器的editingCanceled()方法来通知节点值的编辑器已经退出。editingStopped()方法被调用来通知编辑会话的完成。

通常情况下,并没有必要创建CellEditorListener。然而,当创建一个TreeCellEditor(或是任意的CellEditor),管理其监听器列表并且在需要的时候通知这些监听器是必需的。幸运的是,这是借助于AbstractCellEditor为我们进行自动管理的。

17.4.8 创建更好的复选框节点编辑器

当配合JTree使用时,使用DefaultCellEditor类所提供的JCheckBox编辑器并不是一个很好的选择。尽管编辑器可以被包装进DefaultTreeCellEditor来获得相应的树图标,我们不能在复选框内显示文本(也就是除了true或是false)。其他的文本字符串也可以显示在树中,但是一旦一个节点被编辑,被编辑器节点的文本标签只能是true或是false。

要使得具有文本标签的可编辑复选框作为树的单元编辑器,我们必须自己创建。完整的过程涉及到创建三个类-用于树中每个节点的数据模型,渲染自定义数据结构的树单元渲染器以及实际的编辑器-外加一个将他们连接在一起的测试程序。

注意,在这里创建的渲染器与编辑器只支持用于编辑叶子节点的类复选框数据。如果我们要支持非叶节点的复选框,我们需要移除检测叶节点的代码。

创建CheckBoxNode类

我们所要创建的第一个类用于处理树中叶节点的数据模型。我们可以使用与JCheckBox类相同的数据模型,但是这个数据模型包含我们并不需要额外的节点信息。我们所需要的信息仅是节点的被选中状态与其文本标签。列表17-9包含了这个类的基本定义,其中包含用于状态与标签的setter与getter方法。其他类的构建则没有这么容易。

package swingstudy.ch17;
 
public class CheckBoxNode {
 
	String text;
	boolean selected;
 
	public CheckBoxNode(String text, boolean selected) {
		this.text = text;
		this.selected = selected;
	}
 
	public boolean isSelected() {
		return selected;
	}
 
	public void setSelected(boolean newValue) {
		selected = newValue;
	}
 
	public String getText() {
		return text;
	}
 
	public void setText(String newValue) {
		text = newValue;
	}
 
	public String toString() {
		return getClass().getName()+"["+text+"/"+selected+"]";
	}
}

创建CheckBoxNodeRenderer类

渲染器包含两部分。对于非叶节点,我们可以使用DefaultTreeCellRenderer,因为这些节点本来不是CheckBoxNode元素。对于CheckBoxNode类型的叶节点的渲染器,我们需要将数据结构映射为相应的渲染器。因为这些节点包含一个选中状态与文本标签,JCheckBox可以作为叶节点的很好渲染器。

这两部分中比较容易解释的是非叶子节点的渲染器。在这个例子中,如通常一样,他简单的配置一个DefaultTreeCellRenderer;而并不做任何特殊的事情。

叶子节点的渲染器需要更多的工作。在配置任何节点之前,我们需要使其看起来像是默认渲染哭喊。构造函数需要渲染器的观感所提供的必须的字体与各种颜色,从而保证两个渲染器看起来比较相似。

树单元渲染器CheckBoxNodeRenderer类的定义显示在列表17-10中。

package swingstudy.ch17;
 
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
 
import javax.swing.JCheckBox;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellRenderer;
 
public class CheckBoxNodeRenderer implements TreeCellRenderer {
 
	private JCheckBox leafRenderer = new JCheckBox();
	private DefaultTreeCellRenderer nonLeafRenderer = new DefaultTreeCellRenderer();
	Color selectionBorderColor, selectionForeground, selectionBackground, textForeground, textBackground;
 
	protected JCheckBox getLeafRenderer() {
		return leafRenderer;
	}
 
	public CheckBoxNodeRenderer() {
		Font fontValue;
		fontValue = UIManager.getFont("Tree.font");
		if(fontValue != null) {
			leafRenderer.setFont(fontValue);
		}
		Boolean booleanValue = (Boolean)UIManager.get("Tree.drawsFocusBorderAroundIcon");
		leafRenderer.setFocusPainted((booleanValue != null) && (booleanValue.booleanValue()));
 
		selectionBorderColor = UIManager.getColor("Tree.selectionBorderColor");
		selectionForeground = UIManager.getColor("Tree.selectionForeground");
		selectionBackground = UIManager.getColor("Tree.selectionBackground");
		textForeground = UIManager.getColor("Tree.textForeground");
		textBackground = UIManager.getColor("Tree.textBackground");
	}
 
	@Override
	public Component getTreeCellRendererComponent(JTree tree, Object value,
			boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
		// TODO Auto-generated method stub
 
		Component returnValue;
 
		if(leaf) {
			String stringValue = tree.convertValueToText(value, selected, expanded, leaf, row, false);
			leafRenderer.setText(stringValue);
			leafRenderer.setSelected(false);
 
			leafRenderer.setEnabled(tree.isEnabled());
 
			if(selected) {
				leafRenderer.setForeground(selectionForeground);
				leafRenderer.setBackground(selectionBackground);
			}
			else {
				leafRenderer.setForeground(textForeground);
				leafRenderer.setBackground(textBackground);
			}
 
			if((value != null) && (value instanceof DefaultMutableTreeNode)) {
				Object userObject = ((DefaultMutableTreeNode)value).getUserObject();
				if(userObject instanceof CheckBoxNode) {
					CheckBoxNode node = (CheckBoxNode)userObject;
					leafRenderer.setText(node.getText());
					leafRenderer.setSelected(node.isSelected());
				}
			}
			returnValue = leafRenderer;
		}
		else {
			returnValue = nonLeafRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
		}
		return returnValue;
	}
 
}

注意,getLeafRenderer()方法是我们在编辑器中所需要的助手方法。

创建CheckBoxNodeEditor类

CheckBoxNodeEditor类是创建更好的复选框编辑器的最后一部分。他作为TreeCellEditor实现,允许我们支持其叶子节点数据是CheckBoxNode类型的树的编辑。TreeCellEditor接口是CellEditor实现的扩展,所以我们必须实现两个接口的方法。我们不能扩展DefaultCellEditor或是DefaultTreeCellEditor,因为他们会要求我们使用他们所提供的JCheckBox编辑器实现,而不是我们在这里所创建的新编辑器。然而,我们可以扩展AbstractCellEditor,并且添加必需的TreeCellEditor接口实现。AbstractCellEditor为我们管理CellEditorListener对象列表,并且具有依据停止或是关闭编辑来通知监听器列表的方法。

因为编辑器承担渲染器的角色,我们需要使用前面的CheckBoxNodeRenderer来获得基本的渲染器外观。这将保证编辑器的外观与渲染器的外观类似。因为叶节点的渲染器是JCheckBox,这可以完美的使得我们可以修改节点状态。编辑器JCheckBox将会是活动的并且是可修改的,从而允许用户由选中状态修改为非选中状态,以及相反的操作。如果编辑器是标准的DefaultTreeCellRenderer,我们需要管理选中变化的创建。

现在已经设置了类的层次结构,所要检测的第一个方法是CellEditor的public Object getCellEditorValue()方法。这个方法的目的就是将存储在节点编辑器的数据转换为存储在节点中的数据。用户界面会在他确定用户已经成功的修改了编辑器中的数据之后调用这个方法来获得编辑器的值。在这个方法中,每次我们需要创建一个新对象。否则,相同的代码会在树中出现多次,使得所有的节点与与最后一个编辑的节点的渲染器相同。要将编辑器转换为数据模型,需要向编辑器询问其当前标签与选中状态是什么,然后创建并返回新节点。

public Object getCellEditorValue() {
  JCheckBox checkbox = renderer.getLeafRenderer();
  CheckBoxNode checkBoxNode =
    new CheckBoxNode(checkbox.getText(), checkbox.isSelected());
  return checkBoxNode;
}

注意,直接访问树中的节点并更新并不是编辑器的工作。getCellEditorValue()方法会返回相应的节点对象,从而用户界面可以通知树的任何变化。

如果我们要自己实现CellEditor接口,我们也需要自己管理CellEditorListener列表。我们需要使用addCellEditorListener()与removeCellEditorListener()方法管理列表,并且提供通知接口中每个方法的监听器列表的方法。但是,因为我们要派生AbstractCellEditor,我们并没有必要自己做这些事情。我们只需要知道为了在合适的时候通知监听器列表,该类提供了fireEditingCanceled()与fireEditingStopped()方法。

下一个CellEditor方法,cancelCellEditing(),会在树中的一个新节点被选中时调用,表明前一个选中的编辑过程已经被停止,并且中间的更新已经中止。这个方法能够做任何事情,例如销毁编辑器所需要的中间对象。然而,这个方法应该做的就是调用fireEditingCanceled();这可以保证所注册的CellEditorListener对象会得到关闭的通知。AbstractCellEditor会为我们完成这些工作。除非我们需要做一些临时操作,否则没有必要重写这一行为。

CellEditor接口的stopCellEditing()方法返回boolean。这个方法被调用来检测当前节点的编辑是否可以停止。如果需要进行一些验证来确认编辑是否可以停止,我们需要在这里进行检测。对于这个例子中的CheckBoxNodeEditor,并没有验证检测的必要。所以,编辑总是可以停止 ,使得方法总是返回true。

当我们希望编辑器停止编辑时,我们可以调用fireEditingStopped()方法。例如,如果编辑器是一个文本域,在文本域中按下Enter可以作为停止编辑的信号。在JCheckBox编辑器的例子中,选中可以作为停止编辑器的信号。如果没有调用fireEditingStopped()方法,树的数据模型就不会被更新。

要在JCheckBox选中之后停止编辑,向其关联一个ItemListener。

ItemListener itemListener = new ItemListener() {
  public void itemStateChanged(ItemEvent itemEvent) {
    if (stopCellEditing()) {
      fireEditingStopped();
    }
  }
};
editor.addItemListener(itemListener);

我们要了解的CellEditor接口的下一个方法是public boolean isCellEditable(EventObject event)。这个方法返回一个boolean来表明事件源的节点是否是可编辑的。要确定某一事件是否发生在一个特定的节点上,我们需要一个到编辑器被使用的树的引用。我们可以将这一需要添加到编辑器的构造函数。

要确定在事件过程中在某一个特定的位置上是哪一个节点,我们可以向树请求到事件位置的节点的路径。这个路径被作为TreePath对象返回,我们会在本章稍后进行探讨。树路径的最后一个组件就是事件发生的特定节点。这就是我们必须检测来确定他是否可编辑的节点。如果他是可编辑的,方法会返回true;如果是不可编辑的,则会返回false。在这里要创建的树的例子中,如果节点是叶节点则是可编辑的,并且他包含CheckBoxNode数据。

JTree tree;
public CheckBoxNodeEditor(JTree tree) {
  this.tree = tree;
}
public boolean isCellEditable(EventObject event) {
  boolean returnValue = false;
  if (event instanceof MouseEvent) {
    MouseEvent mouseEvent = (MouseEvent)event;
    TreePath path = tree.getPathForLocation(mouseEvent.getX(), mouseEvent.getY());
    if (path != null) {
      Object node = path.getLastPathComponent();
      if ((node != null) &&  (node instanceof DefaultMutableTreeNode)) {
        DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)node;
        Object userObject = treeNode.getUserObject();
        returnValue = ((treeNode.isLeaf()) && (userObject instanceof CheckBoxNode));
      }
    }
  }
  return returnValue;
}

CellEditor接口的shouldSselectCell()方法允许我们决定一个节点是否是可选择的。对于这个例子中的编辑器,所有可编辑的单元都应被选中。然而,这个方法允许我们查看特定的节点以确定他是否是可选中的。默认情况下,AbstractCellEditor会为这个方法返回true。

其他的方法,getTreeCellEditorComponent()来自于TreeCellEditor接口。我们需要一个到CheckBoxNodeRenderer的引用来获取并将其用作编辑器。除了仅是传递所有的参数以外还有两个小小的变化。编辑器应总是被选中并且具有输入焦点。这简单的强制两个参数总是为true。当节点被选中时,背景会被填充。当获得焦点时,在UIManager.get("Tree.drawsFocusBorderAroundIcon")返回true时会有边框环绕编辑器。

CheckBoxNodeRenderer renderer = new CheckBoxNodeRenderer();
public Component getTreeCellEditorComponent(JTree tree, Object value,
    boolean selected, boolean expanded, boolean leaf, int row) { 
  // Editor always selected / focused
  return renderer.getTreeCellRendererComponent(tree, value, true, expanded, leaf,
    row, true);
}

列表17-11将所有的内容组合在一起,构成了完整的CheckBoxNodeEditor类的源码。

package swingstudy.ch17;
 
import java.awt.Component;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.util.EventObject;
 
import javax.swing.AbstractCellEditor;
import javax.swing.JCheckBox;
import javax.swing.JTree;
import javax.swing.event.ChangeEvent;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreePath;
 
public class CheckBoxNodeEditor extends AbstractCellEditor implements
		TreeCellEditor {
 
	CheckBoxNodeRenderer renderer = new CheckBoxNodeRenderer();
 
	ChangeEvent changeEvent = null;
 
	JTree tree;
 
	public CheckBoxNodeEditor(JTree tree) {
		this.tree = tree;
	}
	@Override
	public Object getCellEditorValue() {
		// TODO Auto-generated method stub
		JCheckBox checkbox = renderer.getLeafRenderer();
		CheckBoxNode checkBoxNode = new CheckBoxNode(checkbox.getText(), checkbox.isSelected());
		return checkBoxNode;
	}
 
	public boolean isCellEditable(EventObject event) {
		boolean returnValue = false;
		if(event instanceof MouseEvent) {
			MouseEvent mouseEvent = (MouseEvent)event;
			TreePath path = tree.getPathForLocation(mouseEvent.getX(), mouseEvent.getY());
			if(path != null) {
				Object node = path.getLastPathComponent();
				if((node != null ) && (node instanceof DefaultMutableTreeNode)) {
					DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)node;
					Object userObject = treeNode.getUserObject();
					returnValue = ((treeNode.isLeaf()) && (userObject instanceof CheckBoxNode));
				}
			}
		}
		return returnValue;
	}
	@Override
	public Component getTreeCellEditorComponent(JTree tree, Object value,
			boolean selected, boolean expanded, boolean leaf, int row) {
		// TODO Auto-generated method stub
		Component editor = renderer.getTreeCellRendererComponent(tree, value, true, expanded, leaf, row, true);
 
		// Editor always selected / focused
		ItemListener itemListener = new ItemListener() {
			public void itemStateChanged(ItemEvent event) {
				if(stopCellEditing()) {
					fireEditingStopped();
				}
			}
		};
 
		if(editor instanceof JCheckBox) {
			((JCheckBox)editor).addItemListener(itemListener);
		}
		return editor;
	}
 
}


注意,在树节点中并没有对数据的直接修改。修改节点并不是编辑器的角色。编辑器仅是获得新节点值,并且使用getCellEditorValue()方法返回。

创建测试程序

列表17-12中的测试由创建CheckBoxNode元素的基础构成。除了创建树数据,树必须有渲染器以及与渲染器相关联的编辑器,并且是可编辑的。

package swingstudy.ch17;
 
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.util.Vector;
 
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
 
public class CheckBoxNodeTreeSample {
 
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
 
		Runnable runner = new Runnable() {
			public void run() {
				JFrame frame = new JFrame("CheckBox Tree");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
				CheckBoxNode accessibilityOptions[] = {
						new CheckBoxNode("Move System caret with focus/selection changes", false),
						new CheckBoxNode("Always exand alt text for images", true)
				};
 
				CheckBoxNode browsingOptions[] = {
					new CheckBoxNode("Notify when downloads complete", true),
					new CheckBoxNode("Disabel script debugging", true),
					new CheckBoxNode("Use AutoComplete", true),
					new CheckBoxNode("Browse in a new process", false)
				};
 
				Vector<CheckBoxNode> accessVector = new NamedVector<CheckBoxNode>("Accessibility", accessibilityOptions);
				Vector<CheckBoxNode> browseVector = new NamedVector<CheckBoxNode>("Browsing", browsingOptions);
 
				Object rootNodes[] = {accessVector, browseVector };
				Vector<Object> rootVector = new NamedVector<Object>("Root", rootNodes);
				JTree tree = new JTree(rootVector);
 
				CheckBoxNodeRenderer renderer = new CheckBoxNodeRenderer();
				tree.setCellRenderer(renderer);
 
				tree.setCellEditor(new CheckBoxNodeEditor(tree));
				tree.setEditable(true);
 
				JScrollPane scrollPane = new JScrollPane(tree);
				frame.add(scrollPane, BorderLayout.CENTER);
				frame.setSize(300, 150);
				frame.setVisible(true);
			}
		};
		EventQueue.invokeLater(runner);
	}
 
}

运行这个程序并且选择CheckBoxNode会打开编辑器。在编辑器被打开之后,再一次选择编辑器会使得树中节点的状态发生变化。编辑器会保持使能状态直到另一个不同的树节点被选中。图17-16显示了使用中的编辑器的示例。


17.5 使用树节点

当我们创建一个JTree,树中任意位置的对象类型可以是任意的Object。并没有要求树的节点实现任何接口或是继承任何类。然而,Swing组件库提供一对接口与一类来处理树节点。树的默认数据模型,DefaultTreeModel,使用这些接口与类。然而,树数据类型接口,TreeModel,允许任意的数据类型做为树的节点。

树的基本接口是TreeNode,他定义了描述只读,父子聚合关系的一系列方法。TreeNode的扩展是MutableTreeNode接口,这个接口允许我们编程实现连接节点并且在每一个节点存储信息。实现这两个接口的类是DefaultMutableTreeNode类。除了实现两个接口的方法以外,该类还提供了一个方法集合用于遍历树并且查询各种节点的状态。

记住,尽管有这些节点对象可用,但是很多的工作仍然无需这些接口与类就可以完成,正如本章前面所示。

17.5.1 TreeNode接口

TreeNode接口描述了树单独部分的一个可能定义。他为TreeNode的一个实现,DefaultTreeModel类,用来存储到描述一棵树层次数据的引用。这个接口可以使得我们确定当前节点的父节点是哪一个节点,以及获取关于节点集合的信息。当父节点为null时,此节点就是树的根。

public interface TreeNode {
  // Properties
  public boolean getAllowsChildren();
  public int getChildCount();
  public boolean isLeaf();
  public TreeNode getParent();
  // Other methods
  public Enumeration children();
  public TreeNode getChildAt(int childIndex);
  public int getIndex(TreeNode node);
}

注意,通常情况下,只有非叶节点允许有子节点。然而,安全限制也许会限制非叶子节点具有子节点,或者是至少限制子节点的显示。想像一个目录树,其中我们并没有某个特定目录的读取权限。尽管该目录并不是叶子节点,他也不能有子节点,因为我们并没有查找其子节点的权限。

17.5.2 MutableTreeNode接口

尽管TreeNode接口允许我们获取关于树节点层次结构的信息,但是他并不允许我们创建这个层次结构。TreeNode只是提供我们访问只读树层次结构的能力。另一方面,MutableTreeNode接口允许我们创建这个层次并且在树中的特定节点存储信息。

public interface MutableTreeNode implements TreeNode {
  // Properties
  public void setParent(MutableTreeNode newParent);
  public void setUserObject(Object object);
  // Other methods
  public void insert(MutableTreeNode child, int index);
  public void remove(MutableTreeNode node);
  public void remove(int index);
  public void removeFromParent();
}

当创建树节点的层次结构时,我们可以创建子节点并将其添加到父节点或是创建父节点并且添加子节点。要将一个节点关联到你节点,我们使用setParent()方法设置其父节点。使用insert()方法可以使得我们将子节点添加到父节点。insert()方法的参数包含一个索引参数。索引表示子节点集合中的位置。索引是由零开始的,所以索引为零将会把节点添加为树的第一个子节点。将节点添加到为最后一个子节点,而不是第一个,需要我们使用getChildCount()方法查询节点有多少个子节点,然后加1:

mutableTreeNode.insert(childMutableTreeNode, mutableTreeNode.getChildCount()+1);

至少对于下面将要描述了DefaultMutableTreeNode类来说,setParent()将节点设置子节点的父节点,尽管他并没有子节点作为父节点的一个孩子。换句话说,我们不要自己调用setParent()方法;调用insert()方法,而这个方法会相应的设置父节点。

注意,insert()方法不允许循环祖先,其中子节点被添加为父节点的祖先节点。如果这样做,就会抛出IllegalArgumentException。

17.5.3 DefaultMutableTreeNode类

DefaultMutableTreeNode类提供了MutableTreeNode接口的实现(其实现了TreeNode接口)。当我们由一个Hashtable,数组或是Vector构造函数创建树时,JTree会自动将节点创建为DefaultMutableTreeNode类型集合。另一方面,如果我们希望自己创建节点,我们需要为我们树中的每一个节点创建一个DefaultMutableTreeNode的类型实例。

创建DefaultMutableTreeNode

有三个构造函数可以用来创建DefaultMutableTreeNode实例:

public DefaultMutableTreeNode()
DefaultMutableTreeNode node = new DefaultMutableTreeNode();
public DefaultMutableTreeNode(Object userObject)
DefaultMutableTreeNode node = new DefaultMutableTreeNode("Node");
public DefaultMutableTreeNode(Object userObject, boolean allowsChildren)
DefaultMutableTreeNode node = new DefaultMutableTreeNode("Node", false);

存储在每一个节点中的信息被称之为用户对象。当没有通过构造函数进行指定时,用户对象为null。另外,我们可以指定一个节点是否允许具有子节点。

构建DefaultMutableTreeNode层次

构建DefaultMutableTreeNode类型的节点层次需要创建一个DefaultMutableTreeNode类型的实例,为其孩子创建节点,然后连接他们。在使用DefaultMutableTreeNode直接创建层次之前,首先我们来看一下如何使用新的NamedVector类来创建四个节点的树:一个根节点以及三个叶节点。

Vector vector = new NamedVector("Root", new String[]{ "Mercury", "Venus", "Mars"} );
JTree tree = new JTree(vector);

当JTree获得一个Vector作为其构造函数参数时,树为根节点创建一个DefaultMutableTreeNode,然后为Vector中的每个元素创建一个,使得每一个元素节点成为根节点的子节点。不幸的是,根节点的数据并不是我们所指定的Root,而是没有显示的root。

相反,如果我们希望使用DefaultMutableTreeNode来手动创建一个棵树的节点,或者是我们希望显示根节点,则需要更多的一些代码行,如下所示:

DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
DefaultMutableTreeNode mercury = new DefaultMutableTreeNode("Mercury");
root.insert(mercury, 0);
DefaultMutableTreeNode venus = new DefaultMutableTreeNode("Venus");
root.insert(venus, 1);
DefaultMutableTreeNode mars = new DefaultMutableTreeNode("Mars");
root.insert(mars, 2);
JTree tree = new JTree(root);

除了使用MutableTreeNode中的insert()方法来将一个节点关联到父节点,DefaultMutableTreeNode还有一个add()方法可以自动的将子节点添加到尾部,而不是需要提供索引。

DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
DefaultMutableTreeNode mercury = new DefaultMutableTreeNode("Mercury");
root.add(mercury);
DefaultMutableTreeNode venus = new DefaultMutableTreeNode("Venus");
root.add(venus);
DefaultMutableTreeNode mars = new DefaultMutableTreeNode("Mars");
root.add(mars);
JTree tree = new JTree(root);

前面的两个代码块都可以创建如图17-17所示的树。


如果我们并不需要根节点并且希望我们使用NamedVector作为树根节点的行为,我们可以使用下面的代码来实现:

String elements[] = { "Mercury", "Venus", "Mars"} ;
JTree tree = new JTree(elements);

DefaultMutableTreeNode属性

如表17-5所示,DefaultMutableTreeNode有22个属性。大多数的属性都是只读的,使得我们可以确定关于树节点位置与关系的信息。userObject属性包含特定节点的数据,该节点是在节点被创建时提供给DefaultMutableTreeNode的节点。userObjectPath属性包含一个用户对象数组,由根(索引0)到当前节点(可以为根)。



查询节点关系

DefaultMutableTreeNode类提供了若干方法来确定两个节点之间的关系。另外,我们可以使用下面的方法来确定两个节点是否共享相同的父节点:

  • isNodeAncestor(TreeNode aNode):如果aNode是当前节点划当前节点的父节点则返回true。这会迭代检测getParent()方法直到aNode或遇到null为止。
  • isNodeChild(Tree aNode):如果当前节点为aNode的父节点则返回true。
  • isNodeDescendant(DefaultMutableTreeNode aNode):如果当前节点为aNode或是aNode的祖先节点时返回true。
  • isNodeRelated(DefaultMutableTreeNode aNode):如果当前节点与aNode共享相同的根节点则返回true。
  • isNodeSibling(TreeNode aNode):如果两个节点共享相同的父节点则返回true。

每个方法都返回一个boolean值,表明节点之间的关系是否存在。

如果两个节点相关,我们可以请求树的根查找共享的祖先节点。然而,这个祖先节点也许并不是树中最近的祖先节点。如果一个普通的节点位于树中的较低层次,我们可以使用public TreeNode getSharedAncestor(DefaultMutableTreeNode aNode)方法来获得其较近祖先节点。如果由于两个节点不在同一棵树中而不存在,则会返回null。

17.5.4 遍历树

TreeNode接口与DefaultMutableTreeNode类提供了若干遍历特定节点以下所有节点的方法。给定一个特定的TreeNode,我们可以通过每个节点的children()方法遍历到每一个子孙节点,包括初始节点。给定一个特定的DefaultMutableTreeNode,我们可以通过getNextNode()与getPreviousNode()方法查找所有的子孙节点直到没有额外的节点为止。下面的代码片段演示了在给定一个起始节点的情况下使用TreeNode的children()方示来遍历整个树。

public void printDescendants(TreeNode root) {
  System.out.println(root);
  Enumeration children = root.children();
  if (children != null) {
    while(children.hasMoreElements()) {
      printDescendants((TreeNode)children.nextElement());
    }
  }
}

尽管TreeNode的DefaultMutableTreeNode实现允许我们通过getNextNode()与getPreviousNode()方法来遍历树,但是这些方法效率低下,是应该避免使用的。相反,使用DefaultMutableTreeNode的特殊方法来生成节点所有子节点的Enumeration。在了解特定的方法之前,图17-18显示了一个要遍历的简单树。


图17-18有助于我们理解DefaultMutableTreeNode的三个特殊方法。这些方法允许我们使用下面的三种方法之五来遍历树,这些方法中的每一个都是public并且返回一个Enumeration:

  • preOrderEnumeration():返回节点的Enumeration,类似于printDescendants()方法。Enumeration中的第一个节点是节点本身。接下来的节点是节点的第一个子节点,然后是第一个子节点的第一个子节点,依次类推。一旦发现没有子节点的叶子节点,其父节点的下一个子节点会被放入Enumeration中,并且其子节点会被添加到相应的列表中,直到没有节点。由图17-18中的树根开始,遍历将会得到以下列顺序出现的节点的Enumeration:root, New York, Mets, yankess, Rangers, Footabll, Ginats, Jets, Bills, Boston, Red Sox, Celtics, Bruins, Denver, Rockies, Avalanche, Broncos。
  • depthFirstEnumeration()与postOrderEnumeration():返回一个与preOrderEnumeration()行为相反的Enumeration。与首先包含当前节点然后添加子节点不同,这些方法首先添加子节点然后将当前节点添加到Enumeration。对于图17-18中的树,这会生成下列顺序的Enumeration:Mets,Yankees,Rangers,Giants,Jets,Bills,Football,New York,Red Sox,Celtics,Bruins,Boston,Rockies,Avalanche,Broncos,Denver,root。
  • breadthFirstEnumeration():返回一个按层添加的节点的Enumeration。对于图17-18中的树,Enumeration的顺序如下:root,New York,Boston,Denver,Mets,Yankees,Rangers,Football,Red Sox,Celtics,Bruins,Rockies,Avalanche,Broncos,Giants,Jets,Bills。

但是还有一个问题:我们如何获得起始节点?当然,第一个节点可以被选为用户动作的结果,或者是我们可以向TreeModel查询根节点。我们稍后就会探讨TreeModel,但是下面显示了获取根节点的源码。因为TreeNode是唯一可以在树中进行排序的对象可能类型,TreeModel的getRoot()方法会返回一个对象。

TreeModel model = tree.getModel();
Object rootObject = model.getRoot();
if ((rootObject != null) && (rootObject instanceof DefaultMutableTreeNode)) {
  DefaultMutableTreeNode root = (DefaultMutableTreeNode)rootObject;
  ...
}

17.5.5 JTree.DynamicUtilTreeNodes类

JTree包含一个内联类,JTree.DynamicUtilTreeNode,树使用这个类来为我们的对创建节点。DynamicUtilTreeNode是DefaultMutableTreeNode的一个子类,该类只有在我们需要时才会创建子节点。当我们展开父节点或者是我们在树中遍历时会需要子节点。尽管我们通常并没有直接使用这个类,我们也许会发现他的用处。为了进行演示,下面的示例使用Hashtable来为树创建节点。在树的根部并没有可见的节点(使用root的userObject属性设置),根节点有一个Root属性。

DefaultMutableTreeNode root = new DefaultMutableTreeNode("Root");
Hashtable hashtable = new Hashtable();
hashtable.put ("One", args);
hashtable.put ("Two", new String[]{"Mercury", "Venus", "Mars"});
Hashtable innerHashtable = new Hashtable();
Properties props = System.getProperties();
innerHashtable.put (props, props);
innerHashtable.put ("Two", new String[]{"Mercury", "Venus", "Mars"});
hashtable.put ("Three", innerHashtable);
JTree.DynamicUtilTreeNode.createChildren(root, hashtable);
JTree tree = new JTree(root);

上面所列的代码创建了一个与图17-2中的TreeArraySample程序相同的树节点。然而,树中第一层次的结点顺序不同。这是因为在这个示例中节点位于Hashtable中,而不是如TreeArraySample一样位于Vector中。第一层次的树元素是以Hashtable返回的Enumeration的顺序被添加的,而不是按着添加到Vector中的顺序。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值