3. Interfaz Gráfica

3.1. AWT

3.1.1. Introducción a AWT

AWT (Abstract Windows Toolkit) es la parte de Java que se emplea para construir interfaces gráficas de usuario. Este paquete ha estado presente desde la primera versión (la 1.0), aunque con la 1.1 sufrió un cambio notable. En la versión 1.2 se incorporó también a Java una librería adicional, Swing, que enriquece a AWT en la construcción de aplicaciones gráficas.
 

Controles de AWT

Java proporciona una serie de controles que podremos colocar en las aplicaciones visuales que implementemos. Dichos controles son subclases de la clase Component, y forman parte del paquete java.awt. Las más comunes son:

Figura 1. Estructura de clases de AWT

Los controles sólo se verán si los añadimos sobre un contenedor (un elemento de tipo Container, o cualquiera de sus subtipos). Para ello utilizamos el método add(...) del contenedor para añadir el control. Por ejemplo, si queremos añadir un botón a un Panel:

Button boton = new Button("Pulsame");
Panel panel = new Panel();
...
panel.add(boton);
 
Component La clase padre Component no se puede utilizar directamente. Es una clase abstracta, que proporciona algunos métodos útiles para sus subclases. 
Botones
Para emplear la clase Button, en el constructor simplemente indicamos el texto que queremos que tenga : 
Button boton = new Button("Pulsame");
Etiquetas
Para utilizar Label, el uso es muy similar al botón: se crea el objeto con el texto que queremos darle: 
Label etiq = new Label("Etiqueta");
Areas de dibujo La clase Canvas se emplea para heredar de ella y crear componentes personalizados. 
Accediendo al objeto Graphics de los elementos podremos darle la apariencia que queramos: dibujar líneas, pegar imágenes, etc:
Panel p = new Panel();
p.getGraphics().drawLine(0, 0, 100, 100);
p.getGraphics().drawImage(...);
Casillas de verificación
Checkbox se emplea para marcar o desmarcar opciones. Podremos tener controles aislados, o grupos de Checkboxes en un objeto CheckboxGroup, de forma que sólo una de las casillas del grupo pueda  marcarse cada vez. 
Checkbox cb = new Checkbox
              ("Mostrar subdirectorios", 
              false);
System.out.println ("Esta marcada: " + 
                    cb.getState());
Listas

Para utilizar una lista desplegable (objeto Choice ), se crea el objeto y se añaden, con el método addItem(...) , los elementos que queramos a la lista: 
Choice ch = new Choice();
ch.addItem("Opcion 1");
ch.addItem("Opcion 2");
...
int i = ch.getSelectedIndex();
Para utilizar listas fijas (objeto List), en el constructor indicamos cuántos elementos son visibles. También podemos indicar si se permite seleccionar varios elementos a la vez. Dispone de muchos de los métodos que tiene Choice para añadir y consultar elementos. 
List lst = new List(3, true);
lst.addItem("Opcion 1");
lst.addItem("Opcion 2");
Cuadros de texto
Al trabajar con TextField o TextArea, se indica opcionalmente en el constructor el número de columnas (y filas en el caso de TextArea) que se quieren en el cuadro de texto. 
TextField tf = new TextField(30);
TextArea ta = new TextArea(5, 40);
...
tf.setText("Hola");
ta.appendText("Texto 2");
String texto = ta.getText();
Menús
Para utilizar menús, se emplea la clase MenuBar (para definir la barra de menú), Menu (para definir cada menú), y MenuItem (para cada opción en un menú). Un menú podrá contener a su vez submenús (objetos de tipo Menu ). También está la clase CheckboxMenuItem para definir opciones de menú que son casillas que se marcan o desmarcan. 
MenuBar mb = new MenuBar();
Menu m1 = new Menu "Menu 1");
Menu m11 = new Menu ("Menu 1.1");
Menu m2 = new Menu ("Menu 2");
MenuItem mi1 = new MenuItem ("Item 1.1");
MenuItem mi11=new MenuItem ("Item 1.1.1");
CheckboxMenuItem mi2 = 
   new CheckboxMenuItem("Item 2.1");
mb.add(m1);
mb.add(m2);
m1.add(mi1);
m1.add(m11);
m11.add(mi11);
m2.add(mi2);
Mediante el método setMenuBar(...) de Frame podremos añadir un menú a una ventana:
Frame f = new Frame();
f.setMenuBar(mb);

 

3.1.2. Gestores de disposición

Para colocar los controles Java en los contenedores se hace uso de un determinado gestor de disposición. Dicho gestor indica cómo se colocarán los controles en el contenedor, siguiendo una determinada distribución. Para establecer qué gestor queremos, se emplea el método setLayout(...) del contenedor. Por ejemplo:

Panel panel = new Panel();
panel.setLayout(new BorderLayout());
Veremos ahora los gestores más importantes:
BorderLayout

(gestor por defecto para contenedores tipo Window)
Divide el área del contenedor en 5 zonas: Norte ( NORTH ), Sur (SOUTH), Este (EAST), Oeste (WEST) y Centro (CENTER), de forma que al colocar los componentes deberemos indicar en el método add(...) en qué zona colocarlo: 
panel.setLayout(new BorderLayout());
Button btn = new Button("Pulsame");
panel.add(btn, BorderLayout.SOUTH);
Al colocar un componente en una zona, se colocará sobre el que existiera anteriormente en dicha zona (lo tapa). 
FlowLayout

(gestor por defecto para contenedores de tipo Panel)
Con este gestor, se colocan los componentes en fila, uno detrás de otro, con el tamaño preferido (preferredSize ) que se les haya dado. Si no caben en una fila, se utilizan varias. 
panel.setLayout(new FlowLayout());
panel.add(new Button("Pulsame"));
GridLayout
Este gestor sitúa los componentes en forma de tabla, dividiendo el espacio del contenedor en celdas del mismo tamaño, de forma que el componente ocupa todo el tamaño de la celda. 
Se indica en el constructor el número de filas y de columnas. Luego, al colocarlo, va por orden (rellenando filas de izquierda a derecha). 
panel.setLayout(new GridLayout(2,2));
panel.add(new Button("Pulsame"));
panel.add(new Label("Etiqueta"));
Sin gestor Si especificamos un gestor null, podremos colocar a mano los componentes en el contenedor, con métodos como setBounds(...) , o setLocation(...)
panel.setLayout(null);
Button btn = new Button ("Pulsame");
btn.setBounds(0, 0, 100, 30);
panel.add(btn);

Ejemplo: Vemos el aspecto de algunos componentes de AWT, y el uso de gestores de disposición en este ejemplo:

Código
 
El código nos muestra cómo se crea una clase que es una ventana principal (hereda de Frame), y define un gestor que es un GridLayout, con 4 filas y 2 columnas. En ellas vamos colocando etiquetas (Label), botones (Button), casillas de verificación (Checkbox), listas desplegables (Choice) y cuadros de texto (TextField). Además, se crea un menú con diferentes opciones.

3.1.3. Modelo de Eventos en Java

Hasta ahora hemos visto qué tipos de elementos podemos colocar en una aplicación visual con AWT, y cómo colocarlos sobre los distintos contenedores que nos ofrece la librería. Pero sólo con esto nuestra aplicación no hace nada: no sabemos cómo emitir una determinada respuesta al pulsar un botón, o realizar una acción al seleccionar una opción del menú. Para definir todo esto se utilizan los llamados eventos.

Entendemos por evento una acción o cambio en una aplicación que permite que dicha aplicación produzca una respuesta.  El modelo de eventos de AWT se descompone en dos grupos de elementos: las fuentes y los oyentes de eventos. Las fuentes son los elementos que generan los eventos (un botón, un cuadro de texto, etc), mientras que los oyentes son elementos que están a la espera de que se produzca(n) determinado(s) tipo(s) de evento(s) para emitir determinada(s) respuesta(s).

Para poder gestionar eventos, necesitamos definir el manejador de eventos correspondiente, un elemento que actúe de oyente sobre las fuentes de eventos que necesitemos considerar. Cada tipo de evento tiene asignada una interfaz, de modo que para poder gestionar dicho evento, el manejador deberá implementar la interfaz asociada. Los oyentes más comunes son:

ActionListener
Para eventos de acción (pulsar un Button , por ejemplo)
ItemListener
Cuando un elemento (Checkbox, Choice , etc), cambia su estado
KeyListener
Indican una acción sobre el teclado: pulsar una tecla, soltarla, etc.
MouseListener
Indican una acción con el ratón que no implique movimiento del mismo: hacer click, presionar un botón, soltarlo, entrar / salir...
MouseMotionListener
Indican una acción con el ratón relacionada con su movimiento: moverlo por una zona determinada, o arrastrar el ratón.
WindowListener
Indican el estado de una ventana

Cada uno de estos tipos de evento puede ser producido por diferentes fuentes. Por ejemplo, los ActionListeners pueden producirse al pulsar un botón, elegir una opción de un menú, o pulsar Intro. Los MouseListener se producen al pulsar botones del ratón, etc.

Toda la gestión de eventos se lleva a cabo desde el paquete java.awt.event.

Modos de definir un oyente

Supongamos que queremos realizar una acción determinada al pulsar un botón. En este caso, tenemos que asociar un ActionListener a un objeto Button, e indicar dentro de dicho ActionListener qué queremos hacer al pulsar el botón. Veremos que hay varias formas de hacerlo:

1. Que la propia clase que usa el control implemente el oyente

class MiClase implements ActionListener
{
    public MiClase()
    {
           ...
        Button btn = new Button("Boton");
        btn.addActionListener(this);       
           ...
    }
   
    public void actionPerformed(ActionEvent e)
    {
        // Aqui va el codigo de la accion
    }
}
2. Definir otra clase aparte que implemente el oyente
class MiClase
{
     public MiClase()
     {
        ...
        Button btn = new Button("Boton");
        btn.addActionListener(new MiOyente());
        ...
     }
}

class MiOyente implements ActionListener
{
     public void actionPerformed(ActionEvent e)
     {
         // Aqui va el codigo de la accion
     }
}
3. Definir una instancia interna del oyente
class MiClase
{
     public MiClase()
     {
         ...
         Button btn = new Button("Boton");
         btn.addActionListener(new ActionListener()
         {
            public void actionPerformed(ActionEvent e)
            {
                // Aqui va el codigo de la accion
            }
         });
         ...
    }
}
 

Uso de los "adapters"

Algunos de los oyentes disponibles (como por ejemplo MouseListener, consultad su API) tienen varios métodos que hay que implementar si queremos definir el oyente. Este trabajo puede ser bastante pesado e innecesario si sólo queremos usar algunos métodos. Por ejemplo, si sólo queremos hacer algo al hacer click con el ratón, deberemos redefinir el método mouseClicked, pero deberíamos escribir también los métodos mousePressed, mouseReleased, etc, y dejarlos vacíos.

Una solución a esto es el uso de los adapters. Asociado a cada oyente con más de un método hay una clase ...Adapter (para MouseListener está MouseAdapter , para WindowListener está WindowAdapter, etc). Estas clases implementan las interfaces con las que se asocian, de forma que se tienen los métodos implementados por defecto, y sólo tendremos que sobreescribir los que queramos modificar.

Veamos la diferencia con el caso de MouseListener, suponiendo que queremos asociar un evento de ratón a un Panel para que haga algo al hacer click sobre él.

1. Mediante Listener:

class MiClase 
{
     public MiClase()
     {
        ...
        Panel panel = new Panel();
        panel.addMouseListener(new MouseListener()
        {
           public void mouseClicked(MouseEvent e)
           {
               // Aqui va el codigo de la accion
           }
    
           public void mouseEntered(MouseEvent e)
           {
               // ... No se necesita
           }
   
           public void mouseExited(MouseEvent e)
           {
               // ... No se necesita
           }
    
           public void mousePressed(MouseEvent e)
           {
               // ... No se necesita
           }

           public void mouseReleased(MouseEvent e)
           {
               // ... No se necesita
           }       
        });        
        ...
    }
}
Vemos que hay que definir todos los métodos, aunque muchos queden vacíos porque no se necesitan.

2. Mediante Adapter:

class MiClase 
{
     public MiClase()
     {
         ...
         Panel panel = new Panel();
         panel.addMouseListener(new MouseAdapter()
         {
            public void mouseClicked(MouseEvent e)
            {
                // Aqui va el codigo de la accion
            }       
         });        
         ...
     }
}
Vemos que aquí sólo se añaden los métodos necesarios, el resto ya están implementados en MouseAdapter (o en el adapter que corresponda), y no hace falta ponerlos.

Ejemplo: Vemos el uso de oyentes en este ejemplo: Código

 
La aplicación muestra distintos tipos de eventos que podemos definir sobre una aplicación: 

3.1.4. Pasos generales para construir una aplicación gráfica con AWT

Con todo lo visto hasta ahora, ya deberíamos ser capaces de construir aplicaciones más o menos completas con AWT. Para ello, los pasos a seguir son:

1. Definir la clase principal, que será la ventana principal de la aplicación

Cualquier aplicación AWT debe tener una ventana principal que sea de tipo Frame. Así pues lo primero que debemos hacer es definir qué clase hará de Frame:

import java.awt.*;
import java.awt.event.*;

public class MiAplicacion extends Frame
{
     public MiAplicacion()
     {
         setSize(500, 400);
         setLayout(new GridLayout(1, 1));
         ...
     }
}

podemos definir un constructor, y dentro hacer algunas inicializaciones como el tamaño de la ventana, el gestor de disposición, etc.

2. Colocar los controles en la ventana

Una vez definida la clase, e inicializada la ventana, podemos colocar los componentes en ella:

import java.awt.*;
import java.awt.event.*;

public class MiAplicacion extends Frame
{
     public MiAplicacion()
     {
         setSize(500, 400);
         setLayout(new GridLayout(1, 1));
         
         Button btn = new Button("Hola");
         this.add(btn);

         JPanel p = new JPanel();
         JLabel l = new JLabel("Etiqueta");
         JLabel l2 = new JLabel ("Otra etiqueta");
         p.add(l);
         p.add(l2);
         this.add(p);
     }
}

En nuestro caso añadimos un botón, y un panel con 2 etiquetas.

3. Definir los eventos que sean necesarios

Escribimos el código de los eventos para los controles sobre los que vayamos a actuar:

import java.awt.*;
import java.awt.event.*;

public class MiAplicacion extends Frame
{
     public MiAplicacion()
     {
         setSize(500, 400);
         setLayout(new GridLayout(1, 1));
         
         Button btn = new Button("Hola");
         this.add(btn);

         JPanel p = new JPanel();
         JLabel l = new JLabel("Etiqueta");
         JLabel l2 = new JLabel ("Otra etiqueta");
         p.add(l);
         p.add(l2);
         this.add(p);

         btn.addActionListener(new ActionListener()
         {
             public void actionPerformed(ActionEvent e)
             {
                 System.out.println ("Boton pulsado");
             }
         });

         addWindowListener(new WindowAdapter()
         {
             public void windowClosing (WindowEvent e)
             {
                 System.exit(0);
             }
         });
     }
}


4. Mostrar la ventana

Desde el método main de nuestra clase principal podemos hacer que se muestre la ventana:

import java.awt.*;
import java.awt.event.*;

public class MiAplicacion extends Frame
{
     public MiAplicacion()
     {
         ...
     }

     public static void main(String[] args)
     {
         MiAplicacion ma = new MiAplicacion();
         ma.show();
     }
}

5. Definir otras subventanas o diálogos

Aparte de la clase principal, podemos definir otros Frames en otras clases, e interrelacionarlos. También podemos definir diálogos (Dialogs) que dependan de una ventana principal (Frame) y que se muestren en un momento dado.

 

3.2. Swing

3.2.1. Introducción a Swing

Anteriormente se ha visto una descripción de los controles AWT para construir aplicaciones visuales. En cuanto a estructura, no hay mucha diferencia entre los controles proporcionados por AWT y los proporcionados por Swing: éstos se llaman, en general, igual que aquéllos, salvo que tienen una "J" delante; así, por ejemplo, la clase Button de AWT pasa a llamarse JButton en Swing , y en general la estructura del paquete de Swing (javax.swing) es la misma que la que tiene java.awt.

Pero yendo más allá de la estructura, existen importantes diferencias entre los componentes Swing y los componentes AWT:

3.2.2. Características específicas de Swing

Resumen de controles

Los controles en Swing tienen en general el mismo nombre que los de AWT, con una "J" delante. Así, el botón en Swing es JButton , la etiqueta es JLabel , etc. Hay algunas diferencias, como por ejemplo JComboBox (el equivalente a Choice de AWT), y controles nuevos. Vemos aquí un listado de algunos controles:

JComponent La clase padre para los componentes Swing es JComponent , paralela al Component de AWT. 
Botones
Se tienen botones normales (JButton), de verificación (JCheckBox), de radio (JRadioButton), etc, similares a los Button, Checkbox de AWT, pero con más posibilidades (se pueden añadir imágenes, etc). 
Etiquetas
Las etiquetas son JLabel, paralelas a las Label de AWT pero con más características propias (iconos, etc). 
Cuadros de texto
Las clases JTextField y JTextArea representan los cuadros de texto en Swing, de forma parecida a los TextField y TextArea de AWT.
Listas

Las clases JComboBox y JList se emplean para lo mismo que Choice y List en AWT. 
Diálogos y ventanas
Las clases JDialog (y sus derivadas) y JFrame se emplean para definir diálogos y ventanas. Se tienen algunos cuadros de diálogo específicos, para elegir ficheros (JFileChooser ), para elegir colores (JColorChooser), etc. 
Menús
Con JMenu, JMenuBar, JMenuItem, se construyen los menús que se construian en AWT con Menu, MenuBar y MenuItem.

 

Gestores de disposición y modelo de eventos

Los gestores de disposición de Swing son los mismos que los vistos en AWT. Sólo debemos tener en cuenta que hay ciertos métodos de JFrame a los que no podemos acceder directamente (en Frame sí podemos), y se debe acceder a ellos a través de un método llamado getContentPane. Ejemplos de estos métodos son add y setLayout, que pasan a usarse de la siguiente forma:

public class MiFrame extends JFrame
{
    public MiFrame()
    {
        Button b = new Button("Hola");

        this.add(b);						// ERROR
        this.getContentPane().add(b);				// OK
        this.setLayout(new BorderLayout());			// ERROR
        this.getContentPane().setLayout(new BorderLayout());	// OK
    }
...


El modelo de eventos también es  el mismo que el visto en AWT.

Otras características

Swing ofrece otras posibilidades, que se comentan brevemente:


Ejemplo: Vemos el aspecto de algunos componentes de Swing, paralelo al visto en el tema de AWT:


Código

Observad cómo se pasa una aplicación de AWT a Swing. Hay que cambiar los componentes de AWT por los equivalentes de Swing (Frame por JFrame, Button por JButton, etc), y luego hay algunos métodos a los que no podemos llamar directamente, como son los métodos add y setLayout de JFrame, a los que se debe llamar a través del método getContentPane (IMPORTANTE: esto se aplica única y exclusivamente a ciertos métodos de JFrame, como los indicados).

Ejemplo: Vemos un ejemplo de uso de iconos y temporizadores (como icono se emplea esta imagen): Código

Para utilizar los iconos se utiliza un objeto de tipo ImageIcon y se dice cuál es el fichero de la imagen. Para el temporizador, se utiliza un objeto de tipo Timer. Vemos que se define un ActionListener, que se ejecuta cada X milisegundos (1000, en este caso), ejecutando así un trabajo periódico (mediante el método setRepeats del Timer indicamos que el listener se ejecute periódicamente, o no).

3.3. Applets

Los ejemplos vistos hasta ahora son aplicaciones, puesto que son instancias de la clase Frame o JFrame, y por tanto son ventanas que pueden ejecutarse independientemente.

Un applet es una aplicación normalmente corta (aunque no hay límite de tamaño), cuya principal funcionalidad es ser accesible a un servidor Internet (una aplicación que pueda visualizarse desde un navegador).

La forma de definir un applet es muy similar a la definición de una aplicación, salvo por algunas diferencias:

Un ejemplo básico de applet sería:
public class MiApplet extends Applet
{
    public void init()
    {
        setLayout(new BorderLayout());
        Button b = new Button("Hola");
        add(b, BorderLayout.NORTH);
        ...
    }
}

El appletviewer es un navegador mínimo distribuido con Java, que espera como argumento un fichero HTML, que contendrá una marca indicando el código que cargará el appletviewer . Podemos así cargar un applet que esté en una página HTML con:

appletviewer <fichero HTML>
En la página HTML debemos incluir etiquetas que permitan cargar el applet, como son las etiquetas APPLET u OBJECT:
<HTML>
  <BODY>
  ...
  <APPLET CODE = MiApplet.class WIDTH = 300 HEIGHT = 100>
  </APPLET>
  ...
  </BODY>
</HTML>
Donde se indican el fichero .class compilado del applet, la anchura y altura. Este código se coloca en un fichero HTML y puede verse desde cualquier navegador que soporte Java, o con el programa appletviewer .

La clase Applet tiene unos métodos predefinidos para controlar los applets:

3.3.1. Applets Swing

La única diferencia entre los applets construidos en AWT y los construidos con Swing es que éstos heredan de la clase JApplet en lugar de la clase Applet. Pero se tiene el inconveniente de que actualmente sólo la utilidad appletviewer está preparada para ejecutar applets de Swing con Java 1.2 o posteriores. Para el resto de navegadores deberemos contar con el Java Plug-in 1.1.1, que contiene la versión 1.0.3 de Swing . El resto de la estructura de los applets es la misma que para AWT.

Ejemplo: Vemos el ejemplo anterior convertido en applet. Puede verse aquí el código y aquí la página HTML con el applet.

3.4. Gráficos y animación

Hasta ahora hemos visto la creación de aplicaciones con una interfaz gráfica a partir de una serie de componentes definidos en la API de AWT y de Swing (ventanas, botones, campos de texto, etc).

En este punto veremos como dibujar nuestros propios gráficos directamente en pantalla. Para ello Java nos proporciona acceso al contexto gráfico del área donde vayamos a dibujar, permitiéndonos a través de éste modificar los pixels de este área, dibujar una serie de figuras geométricas, así como volcar imágenes en ella.

3.4.1. Gráficos en AWT

Para dibujar gráficos en un área de la pantalla, AWT nos ofrece un objeto con el contexto gráfico de dicha área, perteneciente a la clase Graphics. Este objeto nos ofrece una serie de métodos que nos permiten dibujar distintos elementos en pantalla. Más adelante veremos con detalle los métodos más importantes.

Este objeto Graphics nos lo deberá proporcionar AWT en el momento en que vayamos a dibujar, ya que no podremos obtenerlo por nuestra cuenta de ninguna otra forma.

Para dibujar en pantalla cualquier componente AWT, estos componentes proporcionan dos métodos: paint(Graphics g) y update(Graphics g).

Estos métodos serán invocados por AWT cuando necesite dibujar su contenido en pantalla. Por ejemplo, cuando la ventana se muestre por primera vez, AWT invocará al método paint(Graphics g) de todos los componentes AWT que contenga la aplicación, de forma que estos se dibujen en pantalla. Cuando una aplicación minimizada se maximice, o una aplicación que estaba total o parcialmente tapada por otra pase a primer plano de nuevo, también podrá ser necesario invocar dicho método para volver a dibujar los componentes de la ventana.

Cada componente AWT definirá su propio método paint(Graphics g) para dibujar su contenido. Por ejemplo, un Button dibujará en pantalla la forma del botón y el texto. Si creamos una subclase de estos componentes y sobrescribimos su método paint(Graphics g) con un método propio, dentro de él podremos utilizar el objeto de contexto gráfico Graphics para dibujar en el área de dicho componente.

La mayoría de los componentes tienen ya definido su propio comportamiento y apariencia, por lo que no deberemos sobrescribir este método ya que el componente dejaría de funcionar correctamente. Sin embargo, hay un componente diseñado para que el usuario pueda utilizarlo como área de dibujo, este es el caso del Canvas. Este componente simplemente comprende un área vacía de la pantalla, en la que nosotros podremos dibujar nuestros propios gráficos sobrescribiendo su método paint(Graphics g). Sobrescribiremos el método de la siguiente forma:

public class MiCanvas extends Canvas {
	public void paint(Graphics g) {
		// Dibujamos en el área del canvas
		// usando el objeto g proporcionado
	}
}

Con esto en la clase MiCanvas hemos creado un componente propio en el que nosotros controlamos lo que se dibuja en pantalla. Podremos añadir este componente a nuestra aplicación de la misma forma que añadimos cualquier otro componente AWT:

MiCanvas mc = new MiCanvas();
panel.add(mc);

Hemos de recordar que no podemos controlar cuando se invoca el método paint(Graphics g), esté método será invocado por AWT en el momento en el que el SO necesite que la ventana sea redibujada. En él simplemente definimos como se dibujará el contenido de nuestro componente en pantalla, y AWT ya se encargará de invocarlo cuando sea necesario. 

3.4.2 Contexto gráfico: Graphics

El objeto Graphics nos permitirá acceder al contexto gráfico de un determinado componente y a través de él dibujar en su área en pantalla. Vamos a ver ahora como dibujar utilizando dicho objeto.

3.4.2.1 Atributos

El contexto gráfico tendrá asociado el color del lápiz que usamos en cada momento para dibujar, así como la fuente de texto que se utilizará en el caso de que dibujemos una cadena de texto. Para consultar o modificar el color o la fuente asociadas al contexto gráfico se proporcionan los siguientes métodos:

Una vez establecidos estos atributos en el contexto gráfico, podremos dibujar en él una serie de elementos utilizando una serie de métodos de Graphics. Estos métodos comienzan por drawXXXX para dibujar el contorno de una determinada forma, o fillXXXX para dibujar dicha forma con relleno.

El sistema de coordenadas del área en pantalla tendrá la coordenada (0,0) en su esquina superior izquierda, y las coordenadas serán positivas hacia la derecha (coordenada x) y hacia abajo (coordenada y), tal como se muestra a continuación:

Figura 1. Coordenadas del área de dibujo

3.4.2.2 Figuras

El contexto gráfico nos ofrece una serie de métodos para dibujar en él las principales primitivas geométricas básicas:

Por ejemplo, el siguiente canvas aparecerá con un dibujo de un círculo rojo y un rectángulo (cuadrado) verde:

public class MiCanvas extends Canvas {
	public void paint(Graphics g) {
		g.setColor(Color.red);
		g.fillOval(10,10,50,50);
		g.setColor(Color.green);
		g.fillRect(60,60,50,50);
	}
}

3.4.2.3 Texto

Aparte de dibujar figuras geométricas también podremos dibujar cadenas de texto. Para ello se proporciona el método drawString(String s, int x, int y) que dibuja la cadena s en las coordenadas (x,y). Este punto corresponderá al inicio de la cadena, en la línea de base del texto como se muestra a continuación:

Figura 2. Línea de base del texto

Con esto dibujaremos un texto en pantalla, pero podemos querer hacer por ejemplo que el texto sea sensible a pulsaciones del ratón sobre él. Lo único que nos proporciona AWT es la posición del ratón dentro del área, por lo que para saber si está sobre el texto tendremos que saber que región ocupa el texto. Aquí es donde encontramos el problema, según la fuente, la cadena escrita, y el contexto donde la escribamos, el texto puede tener distintas dimensiones, y nosotros sólo conocemos las coordenadas de comienzo. La solución a esto la proporciona el objeto FontMetrics, que podemos obtener llamando al método getFontMetrics(Font f) del contexto gráfico o del componente AWT (ambos tipos de objetos contienen este método). Este objeto nos dará información sobre las métricas de dicha fuente en este contexto. De este objeto podemos sacar la siguiente información:

Con estas medidas podremos conocer exactamente los límites de una cadena de texto, tal como se muestra a continuación:

Figura 3. Métricas del texto

3.4.2.4 Imágenes

Por último, un método importante es el que nos permite volcar una imagen al área de dibujo. Las imágenes en Java se encapsulan mediante la clase Image. Podemos o bien crearnos una imagen vacia para dibujar nosotros en ella, o cargar imágenes desde ficheros. Para cargar una imagen de un fichero en caso de un Applet, simplemente deberemos llamar al método getImage(URL url) de la clase Applet que nos devolverá el objeto Image con la imagen cargada, que podrá estar en formato GIF, JPG o PNG. En el caso de una aplicación deberemos seguir los siguientes pasos:

Obtener la implementación del toolkit de AWT a partir de cualquier componente AWT de nuestra aplicación. Si tenemos una ventana con un canvas, tanto la ventana como el canvas nos servirían dado que ambos son componentes AWT. Sobre el componente llamaremos al método getToolkit() para obtener el objeto Toolkit:

Canvas mc = new MiCanvas();
Toolkit toolkit = mc.getToolkit();

También podemos obtener el toolkit por defecto si no podemos acceder a ningún componente AWT:

Toolkit toolkit = Toolkit.getDefaultToolkit();

Utilizar el método createImage(String fichero) del Toolkit para cargar la imagen del fichero de nombre fichero:

Image img = toolkit.createImage("foto.jpg");

Una vez obtenida la imagen podremos dibujarla en el contexto gráfico con drawImage(Image img, int x, int y, ImageObserver obs). Con esto dibujaremos la imagen img en las coordenadas (x,y) del área. Además necesitamos proporcionar el objeto ImageObserver que se utilizará para avisar a la aplicación cuando la imagen esté cargada del todo, y por lo tanto puede mostrarla por pantalla. Cualquier componente AWT capaz de mostrar imagenes implementará la interfaz ImageObserver, por lo que podremos utilizarlo en la llamada a dicho método. Por ejemplo, si dibujamos la imagen en un Canvas, utilizaremos el mismo Canvas como ImageObserver ya que es el componente en el que vamos a observar la imagen:

public class MiCanvas extends Canvas {
	public void paint(Graphics g) {
		Toolkit toolkit = getToolkit();
		Image img = toolkit.createImage("foto.jpg");

		g.drawImage(img, 0, 0, this);
	}
}

Si no necesitamos utilizar un ImageObserver podemos especificar null en este parámetro. Esto será útil cuando trabajemos con imágenes que sabemos que ya están cargadas, y cuando no tengamos la referencia al componente donde se va a dibujar la imagen.

3.4.2.5 Otros métodos

Otros métodos útiles del contexto gráfico son:

3.4.3 Animaciones

Hasta ahora hemos visto como dibujar gráficos en pantalla, pero lo único que hacemos es definir un método que se encargue de dibujar el contenido del componente, y ese método será invocado cuando el sistema necesite dibujar la ventana.

Sin embargo puede interesarnos cambiar dinámicamente los gráficos de nuestro componente. Para ello deberemos indicar el momento en el que queremos que se redibujen los gráficos, ya que el sistema por si solo sólo llamará a paint(Graphics g) cuando sea necesario volver a dibujar la ventana porque su contenido se ha perdido, pero no lo llamará cuando hayamos realizado cambios.

3.4.3.1 Redibujado del área

Para forzar que se redibuje el área del componente, deberemos llamar al método repaint() del componente (del canvas por ejemplo). Con eso estamos solicitando al sistema que se repinte el componente, pero no lo repinta en el mismo momento en el que se llama. El sistema introducirá esta solicitud en la cola de ventanas de debe repintar, y cuando tenga tiempo repintará su contenido.

MiCanvas mc = new MiCanvas();
...
mc.repaint();

En este caso para repintar el componente no llamará a su método paint(Graphics g), sino al método update(Graphics g). Este es el método que se encarga de actualizar los gráficos. Su implementación por defecto consiste en borrar los gráficos actuales del área del componente, y llamar a paint(Graphics g) para pintarlo de nuevo.

Imaginemos que estamos moviendo un rectángulo por pantalla. El rectángulo irá cambiando de posición, y en cada momento lo dibujaremos en la posición en la que se encuentre. Pero si no borramos el contenido de la pantalla en el instante anterior, el rectángulo aparecerá en todos los lugares donde ha estado en instantes anteriores produciendo este efecto indeseable de dejar rastro. Por ello el método update(Graphics g) vacía todo el área del componente antes de invocar a paint(Graphics g) para que dibuje los gráficos en el instante actual.

Ejemplo de efecto flicker

Sin embargo, al estar vaciando y volviendo a dibujar en el componente, veremos en éste un efecto parpadeo (flicker). Por ello, si queremos que nuestra aplicación no muestre este aspecto de aplicación amateur, deberemos sobrescribir el método update(Graphics g) para que su única función sea llamar a paint(Graphics g) sin vaciar previamente el área:

public class MiCanvas {
	public void update(Graphics g) {
		paint(g);
	}

	public void paint(Graphics g) {
		// Aqui dibujamos el contenido del componente
	}
}

Pero ahora nos encontramos con otros problemas. Al no borrar la pantalla los objetos pueden dejar rastro. Además puede que queramos dibujar varios componentes en pantalla, y si los dibujamos uno detrás de otro puede producirse el efecto poco deseable de ver como se va construyendo la imagen. Para evitar que esto ocurra y conseguir unas animaciones limpias, utilizaremos la técnica del doble buffer.

Ejemplo de efecto rastro

3.4.3.2 Técnica del doble buffer

La técnica del doble buffer consiste en dibujar todos los elementos que queremos mostrar en una imagen en memoria, y una vez se ha dibujado todo, volcarlo a pantalla como una unidad. De esta forma, mientras se va dibujando la imagen, como no se hace directamente en pantalla no veremos efectos de parpadeo al borrar el contenido anterior, ni veremos como se va creando la imagen, en pantalla se mostrará la imagen cuando esté completa.

Ejemplo de doble buffer

Para utilizar esta técnica lo primero que deberemos hacer es crearnos el denominado back buffer que será el buffer en memoria donde dibujamos la imagen. Para implementarlo en Java utilizaremos una imagen (objeto Image) que tendremos en memoria, y sobre la que dibujaremos el contenido que queramos mostrar. Deberemos crear una imagen del mismo tamaño del componente en el que vamos a dibujar. Para crear una imagen en blanco podemos usar el método createImage(int width, int height) que se encuentra en cualquier componente AWT (Component), como por ejemplo nuestro canvas, y crea una imagen vacia con las dimensiones especificadas. Tendremos que crearla con las dimensiones del componente:

Image backbuffer = createImage(getWidth(), getHeight());

Al igual que cada componente tiene un contexto gráfico asociado en el que podemos dibujar, una imagen en memoria también tendrá su contexto gráfico. Mientras el contexto gráfico de los componentes hace referencia a la pantalla, el de una imagen hará referencia a un espacio de memoria en el que se almacena la imagen, pero la forma de dibujar en ambos se realiza de la misma forma a través de la misma interfaz (objeto Graphics). Para obtener el contexto gráfico de una imagen utilizaremos el método getGraphics() de la misma:

Graphics offScreen = backbuffer.getGraphics();

Una vez obtenido este contexto gráfico, dibujaremos todo lo que queremos mostrar en él, en lugar de hacerlo en pantalla. Una vez hemos dibujado todo el contenido en este contexto gráfico, deberemos volcar la imagen a pantalla para que ésta se haga visible:

g.drawImage(backbuffer, 0, 0, this);

La imagen conviene crearla una única vez, ya que la animación puede redibujar frecuentemente, y si cada vez que lo hacemos creamos un nuevo objeto imagen estaremos malgastando memoria inutilmente. Es buena práctica de programación en Java instanciar nuevos objetos las mínimas veces posibles, intentando reutilizar los que ya tenemos.

Podemos ver como quedaría nuestra clase ahora:

public MiCanvas extends Canvas {

	// Backbuffer
	Image backbuffer = null;

	// Ancho y alto del backbuffer
	int width, height;

	// Coordenadas del rectangulo dibujado
	int x, y;

	public void update(Graphics g) {
		paint(g);
	}

	public void paint(Graphics g) {
		// Solo creamos la imagen la primera vez 
		// o si el componente ha cambiado de tamaño
		if( backbuffer == null || 
			width != getWidth() || 
			height != getHeight() )
		{
			width = getWidth();
			height = getHeight();
			backbuffer = createImage(width, height);
		}

		Graphics offScreen = backbuffer.getGraphics();

		// Vaciamos el área de dibujo

		offScreen.clearRect(0,0,getWidth(), getHeight());

		// Dibujamos el contenido en offScreen
		offScreen.setColor(Color.red);
		offScreen.fillRect(x,y,50,50);

		// Volcamos el back buffer a pantalla
		g.drawImage(backbuffer,0,0,this);
	}
}

En este ejemplo se dibuja un rectángulo rojo en la posición (x,y) de la pantalla que podrá ser variable, tal como veremos a continuación añadiendo a este ejemplo métodos para realizar la animación.

3.4.3.3 Código para la animación

Si queremos hacer una animación tendremos que ir cambiando ciertas propiedades de los objetos de la imagen (por ejemplo su posición) y solicitar que se redibuje tras cada cambio. El bucle para la animación podría ser el siguiente:

public class MiCanvas extends Canvas {
	...
	public void anima() {
		// El rectangulo comienza en (10,10)
		x = 10;
		y = 10;

		while(x < 100) {
			x++;
			repaint();

			try {
				Thread.currentThread().sleep(100);
			} catch(InterruptedException e) {}
		}
	}
}

Con este código de ejemplo veremos una animación en la que el rectángulo que dibujamos partirá de la posición (10,10) y cada 100ms se moverá un pixel hacia la derecha, hasta llegar a la coordenada (100,10).

Para ello sólo tendremos que invocar el método anima() de nuestro componente. Pero es importante no invocar este método directamente desde cualquier respuesta a un evento, como puede ser la pulsación de un boton:

public void actionPerformed(ActionEvent e) {
	// ¡¡¡ No hay que hacer esto !!!
	mc.anima();
}

¿Por qué no debemos hacer esto? Parece dificil ver que esto pueda causar algún problema, pero debemos pensar que Java tiene un hilo en el que se tratan los eventos que se van produciendo, y este mismo hilo será el que se encargue de repintar la pantalla cuando se lo solicitemos. Sin embargo, si desde este hilo llamamos al método que realiza la animación y éste no devuelve el control hasta que la animación no ha terminado, al no continuar el hilo no podrá repintar el contenido de la pantalla, por mucho que se lo pidamos dentro de nuestra función de animación (la ventana de la aplicación quedaría como "colgada"). Esto producirá el efecto de no ver la animación mientras se está realizando, sólo se actualizará la pantalla una vez haya terminado la animación, por lo tanto se producirá un salto desde el estado inicial hasta el estado final.

Por lo tanto, si queremos hacer una animación lo mejor será crear un hilo independiente que se encargue de realizar dicha animación, y de esta manera el hilo de procesamiento de eventos pueda continuar realizando sus tareas mientras se ejecuta la animación:

public class MiCanvas extends Canvas implements Runnable {
	...
	public void run() {
		anima();
	}
}

Ahora si que podremos invocar la animación desde el código de los eventos creando un hilo independiente que se encargue de ella:

public void actionPerformed(ActionEvent e) {
	// Así si que funciona
	Thread t = new Thread(mc);
	t.start();
}

3.4.3.4 Modo XOR

Si no queremos tener que sobrescribir la pantalla entera cada vez que se actualiza, podemos utilizar otra técnica que se basa en el modo XOR de dibujo para evitar que los objetos dejen rastro al ser modificados.

Lo que haremos en este caso es activar el modo XOR mientras estemos modificando un objeto. Cada vez que realicemos un cambio en el objeto (movimiento, cambio de tamaño o forma), lo que haremos será dibujar de nuevo el objeto en su posición anterior, de forma que al estar activado el modo XOR se borrará el objeto, y a continuación lo dibujamos en su nueva posición. Este modo es muy utilizado en programas de dibujo, mientras estamos dibujando figuras en pantalla.

3.4.4 API de Java 2D

Java 2D es una nueva API introducida a partir de JDK 1.2, que extiende AWT para proporcionar un extenso conjunto de funcionalidades para trabajar con gráficos 2D, texto e imágenes. Aporta una serie de formas primitivas básicas con las que podremos trabajar.

Además de mostrar gráficos por pantalla, es capaz de sacarlos a través de la impresora, utilizando un modelo uniforme de render para ambos casos.

El mecanismo de render es el mismo que vimos en AWT para versiones anteriores de JDK, cuando el sistema necesita redibujar un componente, se invoca a su método paint o update. Sin embargo, lo que se proporciona es un objeto Graphics2D, que extiende a Graphics proporcionando acceso a las nuevas funcionalidades de Java 2D.

Además, todos aquellos componentes de Swing derivados de JComponent implementan internamente el doble buffer, por lo que no tendremos que ocuparnos de hacerlo nosotros. Simplemente deberemos rellenar el código del método paint, borrando el contenido del contexto gráfico que se nos proporciona, si fuese necesario, y dibujando los gráficos. Es importante hacer notar que no todos los componentes de Swing derivan de JComponent, como es el caso de JFrame por ejemplo, por lo que en este caso no implementará internamente el doble buffer. Para saber si un componente deriva de éste, simplemente tendremos que ir a la documentación de la API de Java y en la página de dicho componente veremos toda la jerarquía de sus ascendientes.

Lo que deberemos hacer (siempre que trabajemos con la versión 1.2 de JDK o posteriores) será hacer una conversión cast del objeto Graphics proporcionado a un objeto Graphics2D. Esto es así porque a los métodos paint y update se les estará proporcionando en realidad un objeto Graphics2D, aunque la referencia a él sea de tipo Graphics por cuestión de mantener la compatibilidad con versiones anteriores. Si queremos utilizar las funcionalidades mejoradas que ofrece la nueva API de Java 2D, deberemos obtener el objeto Graphics2D de la siguiente forma:

public void paint(Graphics g) {
	Graphics2D g2 = (Graphics2D)g;
}

Este objeto nos permitirá realizar un mayor número de operaciones en el contexto gráfico pudiendo así obtener de forma sencilla unos gráficos mejorados.

En este nuevo contexto gráfico tendremos los siguiente atributos:

Se proporcionan una seríe de métodos setXXXX y getXXXX para obtener y modificar los atributos anteriores.

Para dibujar figuras podemos utilizar el siguiente método:

g2.draw(Shape figura);

La información de las figuras estará encapsulada en clases derivadas de Shape (podemos encontrar rectángulos, líneas, curvas, etc). Deberemos instanciar la figura adecuada y dibujarla en el contexto gráfico mediante el método anterior.

Figura 8. Tipos de figuras

3.4.5 Modo a pantalla completa

A partir de JDK 1.4.0 podremos utilizar un modo de gráficos a pantalla completa. Con este modo tendremos acceso exclusivo y directo al dispositivo de la pantalla.

En AWT hemos visto anteriormente que era el sistema el que se encargaba de mandar eventos para repintar el contenido de nuestra ventana. Esto tiene que hacerse así ya que el dispositivo gráfico (la pantalla) se comparte con el sistema operativo y el resto de aplicaciones en ejecución, por lo que es el mismo sistema el que nos tiene que indicar cuando podemos redibujar.

Sin embargo, al tener ahora un acceso exclusivo a la pantalla, podremos dibujar en ella en cualquier momento. Esto es lo que se conoce como render activo, frente al render pasivo que hemos visto anteriormente.

Para utilizar este modo exclusivo a pantalla completa lo primero que deberemos hacer es obtener el dispositivo gráfico (GraphicsDevice) que vamos a utilizar. Lo obtendremos a partir del entorno gráfico (GraphicsEnvironment) de nuestra máquina:

GraphicsEnvironment ge = 
	GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gd = ge.getDefaultScreenDevice();

Podemos tener varios dispositivos gráficos en nuestra máquina, que podrán ser obtenidos también a través del entorno gráfico. Con el método visto anteriormente obtendremos el dispositivo primario que es el que querremos utilizar normalmente, ya que lo normal será disponer de un solo dispositivo gráfico.

Una vez tenemos el dispositivo gráfico podemos comprobar si soporta el modo a pantalla completa con el siguiente método (dentro de GraphicsDevice):

boolean b =  gd.isFullScreenSupported()

Una vez hemos comprobado que soporta dicho modo podremos pasar a pantalla completa indicando que la ventana (Window) de nuestra aplicación vamos a mostrar en la pantalla. Para ello utilizamos el siguiente método (dentro de GraphicsDevice):

gd.setFullScreenWindow(Window w)

Del objeto de dispositivo gráfico podremos obtener la lista de modos soportados por dicho dispositivo (resolución y profundidad de color), y seleccionar cualquiera de estos modos siempre que la operación esté permitida en nuestro sistema. Obtenemos el modo gráfico actual con:

DisplayMode dm = gd.getDisplayMode();

Para obtener todos los modos soportados haremos lo siguiente:

DisplayMode [] dms = gd.getDisplayModes();

Los datos sobre los modos gráficos se encapsulan en la clase DisplayMode. Una vez elegido el modo que queremos utilizar podemos seleccionarlo con:

gd.setDisplayMode(dm);

Antes hemos hablado del concepto de render activo. Para utilizar este tipo de render tendremos que obtener el objeto de contexto gráfico asociado al componente donde vamos a dibujar (la ventana en este caso):

Graphics g = getGraphics();

Una vez hemos terminado de dibujar en la pantalla, debemos llamar a dispose para liberar los recursos que estuviese utilizando.

g.dispose();

Podemos usar tanto el modo activo como el modo pasivo cuando trabajemos a imagen completa. Sin embargo el modo activo nos dará un mayor control y podremos utilizar técnicas avanzada como el intercambio (flipping) de páginas de memoria de video.

Antes hemos visto como implementar el doble buffer. Pero ahora que tenemos acceso directo al dispositivo gráfico, podremos usar una técnica más rápida para implementarlo. Existen dos técnicas:

BLT: Leido como blit (blitting), que significa BLock Transfer. Es una técnica similar a la que hemos descrito anteriormente. El backbuffer es un área en memoria, y cuando queremos volcar este contenido a pantalla copiaremos el contenido de dicho área a la memoria del dispositivo gráfico.

FLIP: Se refiere al intercambio (flipping) de páginas de memoria de video. Normalmente la tarjeta gráfica tendrá varias páginas de memoria (doble buffer o triple buffer normalmente). En un momento dado el dispositivo gráfico estará mostrando el contenido de una de estas páginas. Lo que haremos será utilizar la página que no se esté mostrando como backbuffer. Una vez hayamos terminado de dibujar, haremos que esa página pase a ser el buffer de pantalla mostrándose así su contenido sin tener que hacer ninguna transferencia de datos, y la otra página pasará a ser el backbuffer donde dibujaremos a continuación. Esta técnica, puesto que la página contiene todo el contenido de la pantalla, no podremos utilizarla si estamos compartiendo la pantalla con otras aplicaciones, ya que afectariamos a todas ellas. Es por esto que esta técnica solo podemos utilizarla al trabajar a pantalla completa.

Con el modo a pantalla completa podremos utilizar cualquiera de las dos técnicas anteriores. Para ello tenemos las clases BufferStrategies y BufferCapabilities que implementan dichas técnicas.

EJEMPLO

La siguiente clase es un ejemplo sencillo de cómo establecer la pantalla completa y dibujar animaciones en ella. 

El constructor de la clase principal se encarga de obtener el dispositivo gráfico (GraphicsDevice), elegir el modo gráfico (DisplayMode), y luego pasar a modo pantalla completa con el modo gráfico seleccionado. Después hay un hilo (campo t) que es el que se encarga de hacer las animaciones.

Para las animaciones, definimos un método update que sólo llame a paint, y un método paint que vaya dibujando cada frame de la animación, pero con un doble buffer. El método dibuja se encarga de dibujar en el backbuffer, y luego en paint se vuelca ese contenido directamente en pantalla. La animación en sí (método dibuja) consiste en mover un círculo rojo desde una X inicial de 10 hasta una final de 200.

Veréis también una clase interna llamada DlgModos que se encarga de elegir el modo gráfico (DisplayMode) de entre una lista de posibles modos. Simplemente muestra un cuadro de diálogo para que el usuario elija el modo que quiera. Luego, el modo elegido es asignado al dispositivo gráfico (GraphicsDevice).

Este otro ejemplo es un juego completo hecho utilizando estas técnicas. Fue desarrollado por Miguel Angel Lozano, profesor de este departamento, y es una variante del juego Pang que antiguamente se solía ver en las máquinas recreativas. Podéis probarlo ejecutando la clase Panj.

3.4.6 Sonido y música. Java Sound

Hemos visto como incluir gráficos y animaciones en nuestras aplicaciones y applets. Con Java además podremos añadir sonido y música de forma sencilla.

En las primeras versiones de JDK los applets permitían cargar clips de audio, con lo que podiamos reproducir música y sonido en la web. Los tipos de ficheros y formatos reconocidos entonces eran bastante limitados (MIDI, WAV, AU).

A partir de Java 2, se incorpora la API de Java Sound, que nos permite además de tratar con una mayor número de formatos, incorporar sonido tanto a applets como aplicaciones y trabajar con la reproducción del sonido a un nivel más bajo, lo cual nos dará mayor flexibilidad para incluir todo tipo de efectos de sonido en nuestras aplicaciones.

Nos referiremos a las músicas y a los efectos de sonido como clips de audio. Estos clips de audio estarán encapsulados en la clase AudioClip. Para cargar un clip de audio, los applets siempre han incorporado el siguiente método:

AudioClip clip = getAudioClip(url);

Para ello deberemos estar dentro un applet, ya que dicho método pertenece a la clase Applet y para usarlo debe haber un objeto Applet instanciado.

Si estamos dentro de una aplicación, no tendremos acceso a este método. Por ello, a partir de JDK 1.2 se incorpora a la clase Applet el siguiente método estático:

AudioClip clip = Applet.newAudioClip(url);

Al ser estático no hará falta haber instanciado un applet para poder usarlo, por lo que lo podremos utilizar desde cualquier aplicación Java. El problema que tenemos en este caso es que debemos proporcionar una URL, cuando lo más seguro es que queramos cargar un fichero del disco local. Podemos obtener una url para esto de la siguiente forma:

URL url = new URL("file:" + System.getProperty("user.dir") 
                          + "/sonido.wav";

Utilizamos la propiedad del sistema user.dir para obtener el directorio actual, y con él podemos construir un URL para el acceso a ficheros locales.

Una vez obtenido el clip de audio, podremos reproducirlo.

clip.play();

Reproduce el clip una sola vez.

clip.loop();

Reproduce el clip ciclicamente. Util en el caso de que queramos tener una música de fondo que no pare de sonar.

clip.stop();

Detiene la reproducción del clip de audio. Sobretodo útil para músicas, y cuando hayamos establecido que se reproduzca ciclicamente.

En la API Java Sound incorporada en las últimas versiones de Java, tenemos bastante más clases que nos permitirán controlar la secuenciación, el mezclado, etc. Estas clases se incluyen en el paquete javax.sound y subpaquetes.