JTree with CheckBoxes(带复选框的树结构)

Let me explain what is "JTree with CheckBoxes".

Each node of the JTree contains a CheckBox.
When a node's checkbox is selected, all its descendants should also get checked and vice versa.
When all descendants of a node are not checked but some of them are checked (i.e, partial selection) then the node should be checked with a gray tick.
When all children of a node are unselected, the node should unselected.
If you google for "JTree CheckBox" you will get :
  [url]http://www.java2s.com/ExampleCode/Swing-Components/CheckboxNodeTreeExample.htm[/url]

There are many drawbacks in the above implementation. The major drawback is, It restricts to use DefaultTreeModel whose nodes are CheckNode.

JTree should have two selections: one is normal selection and another is checkSelection (checkbox selected)

Why don't we use TreeSelectionModel to maintain checkSelection. But we can't use DefaultTreeSelection as-it-is without any changes. because we have to restrict the selection as explained in the above points:

In our check selection model, it contains only the selected node but not all its descendants. i.e. let us say Node A has children B, C, D;
When user checks Node A, the tree shows B, C and D also checked. But the selectionModel actually contains only A,

// @author Santhosh Kumar T - [email protected]
public class CheckTreeSelectionModel extends DefaultTreeSelectionModel{
  private TreeModel model;

  public CheckTreeSelectionModel(TreeModel model){
    this.model = model;
    setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
  }

  // tests whether there is any unselected node in the subtree of given path
  public boolean isPartiallySelected(TreePath path){
    if(isPathSelected(path, true))
        return false;
    TreePath[] selectionPaths = getSelectionPaths();
    if(selectionPaths==null)
        return false;
    for(int j = 0; j<selectionPaths.length; j++){
        if(isDescendant(selectionPaths[j], path))
          return true;
    }
    return false;
  }

  // tells whether given path is selected.
  // if dig is true, then a path is assumed to be selected, if
  // one of its ancestor is selected.
  public boolean isPathSelected(TreePath path, boolean dig){
    if(!dig)
        return super.isPathSelected(path);
    while(path!=null && !super.isPathSelected(path))
        path = path.getParentPath();
    return path!=null;
  }

  // is path1 descendant of path2
  private boolean isDescendant(TreePath path1, TreePath path2){
    Object obj1[] = path1.getPath();
    Object obj2[] = path2.getPath();
    for(int i = 0; i<obj2.length; i++){
        if(obj1!=obj2)
          return false;
    }
    return true;
  }

  public void setSelectionPaths(TreePath[] pPaths){
    throw new UnsupportedOperationException("not implemented yet!!!");
  }

  public void addSelectionPaths(TreePath[] paths){
    // unselect all descendants of paths[]
    for(int i = 0; i<paths.length; i++){
        TreePath path = paths;
        TreePath[] selectionPaths = getSelectionPaths();
        if(selectionPaths==null)
          break;
        ArrayList toBeRemoved = new ArrayList();
        for(int j = 0; j<selectionPaths.length; j++){
          if(isDescendant(selectionPaths[j], path))
            toBeRemoved.add(selectionPaths[j]);
        }
        super.removeSelectionPaths((TreePath[])toBeRemoved.toArray(new TreePath[0]));
    }

    // if all siblings are selected then unselect them and select parent recursively
    // otherwize just select that path.
    for(int i = 0; i<paths.length; i++){
        TreePath path = paths;
        TreePath temp = null;
        while(areSiblingsSelected(path)){
          temp = path;
          if(path.getParentPath()==null)
            break;
          path = path.getParentPath();
        }
        if(temp!=null){
          if(temp.getParentPath()!=null)
            addSelectionPath(temp.getParentPath());
          else{
            if(!isSelectionEmpty())
                removeSelectionPaths(getSelectionPaths());
            super.addSelectionPaths(new TreePath[]{temp});
          }
        }else
          super.addSelectionPaths(new TreePath[]{ path});
    }
  }

  // tells whether all siblings of given path are selected.
  private boolean areSiblingsSelected(TreePath path){
    TreePath parent = path.getParentPath();
    if(parent==null)
        return true;
    Object node = path.getLastPathComponent();
    Object parentNode = parent.getLastPathComponent();

    int childCount = model.getChildCount(parentNode);
    for(int i = 0; i<childCount; i++){
        Object childNode = model.getChild(parentNode, i);
        if(childNode==node)
          continue;
        if(!isPathSelected(parent.pathByAddingChild(childNode)))
          return false;
    }
    return true;
  }

  public void removeSelectionPaths(TreePath[] paths){
    for(int i = 0; i<paths.length; i++){
        TreePath path = paths;
        if(path.getPathCount()==1)
          super.removeSelectionPaths(new TreePath[]{ path});
        else
          toggleRemoveSelection(path);
    }
  }

  // if any ancestor node of given path is selected then unselect it
  // and selection all its descendants except given path and descendants.
  // otherwise just unselect the given path
  private void toggleRemoveSelection(TreePath path){
    Stack stack = new Stack();
    TreePath parent = path.getParentPath();
    while(parent!=null && !isPathSelected(parent)){
        stack.push(parent);
        parent = parent.getParentPath();
    }
    if(parent!=null)
        stack.push(parent);
    else{
        super.removeSelectionPaths(new TreePath[]{path});
        return;
    }

    while(!stack.isEmpty()){
        TreePath temp = (TreePath)stack.pop();
        TreePath peekPath = stack.isEmpty() ? path : (TreePath)stack.peek();
        Object node = temp.getLastPathComponent();
        Object peekNode = peekPath.getLastPathComponent();
        int childCount = model.getChildCount(node);
        for(int i = 0; i<childCount; i++){
          Object childNode = model.getChild(node, i);
          if(childNode!=peekNode)
            super.addSelectionPaths(new TreePath[]{temp.pathByAddingChild(childNode)});
        }
    }
    super.removeSelectionPaths(new TreePath[]{parent});
  }
}

You can ask the the above selection model whether B is selected as below:
  checkSelectionModel.isPathSelected(getPath(B), true); // the second argument tells that selection is to be digged.

Please take time to understand the above code (It is little bit complex, I guess).

As usual we attacked the model. next is renderer, In the google link I specified in the beginning restricts you to use its renderer. But in our implementation we let user add checkbox to any TreeCellRenderer:

public class CheckTreeCellRenderer extends JPanel implements TreeCellRenderer{
  private CheckTreeSelectionModel selectionModel;
  private TreeCellRenderer delegate;
  private TristateCheckBox checkBox = new TristateCheckBox();

  public CheckTreeCellRenderer(TreeCellRenderer delegate, CheckTreeSelectionModel selectionModel){
    this.delegate = delegate;
    this.selectionModel = selectionModel;
    setLayout(new BorderLayout());
    setOpaque(false);
    checkBox.setOpaque(false);
  }


  public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus){
    Component renderer = delegate.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);

    TreePath path = tree.getPathForRow(row);
    if(path!=null){
        if(selectionModel.isPathSelected(path, true))
          checkBox.setState(Boolean.TRUE);
        else
          checkBox.setState(selectionModel.isPartiallySelected(path) ? null : Boolean.FALSE);
    }
    removeAll();
    add(checkBox, BorderLayout.WEST);
    add(renderer, BorderLayout.CENTER);
    return this;
  }
}

The above renderer can wrap any of your TreeCellRenderer implementation.
I used TristateCheckBox from [url]http://www.javaspecialists.co.za/archive/Issue082.html[/url]

We can also implement editor in the same way such that it can wrap any TreeCellEditor implementation. But I am leaving it as exercise for you :)

The next question comes is:
  We have SelectionModel and renderer. But who will listen of mouseclicks in checkbox and update the selectionmodel ???

Here is that class, CheckTreeManager:

public class CheckTreeManager extends MouseAdapter implements TreeSelectionListener{
  private CheckTreeSelectionModel selectionModel;
  private JTree tree = new JTree();
  int hotspot = new JCheckBox().getPreferredSize().width;

  public CheckTreeManager(JTree tree){
    this.tree = tree;
    selectionModel = new CheckTreeSelectionModel(tree.getModel());
    tree.setCellRenderer(new CheckTreeCellRenderer(tree.getCellRenderer(), selectionModel));
    tree.addMouseListener(this);
    selectionModel.addTreeSelectionListener(this);
  }

  public void mouseClicked(MouseEvent me){
    TreePath path = tree.getPathForLocation(me.getX(), me.getY());
    if(path==null)
        return;
    if(me.getX()>tree.getPathBounds(path).x+hotspot)
        return;

    boolean selected = selectionModel.isPathSelected(path, true);
    selectionModel.removeTreeSelectionListener(this);

    try{
        if(selected)
          selectionModel.removeSelectionPath(path);
        else
          selectionModel.addSelectionPath(path);
    } finally{
        selectionModel.addTreeSelectionListener(this);
        tree.treeDidChange();
    }
  }

  public CheckTreeSelectionModel getSelectionModel(){
    return selectionModel;
  }

  public void valueChanged(TreeSelectionEvent e){
    tree.treeDidChange();
  }
}

It listens for mouse clicks in the hotspot (inside checkbox) and updates the selection
updates the tree (repaints) when checkselection is changed
listens for selectionmodel changes and updates the view. Because it allows you tocheck a node programmatically..
How to use this:

// makes your tree as CheckTree
CheckTreeManager checkTreeManager = new CheckTreeManager(yourTree);

// to get the paths that were checked
TreePath checkedPaths[] = checkTreeManager.getSelectionModel().getSelectionPaths();

See how powerful our solution is:

You can convert any JTree (with any TreeModel and any TreeCellRenderer) just by one line.
normal selection doesn't interfere with checkselection. In the google link i specified, when you select a node, it gets checked.
you can reuse your renderers/editors


The webstart demo shows the selection at the bottom of the window in a JLable. This lets you understand how our CheckSelectionModel is updated with your changes to checkselection.

你可能感兴趣的:(职场,休闲)