Arquivos Mensais: março \28\UTC 2011

JTree dentro de um JComboBox

Esta é uma tradução de uma postagem no Santosh Kumar’s Weblog. Estava precisando de algo assim e após uma pesquisa encontrei esta solução. Resolvi então postar aqui os passos. O original está aqui.

JComboBox requer ComboBoxModel, mas temos TreeModel, porque não tentar criar um adaptador chamado TreeListModel que nós permita ver TreeModel como se fosse um ListModel?

class TreeListModel extends AbstractListModel implements ComboBoxModel{
    private TreeModel treeModel;
    private Object selectedObject; 

    public TreeListModel(TreeModel treeModel){
        this.treeModel = treeModel;
    } 

    public int getSize(){
        ????
    } 

    public Object getElementAt(int index){
        ????
    } 

    public void setSelectedItem(Object anObject){
        if((selectedObject!=null && !selectedObject.equals(anObject)) ||
                selectedObject==null && anObject!=null){
            selectedObject = anObject;
            fireContentsChanged(this, -1, -1);
        }
    } 

    public Object getSelectedItem() {
        return selectedObject;
    }
} 

Precisamos implementar getSize() e getElementAt(index),mas como?

– tamanho da lista (size) = total de nós da árvore

– elemento i na lista = linha i na árvore quanto completamento expandida

A resposta está em DefaultMutableTreeNode.PreorderEnumeration que na sua implementação assume que os nós são objetos TreeNode. Como queremos que a solução seja para qualquer TreeModel substituiremos o método enumeration() da interface TreeModel desta forma:

class ChildrenEnumeration implements Enumeration{
    TreeModel treeModel;
    Object node;
    int index = -1; 

    public ChildrenEnumeration(TreeModel treeModel, Object node){
        this.treeModel = treeModel;
        this.node = node;
    } 

    public boolean hasMoreElements(){
        return index<treeModel.getChildCount(node)-1;
    } 

    public Object nextElement(){
        return treeModel.getChild(node, ++index);
    }
} 

Agora copiamos DefaultMutableTreeNode.PreorderEnumeration e removemos os vestígios da interface TreeNode:

class PreorderEnumeration implements Enumeration{
    private TreeModel treeModel;
    protected Stack stack; 

    public PreorderEnumeration(TreeModel treeModel){
        this.treeModel = treeModel;
        Vector v = new Vector(1);
        v.addElement(treeModel.getRoot());
        stack = new Stack();
        stack.push(v.elements());
    } 

    public boolean hasMoreElements(){
        return (!stack.empty() &&
                ((Enumeration)stack.peek()).hasMoreElements());
    } 

    public Object nextElement(){
        Enumeration enumer = (Enumeration)stack.peek();
        Object node = enumer.nextElement();
        if(!enumer.hasMoreElements())
            stack.pop();
        Enumeration children = new ChildrenEnumeration(treeModel, node);
        if(children.hasMoreElements())
            stack.push(children);
        return node;
    }
} 

Agora completaremos os métodos deixados para trás em TreeListModel:

public int getSize(){
        int count = 0;
        Enumeration enumer = new PreorderEnumeration(treeModel);
        while(enumer.hasMoreElements()){
            enumer.nextElement();
            count++;
        }
        return count;
    } 

    public Object getElementAt(int index){
        Enumeration enumer = new PreorderEnumeration(treeModel);
        for(int i=0; i<index; i++)
            enumer.nextElement();
        return enumer.nextElement();
    } 

Nosso TreeListModel está completo. Vamos agora para parte do Renderer.

JComboBox requer ListCellRenderer mas temos TreeCellRenderer. Escreveremos então um adaptador para isso também.

public class TreeListCellRenderer extends JPanel implements ListCellRenderer{
    private static final JTree tree = new JTree();
    TreeModel treeModel;
    TreeCellRenderer treeRenderer; 

    public TreeListCellRenderer(TreeModel treeModel, TreeCellRenderer treeRenderer){
        this.treeModel = treeModel;
        this.treeRenderer = treeRenderer;
        setOpaque(false);
    } 

    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus){
        if(value==null){ //if selected value is null
            return this;
        } 

        boolean leaf = treeModel.isLeaf(value);
        Component comp = treeRenderer.getTreeCellRendererComponent(tree, value, isSelected, true, leaf, index, cellHasFocus); 

        return comp;
    }
}

No entanto, há um problema com a implementação acima: o renderer não esta sendo recuado de acordo com a profundidade do nó da árvore.

Como é que vamos recuar um renderer? A resposta é EmptyBorder cuja inserção esquerda é igual ao número de pixels reqd para recuo.

class IndentBorder extends EmptyBorder{
    int indent = UIManager.getInt("Tree.leftChildIndent"); 

    public IndentBorder(){
        super(0, 0, 0, 0);
    } 

    public void setDepth(int depth){
        left = indent*depth;
    }
} 

E TreeListCellRenderer é modificado para usar IndentBorder:

class TreeListCellRenderer extends JPanel implements ListCellRenderer{
    private static final JTree tree = new JTree();
    TreeModel treeModel;
    TreeCellRenderer treeRenderer;
    IndentBorder indentBorder = new IndentBorder(); 

    public TreeListCellRenderer(TreeModel treeModel, TreeCellRenderer treeRenderer){
        this.treeModel = treeModel;
        this.treeRenderer = treeRenderer;
        setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
        setBorder(indentBorder);
        setOpaque(false);
    } 

    public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus){
        if(value==null){ //if selected value is null
            removeAll();
            return this;
        } 

        boolean leaf = treeModel.isLeaf(value);
        Component comp = treeRenderer.getTreeCellRendererComponent(tree, value, isSelected, true, leaf, index, cellHasFocus);
        removeAll();
        add(comp); 

        int depth = //somehow calculate depath
        indentBorder.setDepth(depth); 

        return this;
    }
}

Quase tudo está concluído, exceto o cálculo da profundidade de um nó.

Adicionamos uma variável membro chamada de ‘depth’ para a classe ChildrenEnumeration. Esta classe não faz qualquer outra coisa além de assegurar o valor da profundidade:

class ChildrenEnumeration implements Enumeration{
    .....
    int depth;
    .....
} 

A classe PreorderEnumeration é, então, modificada para calcular a profundidade e devolver a profundidade do elemento:

class PreorderEnumeration implements Enumeration{
    .....
    private int depth = 0; 

    ......

    public Object nextElement(){
        Enumeration enumer = (Enumeration)stack.peek();
        Object node = enumer.nextElement();
        depth = enumer instanceof ChildrenEnumeration
                ? ((ChildrenEnumeration)enumer).depth
                : 0;
        if(!enumer.hasMoreElements())
            stack.pop();
        ChildrenEnumeration children = new ChildrenEnumeration(treeModel, node);
        children.depth = depth+1;
        if(children.hasMoreElements()){
            stack.push(children);
        }
        return node;
    } 

    public int getDepth(){
        return depth;
    }
} 

Agora TreeListRenderer usa PreorderEnumeration para encontrar a profundidade do renderer:

        // compute the depth of value
        PreorderEnumeration enumer = new PreorderEnumeration(treeModel);
        for(int i = 0; i<=index; i++)
            enumer.nextElement();
        indentBorder.setDepth(enumer.getDepth()); 

Para utilizar nossa implementação:

 JComboBox comboBox = new JComboBox(new TreeListModel(treeModel));
 comboBox.setRenderer(new TreeListCellRenderer(treeModel, treeCellRenderer));
Anúncios