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 |