Ejemplo sencillo con SwingWorker

De ChuWiki

El hilo de despachar eventos[editar]

En java hay un hilo especial EDT (Event Dispatch Thread) que es el encargado de tratar todos los eventos de teclado y ratón, además de pintar las ventanas. Java nos aconseja además que cualquier cosa que hagamos sobre ventanas lo hagamos en este hilo. Por todo ello, debemos tener en cuenta dos cosas:

  • Si una tarea va a tardar mucho, no podemos hacerla en el hilo de eventos, puesto que mientras se está ejecutando esa tarea, no se atenderían los eventos de teclado ni de ratón, tampoco se repintarían las ventanas. La sensación para el usuario es que mientras se ejecuta esa tarea, la interface gráfica de usuario se queda bloqueada.
  • Debemos pasarnos al hilo de eventos para hacer cualquier cosa sobre una ventana (cambiar el contenido de un JLabel, cambiar los datos de un JTable, etc).


Por qué SwingWorker[editar]

Podemos hacer todo esto a mano, usando la clase Thread para lanzar un hilo cada vez que vayamos a hacer algo que consuma tiempo (conexiones y consultas a bases de datos, leer ficheros grandes, etc). Y podemos usar SwingUtilities.invokeLater() para hacer que un label.setText() o un tableModel.setValue() se ejecute en el hilo de eventos.

Sin embargo, desde java 6 tenemos disponible la clase SwingWorker que tiene en cuenta todo esto. Esta clase presupone que tenemos que realizar una tarea que tarda mucho, que podemos querer pintar una barra de progreso mientras se realiza esta tarea y que al final querremos presentar los resultados de esa tarea en alguna ventana. Ella se encarga de ir cambiándonos de hilo según haga falta.

Vamos a ver en este tutorial un par de ejemplos sencillos con SwingWorker, uno sin barra de progreso y otro con ella. Aquí puedes ver los fuentes completos de estos ejemplos.


Empezamos con SwingWorker[editar]

Antes de nada, ten en cuenta que SwingWorker está disponible a partir de java 6, por lo que si tienes una versión anterior, no lo tendrás disponible.

Vemos en la API de java, es que SwingWorker es un "genérico" en el que hay que definir dos tipos T y V

Class SwingWorker<T,V>

SwingWorker supone que vamos a realizar una tarea pesada que devuelve un resultado. El tipo T debe ser el tipo de ese resultado. Podemos poner Void (con mayúscula) si la tarea no devuelve ningún resultado. El tipo V es el que se usará para la barra de progreso en el caso de que la haya. Por ejemplo, si nuestra barra de progreso representa un porcentaje de avance, V puede ser Integer y usaremos números de 0 a 100. Si lo que pretendemos es mostrar una etiqueta en la que se irán mostrando textos indicando qué parte de la tarea se está ejecutando, entonces V puede ser String.

Para nuestro primer ejemplo, supongamos que nuestra tarea pesada devuelve un Double y no vamos a poner barra de progreso. Heredamos de SwingWorker así

public class Worker1 extends SwingWorker<Double, Void> {
   ...
}

Debemos ahora sobreescribir dos métodos de la clase padre. El primero es doInBackground(), que es en el que se realizará nuestra tarea pesada. El segundo es done(), que es en el que se debe mostrar el resultado en pantalla. SwingWorker se encargará automáticamente de que doInBackground() y done() se ejecuten en los hilos adecuados, el primero en un hilo propio para él, el segundo en el hilo de eventos.

Para este ejemplo, doInBackground() simplemente contará de 0 a 9 esperando un segundo en cada incremento. Al final devolverá un Double cualquiera (100.0 en este ejemplo). Lo hacemos así para no complicar el ejemplo con unas cuentas, pero en un caso real la tarea será más compleja y el resultado devuelto será resultado de esa tarea.

@Override
protected Double doInBackground() throws Exception {
   // Mostramos el nombre del hilo, para ver que efectivamente
   // esto NO se ejecuta en el hilo de eventos.
   System.out.println("doInBackground() esta en el hilo "
      + Thread.currentThread().getName());

   // Un simble bucle hasta 10, con esperas de un segundo entre medias.
   for (int i = 0; i < 10; i++) {
      try {
         Thread.sleep(1000);
      } catch (InterruptedException e) {
         System.out.println("interrumpido");
      }
   }
   // El supuesto resultado de la operación.
   return 100.0;
}

Y para el done() supongamos que vamos a poner en un JLabel la palabra "Hecho". En un caso real se mostraría un resultado, se actualizaría un TableModel o cualquier otra cosa que queramos hacer sobre una ventana.

public class Worker1 extends SwingWorker<Double, Void> {
   // Esta etiqueta se recibe en el constructor o a través de un
   // metodo setEtiqueta().
   private JLabel etiqueta;
   ...
   @Override
   protected void done() {
      // Mostramos el nombre del hilo para ver que efectivamente esto
      // se ejecuta en el hilo de eventos.
      System.out.println("done() esta en el hilo "
         + Thread.currentThread().getName());
      etiqueta.setText("hecho");
   }
}

Una vez preparado todo, sólo nos queda instanciar y llamar al método execute() del SwingWorker.

Worker1 worker = new Worker1(...);
woker.execute();

La llamada a worker.execute() se encargará de lanzar los hilos y devuelve el control inmediatamente, por lo que nuestro programa podría seguir haciendo otras cosas. Si necesitamos el resultado, podemos llamar a

Double resultado = worker.get();

pero esta llamada sí se queda bloqueada hasta que el SwingWorker termine sus tareas, cosa lógica por otro lado, ya que en un caso normal el resultado no estará disponible hasta que la tarea termine.

Aquí tienes el código completo de Worker1.java y en Principal1.java un pequeño main() para lanzarlo todo.

Poniendo una barra de progreso[editar]

Si queremos poner una barra de progreso, nuestra tarea pesada deberá ir avisando sobre cómo va. La barra de progreso, al ser un componente Swing (una ventana), se debe actualizar en el hilo de eventos. Para facilitar todo esto, incluido el cambio de un hilo a otro, SwingWorker nos proporciona un método publish() al que podemos llamar desde nuestra tarea pesada (desde el método doInBackground()) y ese método se encarga de cambiarnos de hilo al de eventos y llamar al método process(), que debemo sobreescribir para actualizar la barra de progreso.

Vamos a ver todo esto con un poco de calma. Supongamos que queremos una barra de progreso que va de 0 a 100 (un porcentaje del progreso). El valor que usaremos para actualizar la barra de progreso será un entero de 0 a 100, así que al heredar de SwingWorker, el segundo tipo al que antes pusimos Void, le ponemos ahora Integer

public class Worker2 extends SwingWorker<Double, Integer> {
   ...
}

El método publish() admite tantos parámetros de ese tipo (Integer en este ejemplo) como queramos. Para nuestro ejemplo, bastará con pasar un Integer que indique el progreso que llevamos. Como es un bucle de 0 a 99, en cada iteración llamamos a publish() pasando el valor del índice más 1.

    @Override
    protected Double doInBackground() throws Exception {
        System.out.println("doInBackground() esta en el hilo "
                + Thread.currentThread().getName());
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                System.out.println("interrumpido");
            }

            // Se pasa valor para la barra de progreso. ESto llamara al metodo
            // process() en el hilo de despacho de eventos.
            publish(i + 1);
        }

        // Supuesto resultado de la tarea que tarda mucho.
        return 100.0;
    }

Respecto al ejemplo anterior, hemos cambiado el bucle para que sea de 100 pasos en vez de 10, haciendo así más directa la llamada a publish() ya que el valor de i coincidiría con el del porcentaje realizado. Ponemos publish(i+1) puesto que los valores del bucle van de 0 a 99 y queremos que llegue a 100 en el último valor.

Bien, nuestra tarea pesada ya está publicando de alguna forma por donde va. Este publish() cambiará de hilo y llamará al método process(), así que ahora debemos sobreescribir el método process() para que actualice nuestra barra de estado.

public class Worker2 extends SwingWorker<Double, Integer> {
    // Esta JProgressBar la recibiremos en el constructor o en
    // un parametro setProgreso()
    private JProgressBar progreso;

    @Override
    protected void process(List<Integer> chunks) {
        System.out.println("process() esta en el hilo "
                + Thread.currentThread().getName());
        progreso.setValue(chunks.get(0));
    }
}

La lista de enteros que recibimos como parámetro son los enteros que nos pasen con publish(). En este caso sólo nos pasan un entero que es directamente el porcentaje, por lo que sólo nos interesa el de la posición cero de la lista (que es el único que hay). Esto se encargará de actualizar nuestra barra de progreso en el hilo adecuado.

Al igual que en el caso anterior, basta con instanciar el Worker y ejecutarlo

Worker2 worker = new Worker2(...);
worker.execute();

Sin embargo, como ahora tenemos barra de progreso, debemos hacerla visible antes de arrancar el worker, debemos esperar que el worker termine y debemos ocultar la barra. Para la espera debemos usar el método get()

Worker2 worker = new Worker2(...);
dialogoProgreso.setVisible(true);
worker.execute();

// espera a que termine
worker.get();

dialogoProgreso.setVisible(false);

NOTA: Posiblemente lo correcto sea cerrar el dialogo de progreso en el método done() de SwingWorker, ya que del diálogo de progreso es una ventana y debe actualizarse en el hilo de despacho de eventos (EDT), que es en el que se ejecuta done().

En Worker2.java puedes ver el ejemplo completo y en Principal2.java un pequeño main() para arrancarlo todo.

Una pequeña advertencia[editar]

La API de java indica que un SwingWorker está pensado para ser ejecutado una sola vez, por lo que no deberíamos llamar a execute() más de una vez. Si necesitamos que esa tarea se ejecute varias veces, deberíamos intanciar el worker varias veces.