Java Swing: El JTree


Volviendo de nuevo con un nuevo post sobre Swing, esta vez os dejo un componente un poco más complicado de entender, el JTree. Su uso con el Swing Designer es bien simple de ponerlo en un frame y dibujar su interfaz, pero cuando entremos en su estructura que tenemos para manejarlo necesitaremos tener conocimientos de lo que es un Tipo Abstracto de Datos (TAD para los amigos), y en concreto lo que es un árbol. Lo que se puede hacer y cómo se estructura internamente lo supongo por entendido. Si no es el caso mejor empezar por ver ésto, sino lo de a continuación puede convertirse en un jeroglífico.



http://es.wikipedia.org/wiki/Tipo_de_dato_abstracto
Búsqueda en Google sobre TADs árboles


Construyendo el escenario


Para éste tutorial he utilizado Eclipse Juno y la versión 7 update 9 del JDK. Lo primero es crear, dentro de Eclipse, en la ventana del explorador de proyectos, un nuevo proyecto de Java. Luego crear un nuevo JFrame con el asistente de Eclipse y ya tenemos el esqueleto del programa listo para empezar. Yendo al diseñador de formularios del Swing Designer le añades un Absolute layout al formulario para poder posicionar los componentes donde quieras del formulario, y entonces para éste ejemplo le he añadido dos botón y un componente JTree. El componente JTree está a su vez dentro de un JScrollPane para que se muestren las barras de scroll si el contenido es más grande que lo que se puede ver. 

Antes de continuar te debe de haber quedado una ventana parecida a la de la imagen de inicio.

En la versión que tengo, cuando añado el JTree al Frame, se crea con un contenido sobre colores, deportes y comida. Éstos datos están en la propiedad model. De igual manera que algunos otros componentes, tenemos un modelo de árbol para usar el JTree. Por ejemplo, cuando usamos las listas tenemos modelos de listas, pues ahora son modelos de árbol. Si no establecemos el modelo, por defecto se construye con los elementos dichos. Vamos ahora con el TreeModel.

Creando el árbol


Vamos a usar dos tipos de datos para crear la estructura y ponerla en el JTree: el DefaultTreeModel y el  DefaultMutableTreeNode.
El botón para cargar el árbol lo he puesto para leer el árbol de directorios de tu disco duro. El tipo de datos TreeModel es el objeto que tiene internamente el JTree para manejar su estructura en árbol, nosotros vamos a usar una clase derivada de ésta porque se trata de una interfaz que no podemos usar directamente. Entonces tenemos el DefaultTreeModel que va a ser el tipo que usaremos.
Por otro lado tenemos que en cada elemento de un DefaultTreeModel es un DefaultMutableTreeNode. De forma que cada nodo de éstos puede tener a su vez hijos, formando así un árbol según tenga hijos o no. Para entenderlo ésto he pensado usar el árbol de directorio que todos conocemos.
Antes de seguir, explicando las dos funciones del JTree, si queremos crear una estructura de tipo árbol como por ejemplo:

nodoroot
- nodo1 (indice 0)
-- nodo1.1 (indice 0)
-- nodo1.2 (indice 1)
- nodo2 (indice 1)
-- nodo2.1 (indice 0)
--  nodo2.2 (indice 1)

...lo que tenemos que hacer es que cada nodo es un elemento de tipo DefaultMutableTreeNode, y cada elemento se puede hacer hijo de otro. Es decir, lo único que hay que decirle al programa es de cada nodo cuál es su padre con la función insertNodeInto(nodo, padre, índice). Por ejemplo para la estructura anterior programaríamos:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DefaultMutableTreeNode nodoroot, nodo1, nodo11, nodo12, nodo2, nodo21, nodo22;
nodoroot = new DefaultMutableTreeNode("Éste es el nodo principal.");
nodo1 = new DefaultMutableTreeNode("nodo1");
nodo11 = new DefaultMutableTreeNode("nodo11");
nodo12 = new DefaultMutableTreeNode("nodo12");
nodo2 = new DefaultMutableTreeNode("nodo2");
nodo21 = new DefaultMutableTreeNode("nodo21");
nodo22 = new DefaultMutableTreeNode("nodo22");
arbol.setRoot(nodoroot);
arbol.insertNodeInto(nodo1, nodoroot, 0);
arbol.insertNodeInto(nodo2, nodoroot, 1);
arbol.insertNodeInto(nodo11, nodo1, 0);
arbol.insertNodeInto(nodo12, nodo1, 1);
arbol.insertNodeInto(nodo21, nodo2, 0);
arbol.insertNodeInto(nodo22, nodo2, 1);

En el ejemplo os he dejado algo más complicado, con una función recursiva que carga toda la estructura de directorios desde el directorio raiz "/". Es decir, cuando le damos al botón tenemos el código siguiente:

?
1
2
3
4
5
6
7
8
9
10
11
btnCargarrbolDe.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
 
        DefaultTreeModel arbol = (DefaultTreeModel) tree.getModel();
        DefaultMutableTreeNode nroot = new DefaultMutableTreeNode("Árbol de directorios");
 
        arbol.setRoot(nroot);
 
        CargaEstructuraDirectorios(arbol, nroot, "/");
    }
});

... una vez dicho cual es el nodo principal (nroot), la función CargaEstructuraDirectorios lo hace todo. Puede tardar bastante, depende lo que tengas en tu ordenador o lo rápido que sea, déjalo ejecutarse hasta el final si lo quieres ver el resultado.
No voy a entrar en detalle sobre funciones recursivas o lectura de directorios. Lo que hay que saber es que lista un directorio, añadiendo todo lo que encuentra al árbol en su lugar adecuado, de maner que si encuentra un directorio lo añade también. Y acto seguido entra al directorio y lo lista, de manera que si algo de dentro también es un directorio vuelve a hacer lo mismo, es decir, vuelve a entrar en éste segundo directorio y lo lista también añadiendo de nuevo los elementos en el lugar adecuado. Así sucesivamente hasta listar todos los directorios y subdirectorios.
Todo ésto queda simple con una función recursiva que os dejo aquí:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void CargaEstructuraDirectorios(DefaultTreeModel arbol, DefaultMutableTreeNode padre, String ruta) {
    DefaultMutableTreeNode aux = null;
 
    File archivo = new File(ruta);
    File[] archivos = archivo.listFiles();
 
    if (archivos != null) {
        for (int i = 0; i < archivos.length; i++) {
 
            aux = new DefaultMutableTreeNode(archivos[i].getName());
            arbol.insertNodeInto(aux, padre, i);
 
            if (archivos[i].isDirectory()) {
                try {
                    CargaEstructuraDirectorios(arbol, aux, archivos[i].getAbsolutePath() + "/");
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                }
            }
 
        }
 
    }
}


Borrar un nodo


Igual que se añaden nodos, también se pueden borrar. Usando la función removeNodeFromParent que se usa en el ejemplo. Con un botón simple. No sólo podemos borrar un nodo, también podemos borrarlo de un sitio y ponerlo en otro, reordenando nuestro árbol, modificándolo, o lo que necesitemos. Pero con añadir o borrar tenemos lo básico para empezar...

Click, empieza el juego


Ahora se complica, pero empieza el juego xD ¿cómo hacer algo cuando hacemos click en un elemento del árbol? Haciendo click derecho con el ratón en el JTree añadimos el capturador de eventos que vamos a usar para hacer álgo cuando el usuario hace click en el JTree:


Eclipse, de nuevo nos genera el código esqueleto siguiente:

?
1
2
3
4
5
6
7
final JTree tree = new JTree();
tree.addTreeSelectionListener(new TreeSelectionListener() {
    public void valueChanged(TreeSelectionEvent e) {
        DefaultMutableTreeNode nseleccionado = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
        JOptionPane.showMessageDialog(frame, nseleccionado.getPath());
    }
});

Se pueden capturar otros eventos. Y bueno, ya la imaginación o lo que necesitemos entra en juego para desarrollar lo que necesitemos. Con las funciones principales que nos proporcina el DefaultMutableTreeNode pordemos hacer lo que queramos. Podemos tener varios árboles y ponerlos en el JTree cuando queramos uno u otro, podemos recorrer los nodos por los índices, podemos saber cuál es el nodo principal con la función .getRoot, saber cuántos hijos tiene un nodo con .getChildCount, etcétera...
No hay más que curiosear para qué sirven las funciones proporcionadas, como podemos ver en la imagen siguiente del Eclipse:



Hay que saber que cuando hacemos cambios en la estructura de árbol, éstos automáticamente se visualizan en el JTree, con lo que sólo tenemos que centrarnos en el árbol.
Para más información me remito de nuevo a la documentación oficial:

Códigos del ejemplo


En el ejemplo he comentado algunas cosas más. Aquí están en descarga directa los códigos fuentes. Hay un .jar, para ejecutarlo necesitas el JRE instalado, si quieres modificarlo, sólo necesitas un editor de texto ya que sólo hay un fichero .java y recompilarlo.
Todo el código comentado es el siguiente para el que no quiera descargarlo:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
 
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.event.TreeModelListener;
import javax.swing.JButton;
import javax.swing.JTree;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
 
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.io.File;
import javax.swing.JScrollPane;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.TreeSelectionEvent;
 
// La clase principal
public class Principal extends JFrame {
 
    // el panel contenedor
    private JPanel contentPane;
    // el JFrame
    static Principal frame;
 
    /**
     * esta es la función que primero se ejecuta creando el JFRame y visualizándolo
     */
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    frame = new Principal();
                    frame.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
 
    /**
     * la creación del JFrame principal donde está programado todo lo de éste ejemplo
     */
    public Principal() {
        // título de ventana
        setTitle("Java Swing 8 El JTree by Jnj");
        // operación al cerra el JFrame
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // dimensiones y posición en el escritorio
        setBounds(100, 100, 450, 306);
        // se crea el panel
        contentPane = new JPanel();
        // los bordes
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        // se establece
        setContentPane(contentPane);
        contentPane.setLayout(null);
 
        // se pone el botón en la ventana
        JButton btnCargarrbolDe = new JButton(
                "Cargar \u00E1rbol de directorios");
        btnCargarrbolDe.setBounds(10, 11, 200, 23);
        contentPane.add(btnCargarrbolDe);
 
        // las barras de escroll para el JTree
        JScrollPane scrollPane = new JScrollPane();
        scrollPane.setBounds(10, 45, 414, 206);
        contentPane.add(scrollPane);
 
        // el JTree
        final JTree tree = new JTree();
        // que captura el evento click
        tree.addTreeSelectionListener(new TreeSelectionListener() {
            public void valueChanged(TreeSelectionEvent e) {
                // se obtiene el nodo seleccionado
                DefaultMutableTreeNode nseleccionado = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
                // visualiza el path del nodo
                JOptionPane.showMessageDialog(frame, nseleccionado.getPath());
            }
        });
        // se pone el árbol en el panel de las barras de scroll
        scrollPane.setViewportView(tree);
 
        // aquí el botón que borra el último elemento de los primeros hijos
        // es decir, desde el nodo root, borra sólo el último hijo
        JButton btnBorrarltimoNodo = new JButton("Borrar \u00FAltimo nodo");
        btnBorrarltimoNodo.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent arg0) {
 
                DefaultTreeModel arbol = (DefaultTreeModel) tree.getModel();
                DefaultMutableTreeNode padre = (DefaultMutableTreeNode) arbol.getRoot();
                int numeroDeHijos = arbol.getChildCount(padre);
 
                // borra el último hijo del padre
                arbol.removeNodeFromParent((MutableTreeNode) arbol.getChild(
                        padre, numeroDeHijos - 1));
            }
        });
        btnBorrarltimoNodo.setBounds(220, 11, 204, 23);
        contentPane.add(btnBorrarltimoNodo);
 
        // evento click del botón de carga del árbol
        // simplemente añade el nodo root y llama a la función de carga
        // para añadir todos los nodos hijos al nodo root
        btnCargarrbolDe.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent arg0) {
 
                DefaultTreeModel arbol = (DefaultTreeModel) tree.getModel();
                DefaultMutableTreeNode nroot = new DefaultMutableTreeNode(
                        "Árbol de directorios");
 
                arbol.setRoot(nroot);
 
                CargaEstructuraDirectorios(arbol, nroot, "/");
 
            }
        });
         
     
    }
 
    // función recursiva que lista todos los directorios y subdirectorios
    // a partir de una ruta, añadiéndolos a la estructura en árbol
    private void CargaEstructuraDirectorios(DefaultTreeModel arbol,
            DefaultMutableTreeNode padre, String ruta) {
        DefaultMutableTreeNode aux = null;
 
        File archivo = new File(ruta); // puntero al directorio de la ruta
        File[] archivos = archivo.listFiles(); // lista todos los archivos de la ruta
 
        // recorre lo que hay en la ruta
        if (archivos != null) {
            for (int i = 0; i < archivos.length; i++) {
 
                // creando un nodo con cada cosa del directorio
                aux = new DefaultMutableTreeNode(archivos[i].getName());
                // inserta el nodo hijo
                arbol.insertNodeInto(aux, padre, i);
 
                // si encontramos un directorio volvemos a hacer lo mismo con sus hijos
                if (archivos[i].isDirectory()) {
                    try {
                         
                        // llamando recursivamente de nuevo a ésta misma función
                        CargaEstructuraDirectorios(arbol, aux,
                                archivos[i].getAbsolutePath() + "/");
                         
                    } catch (Exception e) {
                        System.out.println(e.getMessage()); // por si acaso le he puesto un try xD
                    }
                }
 
            }
 
        }
    }
     
    // termina la creación del frame
}
 
// fin de la clase