Tutorial de Java

Arboles

Anterior | Siguiente

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.

El ejemplo java1414.java, utiliza estos árboles predefinidos, para presentar un árbol en un panel. Cuando se pulsa el botón, se añade un nuevo sub-árbol a partir del nodo que está seleccionado; si no hay ninguno, se añade en el raíz.

import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;
import com.sun.java.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 test
class Rama {
  DefaultMutableTreeNode r;
  public Rama( String datos[] ) {
    r = new DefaultMutableTreeNode( datos[0] );
    for( int i=1; i < datos.length; i++ )
      r.add( new DefaultMutableTreeNode( datos[i] ) );
  }
  
  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;
  
  public java1414() {
    setLayout( new BorderLayout() );
    raiz = new DefaultMutableTreeNode( "raiz" );
    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
    JButton botonPrueba = new JButton( "Pulsame" );
    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 );
        }
      }
    } );
    
    // 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.addWindowListener( new WindowAdapter() {
      public void windowClosing( WindowEvent evt ) {
        System.exit( 0 );
      }
    });
    frame.getContentPane().add( new java1414(),BorderLayout.CENTER );
    frame.setSize( 200,500 );
    frame.setVisible( true );
  }
}

La figura siguiente muestra la ventana de ejecución del ejemplo, una vez pulsado el botón varias veces y con algunos de los sub-árboles desplegados.

En la aplicación, la primera clase, Rama, coge un array de String y genera uno de los nodos el árbol, con la primera cadena como raíz y el resto como sus hojas. Posteriormente, se pueden hacer llamadas al método node() para generar la raíz de esta rama.

La clase JTree contiene un array de String de dos dimensiones, a partir del cual salen las ramas, y un contador para moverse a través del array. Los objetos DefaultMutableTreeNode contienen los nodos, pero la representación física sobre la pantalla está controlada por la clase JTree y su modelo asociado, el DefaultTreeModel. Cuando el JTree se incorpora al Frame, es incrustado automáticamente en un JScrollPane, para proporcionar desplazamiento o scroll automático al árbol.

El JTree está controlado a través de un modelo. Cuando se hace un cambio en el modelo, este modelo genera un evento que hace que el JTree realice las actualizaciones necesarias en la representación visible del árbol. En el método init(), en el caso de un applet, o en el constructor, en el caso más general, el modelo que se utiliza es capturado a través del método getModel(). Cuando se pulsa un botón, se crea una nueva rama. Entonces, el Componente seleccionado actual será encontrado (o se convertirá en el raíz, en el caso de que no se encuentre) y el método insertNodeInfo() del modelo hará el trabajo de cambiar el árbol y hacer que se actualice.

En la mayor parte de las ocasiones, un ejemplo como el anterior es capaz de solucionar la mayoría de los problemas de implementación que se propongan. Sin embargo, los árboles tienen el poder de hacer casi cualquier cosa que se pueda imaginar; por ejemplo, en el programa anterior se puede sustituir la palabra "default" por cualquier otra clase para proporcionar un entorno de funcionamiento diferente. Pero hay que andar con cuidado, porque muchas de estas clases tienen un interfaz muy intenso, por lo que puede llevar mucho tiempo el comprender todo el intríngulis de los árboles.

Ya en la discusión de JList, se introdujeron los modelos de datos propios, es decir, una forma determinada de controlas el funcionamiento de uno de los Componentes de Swing. Pero no solamente las listas permiten hacer esto, los árboles también soportan estos modelos, aunque en este caso, debido a la naturaleza jerárquica de los árboles, el modelo es bastante más complejo.

En el ejemplo java1415.java se recrea un árbol que contiene las cartas de una baraja, utilizando un modelo de datos propio para la presentación y manipulación de cada uno de los elementos de las ramas del árbol.

En el código del ejemplo, se incluye la clase anidada que es la que controla cada uno de los elementos.

  public class MiRendererDeArbol extends JLabel implements TreeCellRenderer {
    private ImageIcon imgBaraja;
    private ImageIcon[] imgPalos;
    private ImageIcon[] imgCartas;
    private boolean seleccionado;
    
    public MiRendererDeArbol() {
      // Cargamos las imgenes de las cartas
      imgBaraja = new ImageIcon( "_palos.gif" );
      imgPalos = new ImageIcon[4];
      imgPalos[0] = new ImageIcon( "_copas.gif" );
      imgPalos[1] = new ImageIcon( "_oros.gif" );
      imgPalos[2] = new ImageIcon( "_espadas.gif" );
      imgPalos[3] = new ImageIcon( "_bastos.gif" );
      imgCartas = new ImageIcon[10];
      imgCartas[0] = new ImageIcon( "_as.gif" );
      imgCartas[1] = new ImageIcon( "_dos.gif" );
      imgCartas[2] = new ImageIcon( "_tres.gif" );
      imgCartas[3] = new ImageIcon( "_cuatro.gif" );
      imgCartas[4] = new ImageIcon( "_cinco.gif" );
      imgCartas[5] = new ImageIcon( "_seis.gif" );
      imgCartas[6] = new ImageIcon( "_siete.gif" );
      imgCartas[7] = new ImageIcon( "_diez.gif" );
      imgCartas[8] = new ImageIcon( "_once.gif" );
      imgCartas[9] = new ImageIcon( "_doce.gif" );
    }
    
    public Component getTreeCellRendererComponent( JTree arbol,
      Object valor,boolean seleccionado,boolean expandido,
      boolean rama,int fila,boolean conFoco ) {
      // Hay que encontrar el nodo en que estamos y coger el
      // texto que contiene
      
      DefaultMutableTreeNode nodo = (DefaultMutableTreeNode)valor;
      String texto = (String)nodo.getUserObject();
      this.seleccionado = seleccionado;
      // Se fija el color de fondo en función de que esté o no 
      // seleccionada la celda del árbol
      if( !seleccionado )
	setForeground( Color.black );
      else
	setForeground( Color.white );
      // Fijamos el icono que corresponde al texto de la celda, para
      // presentar la imagen de la carta que corresponde a esa celda
      if( texto.equals( "Palos" ) )
	setIcon( imgBaraja );
      else if( texto.equals( "Copas" ) )
	setIcon( imgPalos[0] );
      else if( texto.equals( "Oros" ) )
	setIcon( imgPalos[1] );
      else if( texto.equals( "Espadas" ) )
	setIcon( imgPalos[2] );
      else if( texto.equals( "Bastos" ) )
	setIcon( imgPalos[3] );
      else if( texto.equals( "As" ) )
	setIcon( imgCartas[0] );
      else if( texto.equals( "Dos" ) )
	setIcon( imgCartas[1] );
      else if( texto.equals( "Tres" ) )
	setIcon( imgCartas[2] );
      else if( texto.equals( "Cuatro" ) )
	setIcon( imgCartas[3] );
      else if( texto.equals( "Cinco" ) )
	setIcon( imgCartas[4] );
      else if( texto.equals( "Seis" ) )
	setIcon( imgCartas[5] );
      else if( texto.equals( "Siete" ) )
	setIcon( imgCartas[6] );
      else if( texto.equals( "Sota" ) )
	setIcon( imgCartas[7] );
      else if( texto.equals( "Caballo" ) )
	setIcon( imgCartas[8] );
      else if( texto.equals( "Rey" ) )
	setIcon( imgCartas[9] );
      
      // A continuación del icono, ponemos el texto
      setText( texto );
      
      return( this);
    }
    
    // Sobreescribimos el método paint() para fijar el color de
    // fondo. Normalmente, un JLabel puede pintar su propio fondo,
    // pero, seguramente debido aparentemente a un bug, o a una
    // limitación en el TreeCellRenderer, es necesario recurrir
    // al método paint() para hacer esto
    public void paint( Graphics g ) {
      Color color;
      Icon currentI = getIcon();
      
      // Fijamos el colos de fondo
      color = seleccionado ? Color.red : Color.white;
      g.setColor( color );
      // Rellenamos el rectángulo que ocupa el texto sobre la
      // celda del árbol
      g.fillRect( 0,0,getWidth()-1,getHeight()-1 );
      
      super.paint( g );
    }
  }

La ejecución del ejemplo, genera una imagen como la que se reproduce a continuación, en donde se ha desplegado una de las ramas del árbol y se ha seleccionado uno de sus elementos.

La clase principal de la aplicación crea un árbol que contiene a cada una de las cartas en uno de los palos de la baraja, y no tiene más misterio. La clase anidada MiRendererDeArbol, es la que implementa el comportamiento que van a tener cada uno de los elementos de las ramas del árbol. Como se puede observar, esta circunstancia permite la inclusión de gráficos o cualquier cambio, como el de la fuente de caracteres, en elementos específicos del árbol. Por ejemplo, puede resultar interesante en una aplicación, el presentar los elementos de un nivel superior en negrilla para resaltar su importancia, o se puede querer añadir una imagen para ser más explicativos. Cualquier cosa de estas, se puede implementar a través de un modelo de datos propio.

En el programa, por ejemplo, los elementos de nivel superior, correspondientes a los palos de la baraja, incluyen la carta que muestra el palo, y a cada una de las cartas individuales, también se les ha añadido el gráfico que las representa. Y en el código, lo único especial es la línea en la cual se le notifica al árbol que dispone de un controlador propio e independiente para la manipulación de las celdas o elementos del árbol.

arbol.setCellRenderer( new MiRendererDeArbol() );

La clase anidada implementa un constructor para la carga de las imágenes, a fin de ahorrar tiempo a la hora de la ejecución. El método getTreeCellRendererComponent(), que debe ser proporcionado a requerimiento de la implementación de la clase TreeCellRenderer utilizada por la clase anidada, es el responsable del dibujo de los elementos del árbol. En este caso, este método determina cuál es la imagen que corresponde al elemento del árbol, basándose en el texto asociado al elemento, y le asigna el icono correspondiente. Además, fija el color de fondo de la celda que contiene al elemento, en función de que esté seleccionado o no.

Con esto último hay un problema. El color de fondo de un elemento del árbol no puede fijarse dentro del método de la clase que implementa el modelo, o por lo menos, si se hace así no tiene efecto alguno. Puede que sea una limitación o un problema de Swing que piensan corregir, pero lo cierto es que esto no ocurre en otros casos, como pueden ser las listas o las tablas. Para evitar el problema, es necesario incluir el método paint() dentro de la clase que implementa el controlador de las celdas.

Navegador

Home | Anterior | Siguiente | Indice | Correo