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); |
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:
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:
|
Para eventos de acción (pulsar un Button , por ejemplo) |
|
Cuando un elemento (Checkbox, Choice , etc), cambia su estado |
|
Indican una acción sobre el teclado: pulsar una tecla, soltarla, etc. |
|
Indican una acción con el ratón que no implique movimiento del mismo: hacer click, presionar un botón, soltarlo, entrar / salir... |
|
Indican una acción con el ratón relacionada con su movimiento: moverlo por una zona determinada, o arrastrar el ratón. |
|
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
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.
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:
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. |
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:
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).
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:
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:
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.
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.
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.
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.
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
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); } }
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
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.
Otros métodos útiles del contexto gráfico son:
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.
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.
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.
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.
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.
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(); }
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.
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:
Figura 4. Relleno de las figuras
Figura 5. Tipos de lápìces
Figura 6. Composición de figuras
Figura 7. Efecto aliasing
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
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.
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.