utilizar en swing un árbol

23
Utilizar en Swing un árbol, como los que se despliegan en muchas de las ventanas de los sistemas operativos al uso, es tan simple como escribir: add( new JTree( new Object[]{ "este","ese","aquel" } ) ); Esto crea un arbolito muy primitivo; sin embargo, el API de Swing para árboles es inmenso, quizá sea uno de los más grandes. En principio parece que se puede hacer cualquier cosa con árboles, pero lo cierto es que si se van a realizar tareas con un cierto grado de complejidad, es necesario un poco de investigación y experimentación, antes de lograr los resultados deseados. Afortunadamente, como en el término medio está la virtud, en Swing, JavaSoft proporciona los árboles "por defecto", que son los que generalmente necesita el programador la mayoría de las veces, así que no hay demasiadas ocasiones en que haya que entrar en las profundidades de los árboles para que se deba tener un conocimiento exhaustivo del funcionamiento de estos objetos. Los árboles en Swing se utilizan para representar estructuras de datos jerárquicas empleando para ello la representación típica de los exploradores de ficheros. Cada dato de la estructura jerárquica es un nodo. La figura siguiente muestra la representación abstracta de nodos en la parte izquierda y, en la derecha, la estructura típica de árbol que el lector reconocerá con facilidad. El nodo superior es el nodo raíz (en este caso 1), los nodos finales se conocen como hojas (en este caso 3, 4 y 5) Y los nodos intermedios reciben el nombre de ramas (en este caso

Upload: miguel-fernandez

Post on 03-Jan-2016

111 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Utilizar en Swing un árbol

Utilizar en Swing un árbol, como los que se despliegan en muchas de las ventanas de los sistemas operativos al uso, es tan simple como escribir:

add( new JTree( new Object[]{ "este","ese","aquel" } ) );

Esto crea un arbolito muy primitivo; sin embargo, el API de Swing para árboles es inmenso, quizá sea uno de los más grandes. En principio parece que se puede hacer cualquier cosa con árboles, pero lo cierto es que si se van a realizar tareas con un cierto grado de complejidad, es necesario un poco de investigación y experimentación, antes de lograr los resultados deseados.

Afortunadamente, como en el término medio está la virtud, en Swing, JavaSoft proporciona los árboles "por defecto", que son los que generalmente necesita el programador la mayoría de las veces, así que no hay demasiadas ocasiones en que haya que entrar en las profundidades de los árboles para que se deba tener un conocimiento exhaustivo del funcionamiento de estos objetos.

Los árboles en Swing se utilizan para representar estructuras de datos jerárquicas empleando para ello la representación típica de los exploradores de ficheros. Cada dato de la estructura jerárquica es un nodo. La figura siguiente muestra la representación abstracta de nodos en la parte izquierda y, en la derecha, la estructura típica de árbol que el lector reconocerá con facilidad.

El nodo superior es el nodo raíz (en este caso 1), los nodos finales se conocen como hojas (en este caso 3, 4 y 5) Y los nodos intermedios reciben el nombre de ramas (en este caso 2). También se puede hablar de nodos padre y entonces los nodos que cuelgan de él se conocen como nodos hijo.

La clase JTree

La clase JTree es la punta del iceberg que constituye la implementación de árboles en Swing. Se trata de un componente que puede ser incorporado a cualquier contenedor Swing, siendo lo más habitual añadirlo a un contenedor JScrollPane para proporcionar desplazamiento vertical al árbol cuando se expandan sus nodos. La clase JTree es la que tiene asignados los roles de Vista y Controlador del patrón MVC utilizado por Swing.

Un objeto JTree necesita de un objeto de tipo TreeModel a partir del cual poder obtener información. Este objeto TreeModel se puede crear explícita o implícitamente a partir de

Page 2: Utilizar en Swing un árbol

otra fuentes como vectores o tablas hash. TreeModel es pues una interfaz que proporciona los datos que se van a prsentar a través de JTree, constituyendo la parte del Modelo de patrón MVC. Esta interfaz describe métodos para consultar aspectos de la topología del árbol, incluyendo el nodo raíz. La clase receptora de eventos TreeModelListener se utiliza cuando es importante conocer cambios en la estructura del TreeModel. Swing proporciona una implementación por defecto, la interfaz TreeModel, la clase DefaultTreeModel.

La clase TreeNode es quizás la más importante dentro de la generación de árboles Swing. Proporciona métodos para nombrar a los hijos, determinar cuándo se pueden añadir hijos a un nodo padre, determinar cuándo un nodo es una hoja y métodos para localizar el nodo padre de unodo cuaquiera. Resulta también importante la interfaz MutableTreeNode que extiende la interfaz TreeNode, incorporándole métodos para insertar y eliminar nodos hijos, cambiar el padre de un nodo y almacenar un objeto cualquiera en un nodo. Sin embargo, esta interfaz no proporciona ningún método para recuperar un objeto almacenado en un nodo, pr lo que el desarrollador no está obligado a permitir que los objetos allmacenados en un árbol puedan ser recuperados. Afortunadamente, la implementación de la interfaz, la clase DefaultMutableTreeNode, sí proporciona un método para esta acción.

La clase DefaultMutableTreeNode dispone de un constructor por defecto que crea un nodo, sin ningún objeto almacenado, al cual se pueden añadir hijos. También dispone de otros constructores que permiten incorporar objetos al nodo e indicar si ese nodo puede o no tener hijos.

En resumen, la clase JTree puede construirse de varias formas. La primera de ellas consiste en utilizar el constrctor por defecto que creará un objeto TreeModel automáticamente. Otra forma es mediante un array de objetos de tipo Object, Vector o Hash, en cuyo caso el modelo se creará en base a los datos que proporcionen estos objetos. Y la tercera forma de construir una clase JTree es mediante un objeto TreeModel o TreeNode propios, siendo ésta la más flexible de las tres y la que se utiliza en el siguiente ejemplo.

En el ejemplo java1414.java, se utilizan instancias de la clase DefaultMutableTreeNode para crear un modelo para JTree y establecer las relaciones jerárquicas entre los componentes del árbol. El código que se muestra reproduce la creación de estos objetos.

import java.awt.*;import java.awt.event.*;import javax.swing.*;import javax.swing.tree.*;

// Esta clase coge un array de Strings, haciendo que el primer elemento// del array sea un nodo y el resto sean ramas de ese nodo// Con ello se consiguen las ramas del árbol general cuando se pulsa// el botón de testclass Rama { DefaultMutableTreeNode r; public Rama( String datos[] ) { r = new DefaultMutableTreeNode( datos[0] ); System.out.print ( "Agregando " + datos[0] + ": " ); for ( int i=1; i < datos.length; i++ ) {

Page 3: Utilizar en Swing un árbol

r.add( new DefaultMutableTreeNode( datos[i] ) ); System.out.print ( " " + datos[i] ); } System.out.print ( "\n" ); }

public DefaultMutableTreeNode node() { return( r ); }}

public class java1414 extends JPanel { String datos[][] = { { "Colores","Rojo","Verde","Azul"}, { "Sabores","Salado","Dulce","Amargo"}, { "Longitud","Corta","Media","Larga"}, { "Intensidad","Alta","Media","Baja"}, { "Temperatura","Alta","Media","Baja"}, { "Volumen","Alto","Medio","Bajo"}, }; static int i=0; DefaultMutableTreeNode raiz,rama,seleccion; JTree arbol; DefaultTreeModel modelo;

JButton botonPrueba = new JButton( "Púlsame" );

public java1414() { setLayout( new BorderLayout() ); raiz = new DefaultMutableTreeNode( "raíz" ); arbol = new JTree( raiz ); // Se añade el árbol y se hace sobre un ScrollPane, para // que se controle automáticamente la longitud del árbol // cuando está desplegado, de forma que aparecerá una // barra de desplazamiento para poder visualizarlo en su // totalidad add( new JScrollPane( arbol ),BorderLayout.CENTER ); // Se obtiene el modelo del árbol modelo =(DefaultTreeModel)arbol.getModel(); // Y se añade el botón que va a ir incorporando ramas // cada vez que se pulse botonPrueba.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent evt ) { if( i < datos.length ) { rama = new Rama( datos[i++] ).node(); // Control de la útlima selección realizada seleccion = (DefaultMutableTreeNode) arbol.getLastSelectedPathComponent(); if( seleccion == null ) seleccion = raiz; // El modelo creará el evento adecuado, y en respuesta // a él, el árbol se actualizará automáticamente modelo.insertNodeInto( rama,seleccion,0 ); if ( i == datos.length ) botonPrueba.setEnabled( false );

Page 4: Utilizar en Swing un árbol

} } } );

// Cambio del color del botón botonPrueba.setBackground( Color.blue ); botonPrueba.setForeground( Color.white ); // Se crea un panel para contener al botón JPanel panel = new JPanel(); panel.add( botonPrueba ); add( panel,BorderLayout.SOUTH ); }

public static void main( String args[] ) { JFrame frame = new JFrame( "Tutorial de Java, Swing" ); frame.setDefaultCloseOperation( frame.EXIT_ON_CLOSE ); frame.getContentPane().add( new java1414(),BorderLayout.CENTER ); frame.setSize( 200,500 ); frame.setVisible( true ); }}

En la aplicación se crea el nodo raíz arbol al cual se añaden los hijos color, sabor y medida, a quienes se añaden tabmién otros hijos para generar en la última línea de código el objeto JTree, que internamente crea un TreeModel en base al nodo raíz, incorporándole un TreeModelListener para ese TreeModel, que será el encargado de sincronizar la presentación con los cambios que se vayan produciendo en el TreeModel o en cualquiera de los TreeNode asociados, todo ello de forma transparente para el programador. La figura siguiente muestra el árbol creado en la ejecución del ejemplo

Page 5: Utilizar en Swing un árbol

En la figura se observa cómo los nodos que contienen elementos colgando se presentan de forma diferente a los nodos finales. Ello se debe que JTree utiliza el método isLeaf() para determinar el icono y en la implementación por defecto de DefaultMutableTreeNode se devuelve true cuando un nodo no tiene ningún hijo. No obstante, aunque éste es el funcionamiento normal, puede que en ocasiones no sea el adecuado, por lo que será necesario extender la clase DefaultMutableTreeNode para que se adapte a circunstancias particulares, si fuera necesario. Por ejemplo, si se está utilizando un árbol para la prsentación del contenido de una unidad de disco, la representación que se muestra está basda en directorios y archivos, en donde es más importante un directorio, que representa la capcidad de contener archivos, que la existencia de esos archivos. Es decir, que si se utiliza la implementación por defecto de la clase DefaultMutableTreeNode, un directorio vacío aparecerá representando mediante el icono correspondiente a un archivo, ya que no contiene hijos; por ello, ésta es una de las ocasiones en las que será necesario proporcionar carcterísticas nuevas a la clase para que presente correctamente los directorios vacíos.

Aplicación: Árbol personalizado

Page 6: Utilizar en Swing un árbol

El ejemplo java1434.java es un poco más complejo que los demás presentados en este capítulo, de forma que permita mostrar al lectro la forma de personalizar el funcionamiento y apariencia de un objeto JTree en Swing. La aplicación construye un nueve objeto JVolumenTree que es capaz de mostrar visualmente el tamaño de cada uno de los nodos del árbol, además de su nombre y estructura, que son los elementos habituales de JTree. Por ejemplo, si se utiliza esta nueva clase para presentar la estructura de archivos de una unidad de almacenamiento de datos, será posible observar el tamaño relativo de todos los archivos y directorios de esa unidad.

En la construcción de la aplicación se utiliza una implementación propia de la clase TreeNode en conjunción con la clase proporcionada por Java, DefaultTreeModel. La implementación de TreeNode permitirá almacenar la información referente al tamaño de cada uno de los nodos, ya que la obtención de este dato es una operación costosa y es recomendable realizarla una sola vez, almacenando luego el resultado. Además, al momento de visualizar el contenido del árbol, será necesario presentar la información referente al tamaño de cada nodo, por lo que es imprescindible disponer de métodos para acceder a esa información. Sin embargo, el funcionamiento de despliegue y presentación del conjunto del árbol reaccionará del modo habitual, por lo que es suficiente el uso de la clase DefaultMutableTreeNode para manipular los objetos TreeNode personalizados que se hayan creado en la ejecución de la aplicación.

Ni la interfaz TreeNode, ni la interfaz MutableTreeNode, que extiende a la anterior, definen el tamaño del nodo. Por ello, la primera acción consiste en crear una nueva interfaz que extienda a MutableTreeNode y proporcione los métodos necesarios para poder recuperar esa información al momento de la presentación del nodo en pantalla. El código correspondiente a la interfaz VolumenNodo se presenta a continuación.

import java.text.*;import java.util.*;import javax.swing.tree.*;

public interface VolumenNodo extends MutableTreeNode { // Devuelve el tamaño del nodo excluyendo a sus hijos public long getTamano();

// Devuelve el tamaño total del nodo y sus descendientes public long getTamanoTotal();

// Devuelve el número total del descendientes del nodo public int getLeafCount();

// Devuelve el nodo raíz de la jerarquía en que se encuentra // englobado el nodo public TreeNode getRoot();

// Devuelve true si el tamaño total ya ha sido calculado, o false // en caso contrario public boolean estaCalculado();}

Page 7: Utilizar en Swing un árbol

El lector puede observar que no se hacen indicaciones de la forma en que se va a obtener el tamaño a través del método getTamano. Por ejemplo, si el árbol se utiliza para visualizar un sistema de ficheros, el tamaño de cada fichero se puede obtener mediante el método File.length(), mientras que si la jerarquía representa cualquier otro tipo de objetos, será necesario proporcionar otro mecanismo de obtención del tamaño del nodo.

Tampoco se hacen indicaciones de la forma en que el tamaño de los nodos va a a ser almacenado. Esta tarea puede ser muy pesada y nada trivial, por lo que se proporciona el método estaCalculado para indicar cuando un nodo ya conoce su propio tamaño.

El siguiente paso en la construcción del árbol de esta aplicación consiste en proporcionar una implementación por defecto para la interfaz VolumenNodo, de forma que cada uno de los métodos de la interfaz disponga de su propia implementación y sea el programador quien decida que métodos debe seobreescribir. En este caso, la clase DefaultMutableTreeNode ya proporciona la implementación por defecto para algunos de los métodos, como getRoot() y getLeafCount(); no obstante, será necesario sobreescribir este último para adecuarlo a su cometido en la aplicación.

Por lo tanto, la clase DefaultVolumenNodo será la que implemente la interfaz VolumenNodo y extienda la clase DefaultMutableTreeNode, proporcionando miembros para almacenar el tamaño del nodo, el tamaño del nodo con sus descendientes y el número de descendientes del nodo. También proporciona un indicador, inicializado a false, que permitirá conocer si los tamaños de los nodos hijos han sido calculados. El código que se muestra a continuación, DefaultVolumenNodo, reproduce la declaración de los miembros de la clase y el constructor.

public class DefaultVolumenNodo extends DefaultMutableTreeNode implements VolumenNodo { // La instancia del helper para este nodo private final VolumenNodoHelper helper; // Tamaño del nodo escluyendo los hijos private final long tamano; // Tamaño total del nodo más el de todos sus descendientes private long totalTamano; // Almacena el número total de hijos que son descendientes de este // nodo. Debe mantenerse en un caché, para proporcionar mejor // rendimiento a la implementación del método getLeafCount() de // la implementación por defecto DefaultMutableTreeNode private int totalNodos; // Indica si se ha concluido el cálculo de todos los hijos del nodo private boolean calculado; // Indica si el árbol de descendientes del nodo se ha consctruido private boolean explorado = false;

// Contruye un nodo a partir del objeto que se proporciona como // parámetro y la clase de ayuda que también debe indicarse public DefaultVolumenNodo( final Object obj, final VolumenNodoHelper _helper ) { // Guarda los parámetros de la invocación super( obj ); helper = _helper;

Page 8: Utilizar en Swing un árbol

// Obtiene el tamaño del objeto a través de la clase auxiliar tamano = helper.getTamano( obj ); // Inicializa el tamaño total del nodo al tamaño del nodo, // que posteriormente será aumentado en el tamaño de los // nodos hijos totalTamano = tamano; // Inicializa el número de nodos a 0, o a 1 si estamos ante // una hoja, para ques e tenga en cuenta a la hora de calcular // el tamaño completo del nodo padre totalNodos = helper.esContenedor(obj) ? 0 : 1; // Si el nodo no permite tener hijos, se da por concluido el // cálculo del tamaño de ese nodo calculado = !getAllowsChildren(); }

Para que la representación del árbol pueda llevarse a cabo, será necesario proporcionar los datos que necesita esta clase correspondientes al texto que se presentará en el árbol como identificador de cada uno de los nodos, a la indicación de si el nodo es un contenedor o no, para poder determinar su ícono, y finalmente, el tamaño del nodo. Esa información se puede proporcionar de tres formas distintas:

La clase DefaultVolumenNodo puede disponer de métodos abstractos para devolver la información, de forma que se puedan especializar esos métodos para cada tipo de nodo que se vaya a representar.

La clase DefaultVolumenNodo puede recibir esa información a través del constructor o mediante métodos set() que permitan fijar esos valores.

La provisión de esa información se puede dejar en manos de una classe auxiliar de ayuda, en un objeto de tipo Helper.

En este ejemplo se utiliza la tercera opción, porque es la más ilustrativa a efectos de aprendizaje de las características de Java. Esta decisión hace necesaria la definición de una nueva interfaz, VolumenNodoHelper, para proporcionar la información necesaria citada anteriormente. La clase DefaultMutableTreeNode proporciona la noción del objeto de usuario asociado a cada nodo, que se va a utilizar en este caso como clave para solicitar del objeto Helper la información acerca del nodo. La interfaz define tres métodos, tal como se muestra en el siguiente código.

public interface VolumenNodoHelper { // Devuelve la representación del nodo en forma de cadena public String toString( Object obj );

// Devuelve el conjunto de objetos de los nodos hijos del nodo que // se pasa como parámetro public Object[] getHijos( Object obj );

// Indica si el nodo representado por el objeto que se pasa como // parámetro es un contenedor public boolean esContenedor( Object obj );

// Devuelve el tamano del nodo representado por el objeto que se // pasa como parámetro, excluyendo a cualquiera de los hijos public long getTamano( Object obj );

Page 9: Utilizar en Swing un árbol

}

El objeto Helper se proporciona a la clase en el constructor y ésta lo almacena para su uso posterior. Se puede utilizar el mismo objeto en el constructor de todos los nodos de la jerarquía del árbol. Si el lector observa el constructor de la clase DefaultVolumenNodo, advertirá que el miembro que representa el tamaño del nodo se inicializa con el tamaño del propio nodo, al que se irá sumando el tamaño de todos los nodos hijos de que disponga en la jerarquía del árbol.

Ya se ha indicado antes que la obtención del tamaño de un nodo puede ser una tarea complicada y sobre todo, que consuma mucho tiempo, por lo que debería realizarse en una única ocasión. Sin embargo, es importante que el cálculo se realice una vez conocido el tamaño de todos los nodos hijos. Un nodo no puede saber si el cálculo del tamaño de sus hijos está concluido, por lo que es necesario proporcionar un método para indicar a ese nodo que ya se puede iniciar el cálculo de su propio tamaño, una vez conocido el de sus hijos.

El método calcular() se añade a cada nodo como encargado de ejecutar la tarea anterior. Primero invocará a los métodos calcular() de todos los hijos, para luego realizar el cálculo de su propio tamaño. Por lo tanto, lo único necesario es invocar este método sobre el nodo raíz, una vez que la jerarquía de nodos del árbol esté completa, y será el propio método calcular() de ese nodo raíz el que recorra toda la jerarquía, calculando el tamaño de cada uno de los nodos, realizando los cálculos necesarios y sin ningún tipo de redundancia. La implementación del método es el que se muestra a continuación.

// Calculamos el tamaño total del nodo y el número de hijos// que dependen de élpublic void calcular( final DefaultTreeModel modelo ) { // Nos aseguramos de que hemos construido el árbol completo // del nodo explorar( modelo );

// Para cada uno de los nodos hijos, hacemos lo siguiente... final Enumeration hijos = children(); while( hijos.hasMoreElements() ) { final DefaultVolumenNodo node = (DefaultVolumenNodo)hijos.nextElement();

// Si el hijo es un contenedor, seguimos recursivamente a través // de sus hijos y almacenamos los resultados if( node.getAllowsChildren() ) { node.calcular( modelo ); totalTamano += node.getTamanoTotal(); totalNodos += node.getLeafCount(); } // Si el hijo es una hoja, acumulamos los valores else { totalTamano += node.getTamano(); totalNodos++; } } // Indicamos que se ha acabado el cálculo del tamaño total

Page 10: Utilizar en Swing un árbol

calculado = true;

// La llamada se engloba en un try-catch para evitar la excepción // de Swing de finalización de la exploración try { // Indicamos al modelo que hemos cambiado el nodo modelo.nodeChanged( this ); } catch( NullPointerException e ) { e.printStackTrace(); }}

Cuando el cálculo del tamaño de un nodo está completo, se actualiza el valor de la variable miembro que almacena esa información y se asigna el valor de true al indicador de que el tamaño de ese nodo es conocido, de modo que el método estaCalculado devolverá true a partir de ese instante.

Una mejora más que todavía se puede incorporar es automatizar la construcción de la jerarquía de nodos del árbol, de forma que si ya existe previamente, solamente se recorra, sin necesidad de volver a crear nodo alguno, ni realizar cálculos. Siguiendo el ejemplo del sistema de ficheros, se puede enumerar la jerarquía de cualquier directorio tomando ese directorio como nodo raíz y recorriendo todos sus subdirectorios y ficheros que pueda contener, obteniendo de este modo todos sus hijos. Entonces, se puede incorporar un nuevo método a la clase Helper, getHijos(), al cual se pasará como parámetro el nodo a partir del cual se quiere obtener la jerarquía del árbol.

Para recorrer la jerarquía de nodos sin realizar cálculos se añade el método explorar(), de forma que se creen automáticamente los nodos hijos. Es importante asegurar que explorar() solamente se invoque una vez, de forma que cada uno de los nodos se añada en una única ocasión a la jerarquía. Para ello se incorpora una variable booleana como miembro que indicará si el método explorar() sobre un nodo determinado ya ha sido invocado. La implementación de este método es la siguiente:

// Explora el nodo actual, constuyendo los nodos hijos y añadiéndolos // al modelo del árbol. Este método debe ser invocado antes de que el // nodo se expanda en la vista de árbol y antes de que se relicen las // búsuqedas recursivas public synchronized void explorar( final DefaultTreeModel modelo ) { // Controlamos que no se haya explorado ya if( !explorado ) { // Recuperamos el conjundo de hijos, si los hay final Object[] hijos = helper.getHijos( getUserObject() );

for( int hijo=0; hijo < hijos.length; hijo++ ) { // Para cada uno de ellos se contruye un nuevo nodo, // utilizando la misma clase de ayuda y añadiéndolo add( new DefaultVolumenNodo(hijos[hijo],helper) ); }

// Indicamos que esta rama ya está explorada explorado = true; // La llamada se engloba en un try-catch para evitar la

Page 11: Utilizar en Swing un árbol

// excepción de Swing de finalización de la exploración try { // Notificamos al modelo que la estructura ha cambiado modelo.nodeStructureChanged( this ); } catch( NullPointerException e ) { e.printStackTrace(); } } }

Invocando a este método al inicio del método calcular() se asegura que todos los hijos son creados con su tamaño adecuado. Además, la expansión de cada nodo en sus nodos hijos se realizará cuando se solicite, y no antes, de forma que no se realice ningún trabajo en vano, es decir, si un nodo nunca va a ser necesario, nunca será creado; sin embargo, la naturaleza recursiva del método calcular hace que una sola llamada a este método sobre el nodo raíz del árbol, cree la jerarquía de nodos comleta y el tamaño de cada uno de ellos sea calculado.

Este método se encuentra protegido contra llamadas repetidas a través de la variable explorado, que indica si la jerarquía del nodo ya ha sido revisada o no, de forma que esa jerarquía solamente se construya la primera vez que se llama al nodo. Luego es posible aprovechar esa circunstancia para que en el caso de que el usuario decida abrir una rama no creada se invoque a este método sobre ese nodo en ese mismo instante y, sino, ya será invocado por calcular() posteriormente, pero el resultado será el mismo.

La clase VolumenNodoRenderer

La interfaz TreeCellRenderer define el método getTreeCellRendererComponent(), que devuelve un objeto Component que puede ser personalizado de acuerdo a los parámetros del método. Éste es el método que utiliza la clase JTree a la hora de visualizar un nodo.

Los parámetros del método describen el estado del nodo que va a ser presentado, como por ejemplo si está seleccionado, si está expandido, si es un nodo final o si tiene el foco. Son muchos parámetros e imprescindibles, porque hay información que solamente posee el objeto JTree, no la clase TreeModel o cualquiera de las instancias de TreeNode. Por ejemplo, el modelo no sabe nada acerca de si un nodo está expandido o no, porque según el patrón MVC puede haber dos o más vistas activadas sobre un mismo árbol, presentando cada una de ellas un estado diferente.

Para atender a todas estas diferencias, que resultan muy significativas a la hora de visualizar un árbol, Swing proporciona la clase DefaultTreeCellRenderer, que implementa la interfaz TreeCellRenderer. Esta clase, además de implementar la interfaz, también extiende la clase JLabel de modo que puede presentar un ícono con un texto adyacente.

En la aplicación que atañe a este capítulo es necesario presentar también información gráfica acerca del tamaño del nodo. Como no se pretende ser preciso, sino solamente mostrar al lector las técnicas de uso de JTree y, como una imagen es mucho más

Page 12: Utilizar en Swing un árbol

represantiva que un texto, se va a incorporar a la identificación de cada nodo una imagen con una gráfica de pastel que represente el tamaño del nodo. Es decir, además del icono y texto estándares de cada nodo, se añadirá una imagen más, y la clase VolumenNodoRenderer será la encargada de esa misión.

El método para añadir esa imagen consiste en aumentar en el JLabel de identificación del nodo, la separación entre el icono y el texto, colocando en medio la imagen que indica el tamaño del nodo. A la clase VolumenNodoRenderer se pasará un array de imágenes representando el tanto por ciento del tamaño relativo de cada nodo respecto a su padre; así que dependerá del número de imágenes proporcionadas el que cada gráfico sea más o menos preciso. Un detalle importante es la distinción entre un nodo vacío y otro que está casi vacío, pero que tiene un tamaño, aunque sea mínimo; para ello, en el constructor de la clase VolumenNodoRenderer, que se reproduce a continuación, se ha añadido un parámetro para indicar la imagen que se presentará cuando se produzca esta circunstancia.

public VolumenNodoRenderer( final Image[] _imagenes, final Image _imgVacio,final Image _imgCalculando ) { // Almacenamos los parámetros en las variables miembro de la // clase imagenes = _imagenes; imgVacio = _imgVacio; imgCalculando = _imgCalculando; umbralVacio = (1.0 / (_imagenes.length - 1)) / 2.0;

// Fijamos el modo de presetnación inicial setModoPresentacion( JVolumenTree.TAM_RELATIVO_RAIZ ); // Preparamos el controlador de las imágenes, indicándole su // tamaño para que controle su carga imgTrack = new CargaImagenes( imagenes,imgVacio,imgCalculando );}

Cuando se invoque al método getTreeCellRendererComponent(), se seleccionará la imagen adecuada al tamaño del nodo, asumiendo que ese tamaño ya está calculado. El método setImagen() será el encargado de elegir la imagen correcta entre el array de imágenes que se haya proporcionado al constructor de la clase.

// Este es el método encargado de seleccionar la imagen que// corresponde al tamaño del nodo que se indicaprivate Image selImagen( final double proporcion, final VolumenNodo nodo,final VolumenNodo nodoCompara ) { Image imagen;

// Si todavía no tenemos imagen seleccionada y el nodo o el que // sirve como referencia no ha sido añadido todavía, mostramos // la imagen de que estamos calculando if( (imgCalculando != null) && ( !nodo.estaCalculado() || ( (nodoCompara != null) && !nodoCompara.estaCalculado()) ) ) imagen = imgCalculando; // Si el tamaño es mayor que cero, pero está por debajo del // umbral del tamaño que sirve de límite para indicar que un // nodo está casi vacío, peresentamos esa imagen else if( (imgVacio != null) && (proporcion > 0.0) &&

Page 13: Utilizar en Swing un árbol

(proporcion < umbralVacio) ) imagen = imgVacio; // En cualquier otro caso, presentamos la imagen que corresponda // a la proporción del tamaño del nodo else { final int index = (int)Math.round( (imagenes.length-1) * Math.min(1.0,proporcion) ); imagen = imagenes[index]; } return( imagen );}

Sistema de ficheros

En las secciones anteriores se han descrito las interfaces y clases que permiten crear un JTree personalizado que ahora se utilizará para crear la clase JVolumenTree, que mostrará la jerarquía de un sistema de ficheros indicando el tamaño de cada uno de los directorios y archivos que lo componen.

Para recuperar información acerca de los directorios y archivos de un sistema de ficheros, Java proporciona la clase File en el paquete java.io, cuyos métodos pueden recuperar información acerca del nombre del archivo o directorio, getName(); distinción de si se trata de un archivo o un directorio, isDirectory(); tamaño del archivo o directorio, length(); o la lista de archivos que contiene un directorio, listFiles().

En el caso del sistema de ficheros, es necesario proporcionar una implementación adecuada de la interfaz VolumenNodoHelper que aunque simple, porque todo el trabajo reside en cada nodo, sí debe obtener la información de la forma adecuada. La implementación a través de la clase FileVolumenNodoHelper se reproduce a continuación.

import java.io.File;

public class FileVolumenNodoHelper implements VolumenNodoHelper { // Devuelve el nombre del fichero que se pasa, o el directorio si // el nombre está en blanco public String toString( final Object object ) { final File file = (File)object; final String name = file.getName(); return( (name.length() > 0) ? name : file.getPath() ); }

// Devuelve el array de ficheros que cuelgan del que se pasa public Object[] getHijos( final Object object ) { File[] ficheros;

if( (ficheros = ((File)object).listFiles()) == null ) ficheros = new File[0];

return( ficheros ); }

// Indica si el fichero que se pasa es un directorio public boolean esContenedor( final Object obj ) {

Page 14: Utilizar en Swing un árbol

return( ((File)obj).isDirectory() ); }

// Devuelve el tamaño del fichero que se pasa como parámetro public long getTamano( final Object obj ) { return( ((File)obj).length() ); }}

Para hacer más sencillo el uso de todas las características propias que se han descrito en las secciones anteriores, se introduce ahora la clase JVolumenTree, que extiende a la clase JTree de Swing.

La característica principal de esta nueva clase es su contructor, en donde el nodo raíz es creado a través del objeto Helper, se aplica el visualizador y se invoca el método calcular() sobre el nodo raíz, que será el encargado de crear la jerarquía completa del árbol y calcular el tamaño de cada uno de los nodos.

// Este es el constructor al que se debe indicar el nodo raiz a partir // del cual se expandirá el árbol, el objeto "helper" que se usará en // la expansión del árbol y presentación de la información textual, y // las imágenes mediante las cuales se vidualizará la magnitud que // corresponde a cada uno de los nodos public JVolumenTree( final Object raiz,final VolumenNodoHelper helper, final Image[] imagenes,final Image imgVacia, final Image imgCalculando ) { // Crea el nodo raiz, crea el modelo del árbol con ese nodo y // contruye el objeto Swing JTree con ese modelo super( new DefaultTreeModel( new DefaultVolumenNodo(raiz,helper) ) );

// Utilizamos el renderer con las imágenes para mostrar el tamaño // con respecto al padre renderCelda = new VolumenNodoRenderer( imagenes, imgVacia,imgCalculando ); renderCelda.setModoPresentacion( TAM_RELATIVO_PADRE ); setCellRenderer( renderCelda ); // Nos aseguramos de que el árbol permita Tooltips, que es donde // presentaremos la información textual del tamaño de los nodos ToolTipManager.sharedInstance().registerComponent( this );

// Incorporamos un receptor de eventos para controlar la expansión // del árbol addTreeExpansionListener( new TreeExpansionListener() { public void treeCollapsed( TreeExpansionEvent evt ) {}

public void treeExpanded( TreeExpansionEvent evt ) { // Comprobamos que el nodo que está siendo expandido ya ha // sido explorado. Para ello lo localizamos y nos aseguramos // de que se ha explorado

Page 15: Utilizar en Swing un árbol

final DefaultVolumenNodo nodo = (DefaultVolumenNodo)evt.getPath().getLastPathComponent(); nodo.explorar( (DefaultTreeModel)getModel() ); } } ); // Iniciamos la tarea en segundo plano que calcula los tamaños inicioCalculo(); }

El ejemplo java1434.java es una muestra del uso que se puede dar a la clase definida anteriormente. Su código es muy simple y se basa en la clase JVolumenFrame que crea la ventana y el contenedor sobre el que se presenta el árbol jerárquico del sistema de ficheros que se indique. La siguiente figura muestra el resultado.

La última línea del constructor de la clase invoca al método inicioCalculo() que lanza una tarea en segundo plano para realizar los cálculos de tamaño de directorios y ficheros, de forma que el usuario siga teniendo el control de la aplicación. El código del método es el que se reproduce en las siguientes líneas.

Page 16: Utilizar en Swing un árbol

// Inicia la tarea demonio que se encarga de calcular el tamaño de// los nodos hijosprivate void inicioCalculo() { // Ejecutamos la acción como una tarea en segundo plano final Thread t = new Thread() { public void run() { // Localizamos el nodo raiz e invocamos al método calcular() // sobre él final DefaultTreeModel modelo = (DefaultTreeModel)getModel(); final DefaultVolumenNodo raiz = (DefaultVolumenNodo)modelo.getRoot(); raiz.calcular( modelo ); // Nos aseguramos de que el árbol actualiza la información repaint(); } }; // Fijamos la prioridad mínima a la tarea t.setPriority( Thread.MIN_PRIORITY ); // Y la convertimos en demonio t.setDaemon( true ); t.start();}

Las tres últimas líneas de código son especialmente interesantes. En la primera se asigna a la tarea una prioridad muy baja, de forma que aunque Java no ofrece garantía alguna acerca del scheduling de tareas, esto asegurará que cualquier otra tarea con una prioridad superior tendrá preferencia a la tarea del ejemplo. Esta circunstancia es muy importante, ya que así se evita la interferencia con tareas que necesitan una rápida ejecución, como por ejemplo la respuesta a la interfaz de usuario. La segunda sentencia convierte a la tarea en un demonio, es decir, que si la máquina virtual Java debe cerrarse, lo hará aunque esa tarea esté en ejecución, ya que lo contrario no tendría sentido. La máquina virtual Java concluye su ejecución cuando todas las tareas no-demonio hayan concluido, o cuando se invoque al método System.exit(), lo que ocurra primero. La tercera línea es la encargada de arrancar la ejecución de la tarea.

En el funcionamiento del ejemplo hay que tener en cuenta que el árbol puede aparecer antes que se haya concluido la jerarquía completa, por lo que el usuario debe esperar para poder interactuar con el árbol. Es muy importante, entonces, asegurar que las vistas del árbol se actualizan conforme se vayan añadiendo nodos a la jerarquía durante las llamadas al método calcular().

En el patrón MVC se especifica que el Modelo debe notificar a cualquier Vista registrada todos los cambios que se produzcan. En Swing, esto se consigue añadiendo receptores de eventos al Modelo. El objeto JTree asegurará que se ha añadido un receptor TreeModelListener al modelo del árbol, de forma que se realice un redibujado del árbol cada vez que se reciba una notificación de cambio.

Par que el árbol vaya cambiando a través de la jerarquía de nodos, el modelo debe lanzar una notificación de tipo nodeStructureChanged(). Como la clase VolumenNodo no conoce el modelo asociado al árbol, es necesario pasarlo como parámetro al método explorar(), que

Page 17: Utilizar en Swing un árbol

es llamado directamente por el método calcular(), luego este último también debe recibir el modelo como parámetro.

Cuando la ejecución calcular() llegue a su fin, ya se habrán realizado todas las acciones necesarias para conocer el tamaño de los nodos de la jerarquía en ese punto, por lo que puede enviar la notificación de actualización del árbol invocando al método nodeChanged() de la clase DefaultTreeModel

En el constructor se utiliza el método addTreeExpansionListener() para detectar cuando el usuario desea expandir una rama del árbol. La incorporación de este receptor de eventos implica que el usuario pueda intentar expandir una rama en el mismo instante en que calcular() accede a esa misma rama, en cuyo case es posible que las dos tareas se ejecuten sobre ese mismo nodo y se obtenga el doble de hijos de los reales. Colocando la asignación del valor true a la variable de control del método explorar() al principio del método se disminuye el riesgo de que se produzca el conflicto, aunque no se elimine completamente.

En esta aplicación se han intentado mostrar al lector el manejo de árboles en Swing, presentando algunas de las técnicas que permiten su adecuación a fines concretos y también, el modo de evitar algunos de los efectos colaterales que siempre lleva aparejado el uso de componentes de este tipo y que el lector debe tener siempre presentes a la hora de realizar sus propias aplicaciones.