CheckBoxTree與JTree在兩個層面上存在差異:
1.在模型層上,CheckBoxTree的每個結點需要一個成員來保存其是否被選中,但是JTree的結點則不需要。
2.在視圖層上,CheckBoxTree的每個結點比JTree的結點多顯示一個複選框。
既然存在兩個差異,那麼只要我們把這兩個差異部分通過自己的實現填補上,那麼帶複選框的樹也就實現了。
現在開始解決第一個差異。爲了解決第一個差異,需要定義一個新的結點類CheckBoxTreeNode,該類繼承DefaultMutableTreeNode,
並增加新的成員isSelected來表示該結點是否被選中。對於一顆CheckBoxTree,如果某一個結點被選中的話,其複選框會勾選上,
並且使用CheckBoxTree的動機在於可以一次性地選中一顆子樹。那麼,在選中或取消一個結點時,其祖先結點和子孫結點應該做出某種變化。在此,我們應用如下遞歸規則:
1.如果某個結點被手動選中,那麼它的所有子孫結點都應該被選中;如果選中該結點使其父節點的所有子結點都被選中,則選中其父結點。
2.如果某個結點被手動取消選中,那麼它的所有子孫結點都應該被取消選中;如果該結點的父結點處於選中狀態,則取消選中其父結點。
注意:上面的兩條規則是遞歸規則,當某個結點發生變化,導致另外的結點發生變化時,另外的結點也會導致其他的結點發生變化。
在上面兩條規則中,強調手動,是因爲手動選中或者手動取消選中一個結點,會導致其他結點發生非手動的選中或者取消選中,
這種非手動導致的選中或者非取消選中則不適用於上述規則。
按照上述規則實現的CheckBoxTreeNode源代碼如下:
package demo;
import javax.swing.tree.DefaultMutableTreeNode;
public class CheckBoxTreeNode extends DefaultMutableTreeNode
{
protected boolean isSelected;
public CheckBoxTreeNode()
{
this(null);
}
public CheckBoxTreeNode(Object userObject)
{
this(userObject, true, false);
}
public CheckBoxTreeNode(Object userObject, boolean allowsChildren, boolean isSelected)
{
super(userObject, allowsChildren);
this.isSelected = isSelected;
}
public boolean isSelected()
{
return isSelected;
}
public void setSelected(boolean _isSelected)
{
this.isSelected = _isSelected;
if(_isSelected)
{
// 如果選中,則將其所有的子結點都選中
if(children != null)
{
for(Object obj : children)
{
CheckBoxTreeNode node = (CheckBoxTreeNode)obj;
if(_isSelected != node.isSelected())
node.setSelected(_isSelected);
}
}
// 向上檢查,如果父結點的所有子結點都被選中,那麼將父結點也選中
CheckBoxTreeNode pNode = (CheckBoxTreeNode)parent;
// 開始檢查pNode的所有子節點是否都被選中
if(pNode != null)
{
int index = 0;
for(; index < pNode.children.size(); ++ index)
{
CheckBoxTreeNode pChildNode = (CheckBoxTreeNode)pNode.children.get(index);
if(!pChildNode.isSelected())
break;
}
/*
* 表明pNode所有子結點都已經選中,則選中父結點,
* 該方法是一個遞歸方法,因此在此不需要進行迭代,因爲
* 當選中父結點後,父結點本身會向上檢查的。
*/
if(index == pNode.children.size())
{
if(pNode.isSelected() != _isSelected)
pNode.setSelected(_isSelected);
}
}
}
else
{
/*
* 如果是取消父結點導致子結點取消,那麼此時所有的子結點都應該是選擇上的;
* 否則就是子結點取消導致父結點取消,然後父結點取消導致需要取消子結點,但
* 是這時候是不需要取消子結點的。
*/
if(children != null)
{
int index = 0;
for(; index < children.size(); ++ index)
{
CheckBoxTreeNode childNode = (CheckBoxTreeNode)children.get(index);
if(!childNode.isSelected())
break;
}
// 從上向下取消的時候
if(index == children.size())
{
for(int i = 0; i < children.size(); ++ i)
{
CheckBoxTreeNode node = (CheckBoxTreeNode)children.get(i);
if(node.isSelected() != _isSelected)
node.setSelected(_isSelected);
}
}
}
// 向上取消,只要存在一個子節點不是選上的,那麼父節點就不應該被選上。
CheckBoxTreeNode pNode = (CheckBoxTreeNode)parent;
if(pNode != null && pNode.isSelected() != _isSelected)
pNode.setSelected(_isSelected);
}
}
}
第一個差異通過繼承DefaultMutableTreeNode定義CheckBoxTreeNode解決了,接下來需要解決第二個差異。第二個差異是外觀上的差異,
JTree的每個結點是通過TreeCellRenderer進行顯示的。爲了解決第二個差異,我們定義一個新的類CheckBoxTreeCellRenderer,
該類實現了TreeCellRenderer接口。CheckBoxTreeRenderer的源代碼如下:
package demo;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import javax.swing.JCheckBox;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.plaf.ColorUIResource;
import javax.swing.tree.TreeCellRenderer;
public class CheckBoxTreeCellRenderer extends JPanel implements TreeCellRenderer
{
protected JCheckBox check;
protected CheckBoxTreeLabel label;
public CheckBoxTreeCellRenderer()
{
setLayout(null);
add(check = new JCheckBox());
add(label = new CheckBoxTreeLabel());
check.setBackground(UIManager.getColor("Tree.textBackground"));
label.setForeground(UIManager.getColor("Tree.textForeground"));
}
/**
* 返回的是一個<code>JPanel</code>對象,該對象中包含一個<code>JCheckBox</code>對象
* 和一個<code>JLabel</code>對象。並且根據每個結點是否被選中來決定<code>JCheckBox</code>
* 是否被選中。
*/
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded, boolean leaf, int row,
boolean hasFocus)
{
String stringValue = tree.convertValueToText(value, selected, expanded, leaf, row, hasFocus);
setEnabled(tree.isEnabled());
check.setSelected(((CheckBoxTreeNode)value).isSelected());
label.setFont(tree.getFont());
label.setText(stringValue);
label.setSelected(selected);
label.setFocus(hasFocus);
if(leaf)
label.setIcon(UIManager.getIcon("Tree.leafIcon"));
else if(expanded)
label.setIcon(UIManager.getIcon("Tree.openIcon"));
else
label.setIcon(UIManager.getIcon("Tree.closedIcon"));
return this;
}
@Override
public Dimension getPreferredSize()
{
Dimension dCheck = check.getPreferredSize();
Dimension dLabel = label.getPreferredSize();
return new Dimension(dCheck.width + dLabel.width, dCheck.height < dLabel.height ? dLabel.height: dCheck.height);
}
@Override
public void doLayout()
{
Dimension dCheck = check.getPreferredSize();
Dimension dLabel = label.getPreferredSize();
int yCheck = 0;
int yLabel = 0;
if(dCheck.height < dLabel.height)
yCheck = (dLabel.height - dCheck.height) / 2;
else
yLabel = (dCheck.height - dLabel.height) / 2;
check.setLocation(0, yCheck);
check.setBounds(0, yCheck, dCheck.width, dCheck.height);
label.setLocation(dCheck.width, yLabel);
label.setBounds(dCheck.width, yLabel, dLabel.width, dLabel.height);
}
@Override
public void setBackground(Color color)
{
if(color instanceof ColorUIResource)
color = null;
super.setBackground(color);
}
}
在CheckBoxTreeCellRenderer的實現中,getTreeCellRendererComponent方法返回的是JPanel,而不是像DefaultTreeCellRenderer那樣返回JLabel,
因此JPanel中的JLabel無法對選中做出反應,因此我們重新實現了一個JLabel的子類CheckBoxTreeLabel,它可以對選中做出反應,其源代碼如下:
package demo;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.UIManager;
import javax.swing.plaf.ColorUIResource;
public class CheckBoxTreeLabel extends JLabel
{
private boolean isSelected;
private boolean hasFocus;
public CheckBoxTreeLabel()
{
}
@Override
public void setBackground(Color color)
{
if(color instanceof ColorUIResource)
color = null;
super.setBackground(color);
}
@Override
public void paint(Graphics g)
{
String str;
if((str = getText()) != null)
{
if(0 < str.length())
{
if(isSelected)
g.setColor(UIManager.getColor("Tree.selectionBackground"));
else
g.setColor(UIManager.getColor("Tree.textBackground"));
Dimension d = getPreferredSize();
int imageOffset = 0;
Icon currentIcon = getIcon();
if(currentIcon != null)
imageOffset = currentIcon.getIconWidth() + Math.max(0, getIconTextGap() - 1);
g.fillRect(imageOffset, 0, d.width - 1 - imageOffset, d.height);
if(hasFocus)
{
g.setColor(UIManager.getColor("Tree.selectionBorderColor"));
g.drawRect(imageOffset, 0, d.width - 1 - imageOffset, d.height - 1);
}
}
}
super.paint(g);
}
@Override
public Dimension getPreferredSize()
{
Dimension retDimension = super.getPreferredSize();
if(retDimension != null)
retDimension = new Dimension(retDimension.width + 3, retDimension.height);
return retDimension;
}
public void setSelected(boolean isSelected)
{
this.isSelected = isSelected;
}
public void setFocus(boolean hasFocus)
{
this.hasFocus = hasFocus;
}
}
通過定義CheckBoxTreeNode和CheckBoxTreeCellRenderer。我們解決了CheckBoxTree和JTree的兩個根本差異,但是還有一個細節問題需要解決,
就是CheckBoxTree可以響應用戶事件決定是否選中某個結點。爲此,我們爲CheckBoxTree添加一個響應用戶鼠標事件的監聽器CheckBoxTreeNodeSelectionListener,
該類的源代碼如下:
package demo;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JTree;
import javax.swing.tree.TreePath;
import javax.swing.tree.DefaultTreeModel;
public class CheckBoxTreeNodeSelectionListener extends MouseAdapter
{
@Override
public void mouseClicked(MouseEvent event)
{
JTree tree = (JTree)event.getSource();
int x = event.getX();
int y = event.getY();
int row = tree.getRowForLocation(x, y);
TreePath path = tree.getPathForRow(row);
if(path != null)
{
CheckBoxTreeNode node = (CheckBoxTreeNode)path.getLastPathComponent();
if(node != null)
{
boolean isSelected = !node.isSelected();
node.setSelected(isSelected);
((DefaultTreeModel)tree.getModel()).nodeStructureChanged(node);
}
}
}
}
到此爲止,CheckBoxTree所需要的所有組件都已經完成了,接下來就是如何使用這些組件。下面給出了使用這些組件的源代碼:
package demo;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.tree.DefaultTreeModel;
public class DemoMain
{
public static void main(String[] args)
{
JFrame frame = new JFrame("CheckBoxTreeDemo");
frame.setBounds(200, 200, 400, 400);
JTree tree = new JTree();
CheckBoxTreeNode rootNode = new CheckBoxTreeNode("root");
CheckBoxTreeNode node1 = new CheckBoxTreeNode("node_1");
CheckBoxTreeNode node1_1 = new CheckBoxTreeNode("node_1_1");
CheckBoxTreeNode node1_2 = new CheckBoxTreeNode("node_1_2");
CheckBoxTreeNode node1_3 = new CheckBoxTreeNode("node_1_3");
node1.add(node1_1);
node1.add(node1_2);
node1.add(node1_3);
CheckBoxTreeNode node2 = new CheckBoxTreeNode("node_2");
CheckBoxTreeNode node2_1 = new CheckBoxTreeNode("node_2_1");
CheckBoxTreeNode node2_2 = new CheckBoxTreeNode("node_2_2");
node2.add(node2_1);
node2.add(node2_2);
rootNode.add(node1);
rootNode.add(node2);
DefaultTreeModel model = new DefaultTreeModel(rootNode);
tree.addMouseListener(new CheckBoxTreeNodeSelectionListener());
tree.setModel(model);
tree.setCellRenderer(new CheckBoxTreeCellRenderer());
JScrollPane scroll = new JScrollPane(tree);
scroll.setBounds(0, 0, 300, 320);
frame.getContentPane().add(scroll);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}