Implementando el interface Runnable

prev.gif (997 bytes)chapter.gif (1105 bytes)home.gif (1232 bytes)next.gif (1211 bytes)

Subprocesos (threads)

El interface Runnable

La vida del subproceso

Fechas y horas

Dibujando el reloj

Eliminando el parpadeo

El double-buffer

El código fuente


En esta página vamos a estudiar como se crea un reloj analógico y se muestra en la ventana del applet.

disco.gif (1035 bytes)reloj: RelojApplet.java

 

El interface Runnable

El interface Runnable solamente declara una función miembro denominada run, que han de definir las clases que implementen este interface.

public interface Runnable {
    public abstract void run();
}

En nuestro caso, la clase que describe el applet RelojApplet deriva de Applet por lo que no puede derivar también de Thread (no existe la herencia múltiple en Java), sino que tiene que implementar el interface Runnable y definir la función run.

 

La vida del subproceso

Además, es necesario crear un subproceso, es decir, un objeto de la clase Thread. Creamos este objeto en la redefinición de la función start miembro de la clase que describe el applet. La función miembro start se llama después de init cuando la página que contiene el applet aparece en el navegador. Se crea el objeto hilo de la clase Thread, desde dicho objeto se llama a su función miembro start para poner en marcha el subproceso.

public class RelojApplet extends Applet implements Runnable {
    Thread hilo;
//...
  public void start(){
     if(hilo==null){
        hilo=new Thread(this);
        hilo.start();
     }
  }
}

Al constructor de Thread solamente le podemos pasar this, si la clase que describe el applet implementa el interface Runnable.

A continuación de la llamada a la función start miembro de Thread, se ejecuta la función miembro run, que consta de un bucle indefinido while. En este bucle se pueden realizar distintas tareas: mover una figura, cambiar la imagen que se muestra en una animación, actualizar los gráficos del contexto del applet, realizar un cálculo intensivo, etc. Además, no nos debemos de olvidar de establecer una pausa llamando a la función sleep. Esta función siempre tiene que llamarse dentro de un bloque try..catch

   public void run() {
    while (true) {
        try{
            Thread.sleep(100);
        }catch (InterruptedException e) { }
       //tareas a realizar...
    }
  }

Paramos el subproceso cuando abandonamos la página que contiene el applet, se llama entonces a la función stop miembro de la clase que describe el applet. En dicha función miembro, se para el subproceso hilo, desde este objeto se llama a la función stop miembro de la clase Thread. Por último, se asigna a hilo el valor null.

  public void stop(){
     if(hilo!=null){
        hilo.stop();
        hilo=null;
     }
  }

Existe una alternativa para salir del bucle sin fin while y llegar al final de la función run haciendo que el subproceso alcance el estado Death (muerto) sin llamar explícitamente a la función stop miembro de la clase Thread. Para ello, se obtiene el subproceso actual mediante la función estática currentThread de la clase Thread y se guarda en el objeto miHilo. Naturalmente, subproceso hilo y miHilo serán los mismos. Cuando se llama a la función stop (se abandona la página que contiene el applet) se fuerza a que hilo tome el valor nulo (null). La condición en while se deja de cumplir, llegándose al final del método run, que hace que el subproceso muera de muerte natural.

public void run() {
    Thread miHilo=Thread.currentThread();
    while(miHilo==hilo) {
        try{
            Thread.sleep(100);
        }catch (InterruptedException e) { }
       //tareas a realizar...
    }
 }

  public void stop(){
       hilo=null;
  }

 

Fechas y horas

La versión Java 1.1 ha cambiado completamente el modo en el que se obtienen la fecha y la hora, a fin de cumplir uno de sus objetivos, que el lenguaje se adapte a través de la Internacionalización a los usos y costumbres de los distintos países. La descripción de las nuevas clases Calendar, Date, y DateFormat no se harán en esta sección y se remite al lector al sistema de ayuda de JBuilder para una descripción más completa.

El código que nos permite obtener la fecha (día, mes y año) es el siguiente

    Calendar cal=Calendar.getInstance();
    Date date=cal.getTime();
    DateFormat dateFormatter=DateFormat.getDateInstance(DateFormat. FULL, Locale.getDefault());
    String fecha=dateFormatter.format(date); 

Usamos la clase FontMetrics nos proporciona las características de la fuente de texto, para mostrar la fecha en la parte superior del applet, en su contexto gráfico g.

    FontMetrics fm=g.getFontMetrics();
    g.setColor(Color.yellow);
    g.fillRect(0, 0, anchoApplet, 3*fm.getHeight()/2);
    g.setColor(Color.black);
    g.drawString(fecha, (anchoApplet-fm.stringWidth(fecha))/2, fm.getHeight());

El código que nos permite obtener la hora, los minutos y los segundos es el siguiente

    SimpleDateFormat formatter = new SimpleDateFormat("s" ,Locale.getDefault());
    int segundos=Integer.parseInt(formatter.format(date));
    formatter.applyPattern("m");
    int minutos=Integer.parseInt(formatter.format(date));
    formatter.applyPattern("h");
    int hora=Integer.parseInt(formatter.format(date));

La clase SimpleDateFormat se usa para trasladar la hora en milisegundos a una versión adecuada para el usuario dependiendo del los usos en el país elegido (locale). La clase Locale contiene un método denominado getDefault, que devuelve el locale por defecto del entorno en el que trabajamos.

 

Dibujando el reloj

Para dibujar el reloj se siguen los siguentes pasos

Se dibuja la circunferencia, los números 3, 6, 9, y 12 y las marcas situadas en las horas. Para centrar los números se hace uso del las características de la fuente de texto descritas por el objeto fm de la clase FontMetrics.

    g.setFont(new Font("TimesRoman", Font.BOLD, 14));
    g.setColor(Color.lightGray);
    g.fillOval (xCentro-radio, yCentro-radio, 2*radio, 2*radio);
    g.setColor(Color.black);
    g.drawString("9",xCentro-radio+3, yCentro-fm.getHeight()/2+fm.getAscent());
    g.drawString("3",xCentro+radio-fm.stringWidth("3")-3,yCentro-fm.getHeight()/2+fm.getAscent());
    g.drawString("12",xCentro-fm.stringWidth("12")/2, yCentro-radio+fm.getHeight()+3);
    g.drawString("6",xCentro-fm.stringWidth("6")/2,yCentro+radio-fm.getAscent());
    g.setColor(Color.red);
    for(int i=0; i<12; i++){
        int xHora=xCentro+(int)(Math.cos(i*Math.PI/6)*(7*radio/9));
        int yHora=yCentro+(int)(Math.sin(i*Math.PI/6)*(7*radio/9));
        g.fillOval(xHora-2, yHora-2, 4, 4);
    }

Se calculan empleando algo de trigonometría, las coordenadas de las posiciones de los extremos de las agujas del reloj. Se ha tenido en cuenta el avance de la aguja de la hora a medida que transcurren los minutos desde la posición 0.

La longitud de las agujas se han tomado en la siguiente proporción: la de los segundos tiene una longitud igual al radio de la circunferencia, la de los minutos los 8/9 de este radio, y la de las horas los 6/9 del radio de la circunferencia.

    int xSeg=xCentro+(int)(Math.cos(segundos*Math.PI/30-Math.PI/2)*radio);
    int ySeg=yCentro+(int)(Math.sin(segundos*Math.PI/30-Math.PI/2)*radio);
    int xMin=xCentro+(int)(Math.cos(minutos*Math.PI/30-Math.PI/2)*(8*radio/9));
    int yMin=yCentro+(int)(Math.sin(minutos*Math.PI/30-Math.PI/2)*(8*radio/9));
    int xHora=xCentro+(int)(Math.cos((hora+(double)minutos/60)*Math.PI/6-Math.PI/2)*(6*radio/9));
    int yHora=yCentro+(int)(Math.sin((hora+(double)minutos/60)*Math.PI/6-Math.PI/2)*(6*radio/9));

Se dibujan de distintos colores las agujas como líneas que van desde el centro de la circunferencia hasta las posiciones de sus respectivos extremos. Se han hecho algo más gruesas las agujas de las horas y de los minutos, trazando dos líneas cercanas en vez de una.

    g.setColor(Color.darkGray);
    g.drawLine(xCentro, yCentro, xSeg, ySeg);
    g.setColor(Color.blue);
    g.drawLine(xCentro, yCentro-1, xMin, yMin);
    g.drawLine(xCentro-1, yCentro, xMin, yMin);
    g.drawLine(xCentro, yCentro-1, xHora, yHora);
    g.drawLine(xCentro-1, yCentro, xHora, yHora);

 

Eliminando el parpadeo

Nuestra primera intención es la de escribir un código como el siguiente

   public void run() {
    while (true) {
        try{
            Thread.sleep(100);
        }catch (InterruptedException e) { }
        repaint();
    }
  }
  public void paint(Garphics g){
	dibujaReloj(g);
  }

La función repaint llama a update de la clase base si no está definida en la clase derivada, y update llama a paint de la clase derivada para dibujar el reloj a intervalos regulares de tiempo. El efecto que se consigue es un molesto parpadeo.

La solución más simple para eliminar el parpadeo es la de redefinir la función update en la clase que describe el applet. La definición de update en la clase base Applet la encontramos en su clase Component (Applet es una clase derivada de Panel, ésta lo es de Container, y a su vez, ésta lo es de Component)

    public void update(Graphics g) {
	g.setColor(getBackground());
	g.fillRect(0, 0, width, height);
	g.setColor(getForeground());
	paint(g);
    }

Lo primero que hace update es borrar la ventana y luego, llama al método paint. La parte del código que borra la ventana es la que causa el parpadeo. Por lo tanto, hemos de redefinir update en la clase que describe el applet. Si dibujamos en la ventana sin borrarla previamente eliminamos el parpadeo.

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

Esta tampoco es la solución a este problema, ya que no se borra el fondo y las imágenes aparecen superpuestas

 

El double-buffer

El double-buffer es la solución a muchos de los problemas asociados con la animación. En vez de dibujar directamente en la ventana del applet dibujamos en un buffer intermedio (contexto gráfico en memoria). Cuando es el momento de actualizar la animación lo que hacemos es volcar lo dibujado desde el contexto en memoria a la ventana del applet en una simple y muy rápida operación de transferencia. Luego, volvemos a dibujar en el contexto gráfico en memoria, lo volcamos a la ventana, y así sucesivamente.

public void update(Graphics g){
    if(gBuffer==null){
        imag=createImage(anchoApplet, altoApplet);
        gBuffer=imag.getGraphics();
    }
    gBuffer.setColor(getBackground());
    gBuffer.fillRect(0,0, anchoApplet, altoApplet);
//dibuja el reloj
    dibujaReloj(gBuffer);
//transfiere la imagen al contexto gráfico del applet
    g.drawImage(imag, 0, 0, null);
 }

Como vemos en el código, es muy importante que el contexto gráfico en memoria tenga las dimensiones de la ventana del applet. Como estamos trabajando en un contexto en memoria no hay que preocuparse por los efectos de borrarlo antes de dibujar sobre dicho contexto. De hecho, es el primer paso que hay que hacer cuando empleamos esta técnica conocida por el nombre de double-buffer.

Una vez borrado gBuffer, se dibuja sobre dicho contexto el reloj. Finalmente, se transfiere la imagen creada imag desde la memoria al contexto gráfico g de la ventana del applet, mediante la función drawImage. Fijarse que el método paint no se llama ahora desde update.

Como ya se ha advertido repetidamente en otros ejemplos. Cuando se obtiene el contexto gráfico mediante getGraphics, es responsabilidad del programador, liberar los recursos asociados a dicho objeto mediante la llamada dispose. Dado que update se llama muchas veces a lo largo de un proceso de animación corremos el riesgo de agotar la memoria del ordenador.

  public void update(Graphics g){
     Image imag=createImage(anchoApplet, altoApplet);
     Graphics gBuffer=imag.getGraphics();
//...
  }

Este problema se puede afrontar obteniendo el contexto gráfico la primera vez que se llama a update y guardándolo en el miembro dato gBuffer

  public void update(Graphics g){
     if(gBuffer==null){
          imag=createImage(anchoApplet, altoApplet);
          gBuffer=imag.getGraphics();
     }
//...
}

El código fuente

import java.awt.*;
import java.applet.*;
import java.util.*;
import java.text.*;

public class RelojApplet extends Applet implements Runnable {
   Thread hilo = null;
   int anchoApplet, altoApplet;
//Doble buffer
     Image imag;
     Graphics gBuffer;

  public void init() {
    try {
    jbInit();
    }
    catch (Exception e) {
    e.printStackTrace();
    }
  }
  private void jbInit() throws Exception {
    this.setBackground(Color.white);
    anchoApplet=getSize().width;
    altoApplet=getSize().height;
  }

  public void start() {
    if(hilo == null) {
        hilo = new Thread(this);
        hilo.start();
    }
  }

  public void stop() {
    hilo.stop();
    hilo = null;
  }

  public void run() {
    while (true) {
        try{
            Thread.sleep(100);
        }catch (InterruptedException e) { }
        repaint();
    }
  }

public void dibujaReloj (Graphics g) {
//fecha
    Calendar cal=Calendar.getInstance();
    Date date=cal.getTime();
    DateFormat dateFormatter=DateFormat.getDateInstance(DateFormat. FULL, Locale.getDefault());
    String fecha=dateFormatter.format(date);
    FontMetrics fm=g.getFontMetrics();
    g.setColor(Color.yellow);
    g.fillRect(0, 0, anchoApplet, 3*fm.getHeight()/2);
    g.setColor(Color.black);
    g.drawString(fecha, (anchoApplet-fm.stringWidth(fecha))/2, fm.getHeight());

//hora, minutos y segundos
    SimpleDateFormat formatter = new SimpleDateFormat("s" ,Locale.getDefault());
    int segundos=Integer.parseInt(formatter.format(date));
    formatter.applyPattern("m");
    int minutos=Integer.parseInt(formatter.format(date));
    formatter.applyPattern("h");
    int hora=Integer.parseInt(formatter.format(date));

//el centro del reloj y el radio
    int xCentro=getSize().width/2;
    int yCentro=2*fm.getHeight()+(altoApplet-2*fm.getHeight())/2;
    int radio=(xCentro>(altoApplet-2*fm.getHeight())/2)?(altoApplet-2*fm.getHeight())/2:xCentro;

//Dibujar la circunferencia, los números y las marcas
    g.setFont(new Font("TimesRoman", Font.BOLD, 14));
    g.setColor(Color.lightGray);
    g.fillOval (xCentro-radio, yCentro-radio, 2*radio, 2*radio);
    g.setColor(Color.black);
    g.drawString("9",xCentro-radio+3, yCentro-fm.getHeight()/2+fm.getAscent());
    g.drawString("3",xCentro+radio-fm.stringWidth("3")-3,yCentro-fm.getHeight()/2+fm.getAscent());
    g.drawString("12",xCentro-fm.stringWidth("12")/2, yCentro-radio+fm.getHeight()+3);
    g.drawString("6",xCentro-fm.stringWidth("6")/2,yCentro+radio-fm.getAscent());
    g.setColor(Color.red);
    for(int i=0; i<12; i++){
        int xHora=xCentro+(int)(Math.cos(i*Math.PI/6)*(7*radio/9));
        int yHora=yCentro+(int)(Math.sin(i*Math.PI/6)*(7*radio/9));
        g.fillOval(xHora-2, yHora-2, 4, 4);
    }

//posición de las agujas del reloj
    int xSeg=xCentro+(int)(Math.cos(segundos*Math.PI/30-Math.PI/2)*radio);
    int ySeg=yCentro+(int)(Math.sin(segundos*Math.PI/30-Math.PI/2)*radio);
    int xMin=xCentro+(int)(Math.cos(minutos*Math.PI/30-Math.PI/2)*(8*radio/9));
    int yMin=yCentro+(int)(Math.sin(minutos*Math.PI/30-Math.PI/2)*(8*radio/9));
    int xHora=xCentro+(int)(Math.cos((hora+(double)minutos/60)*Math.PI/6-Math.PI/2)*(6*radio/9));
    int yHora=yCentro+(int)(Math.sin((hora+(double)minutos/60)*Math.PI/6-Math.PI/2)*(6*radio/9));
//las agujas del reloj
    g.setColor(Color.darkGray);
    g.drawLine(xCentro, yCentro, xSeg, ySeg);
    g.setColor(Color.blue);
    g.drawLine(xCentro, yCentro-1, xMin, yMin);
    g.drawLine(xCentro-1, yCentro, xMin, yMin);
    g.drawLine(xCentro, yCentro-1, xHora, yHora);
    g.drawLine(xCentro-1, yCentro, xHora, yHora);
  }

  public void update(Graphics g){
    if(gBuffer==null){
        imag=createImage(anchoApplet, altoApplet);
        gBuffer=imag.getGraphics();
    }
    gBuffer.setColor(getBackground());
    gBuffer.fillRect(0,0, anchoApplet, altoApplet);
//dibuja el reloj
    dibujaReloj(gBuffer);
//transfiere la imagen al contexto gráfico del canvas
    g.drawImage(imag, 0, 0, null);
 }
}