Hilos y gvSIG (4 de 4): Interactuando con el usuario

En el articulo anterior “Hilos y gvSIG: Estructura de nuestro proceso (3 de 4)” vimos cómo podíamos estructurar la lógica de nuestro proceso. En este ultimo articulo vamos a ver cómo podríamos hacer para interactuar con la interface gráfica desde él.

Cuando estamos trabajando en java con swing y múltiples hilos de ejecución tenemos que tener algo de cuidado al interactuar con el interface de usuario. En una aplicación swing el acceso a los elementos gráficos debe realizarse desde el hilo que controla el manejador de eventos de swing. Si desde otro hilo intentamos acceder a los elementos graficos podemos tener resultados inesperados. La librería de swing aporta varias funciones de utilidad para poder sincronizar nuestras operaciones con las que swing hace en su hilo de ejecución. Nos vamos a quedar con tres de ellas:

  • SwingUtilities.isEventDispatchThread, que nos dice si ya estamos ejecutándonos en el hilo de swing
  • SwingUtilities.invokeLater(Runnable action) que se encarga de encolar la ejecución de “action” en el hilo de swing para que se ejecute cuando este pueda.
  • SwingUtilities.invokeAndWait(Runnable action) que se encarga de encolar la petición en el hilo de swing y esperar a que esta se ejecute.

Usando estos tres métodos podemos asegurarnos que las partes de código que tienen que ver con el GUI se ejecuten en el hilo del manejador de eventos de swing.

Por ejemplo, si quisiésemos mostrar un cuadro de diálogo del estilo de showMessageDialog podríamos hacer algo como:

if (!SwingUtilities.isEventDispatchThread()) {
  try {

    SwingUtilities.invokeAndWait(new Runnable() {
      public void run() {

        JOptionPane.showMessageDialog(null, "Éste sería el mensaje a mostrar");
      }
    });

  } catch (InterruptedException e) {
    logger.info("Can't show message dialog ", e);

  } catch (InvocationTargetException e) {
    logger.info("Can't show message dialog ", e);

  }
} else {
  JOptionPane.showMessageDialog(null, "Éste sería el mensaje a mostrar");

}

Aunque de esta manera podemos asegurarnos que nuestros mensajes se presentan al usuario de forma correcta aunque estemos en un hilo distinto al del manejador de eventos de swing, suele resultar bastante engorroso, por eso disponemos de algúnas funciones en gvSIG para acceder a algunos de los diálogos standard. Los siguientes métodos de gvSIG nos permiten presentar algunos de los cuadros de diálogo de la clase JOptionPane sin preocuparnos de esto:

  • ApplicationManager.messageDialog nos muestra un mensaje similar a JOptionPane.showMessageDialog
  • ApplicationManager.confirmDialog nos muestra un mensaje similar a JOptionPane.showConfirmDialog
  • ApplicationManager.inputDialog nos muestra un mensaje similar a JOptionPane.showInputDialog

Además de estos métodos disponemos de un método ApplicationManager.message que nos permitirá presentar un mensaje en la barra de estado de gvSIG, y que también realiza las comprobaciones adecuadas para asegurarse que funciona aunque no estemos en el hilo de swing.

Con estos cuatro métodos junto con el mecanismo del TaskStatus podemos dotar a nuestros procesos de algo de interacción con el usuario sin necesidad de encargarnos nosotros mismos de sincronizarnos con el hilo de swing. Si precisamos una interacción más rica, por ejemplo presentar nuestros propios cuadros de diálogo desde nuestro hilo de ejecución, disponemos de dos métodos más en el ApplicationManager:

  • createComponent, que pasándole la clase que implementa un componente de AWT se encarga de crearlo en el hilo de swing.
  • Y showDialog(final Component contents, final String title), que recibe nuestro JPanel, normalmente instanciado con createComponent y se encarga de mostrarlo a modo de diálogo modal respecto a nuestro hilo.

Y por último comentar que se está realizando un esfuerzo para que la gran mayoría de los métodos del API de andami y el plugin de gvSIG se comporten de forma segura respecto a su ejecución desde otros hilos distintos al del controlador de eventos de swing.

Bueno… con estas utilidades en nuestra caja de herramientas volvamos a nuestra clase MyProcess. En el articulo anterior habíamos visto cómo podría ser la estructura de nuestra clase que implementase nuestro proceso. Habíamos visto también ya que en algún momento se presentaba un cuadro de diálogo al usuario para informar de algún error, pero no nos habíamos centrado en ello.

Así por ejemplo, al final de nuestro bucle en el método run teníamos algo como:

...
  this.postPrecess();
  if (status.isCancellationRequested()) {

    status.cancel();
    return;
  }
  application.message("Process terminated", JOptionPane.INFORMATION_MESSAGE);

} catch (Exception e) {
  logger.info("Error in process", e);

  if (status != null) {
    status.abort();

  }
  application.message("Process error", JOptionPane.WARNING_MESSAGE);
  application.messageDialog(

          "Se ha producido un error realizando el proceso y éste terminará de forma inesperada.\n\n"
              + e.getMessage(), this.getName(),
          JOptionPane.WARNING_MESSAGE);

} finally {

...

En donde podemos ver cómo se invoca a un par de métodos que interactúan con el usuario. Por un lado tenemos la llamada:

application.message("Process terminated", JOptionPane.INFORMATION_MESSAGE);

Que se encarga de mostrar en la barra de estado de gvSIG un mensaje informándonos que ha terminado la ejecución de nuestro proceso. Nótese que como estamos usando el método message del ApplicationManager no es preciso sincronizar su ejecución con el hilo de swing, como hemos comentado antes.

Y un poco más abajo, cuando se atrapan los errores, se muestra un diálogo al usuario informando de ello:

application.messageDialog(
        "Se ha producido un error realizando el proceso y éste terminará de forma inesperada.\n\n"

            + e.getMessage(), this.getName(),
        JOptionPane.WARNING_MESSAGE);

Con todo esto, vamos a ver qué podía hacer nuestro proceso, más para ilustrar el uso de estas funciones que para hacer algo real. Así nuestro proceso se limitará ha hacer un sleep a cada iteración, y además:

  1. Cuando lleve recorridos el 10% de los elementos presentará un mensaje de tipo informativo en la barra de estado informando de ello, además de mostrarlo en el indicador de progreso usando el taskstatus:
    status.message("10%");
    
    application.message("this is the row " + i,
        JOptionPane.INFORMATION_MESSAGE);
  2. Cuando lleve recorridos el 20% de los elementos presentará un mensaje de tipo advertencia en la barra de estado informando de ello, además de mostrarlo en el indicador de progreso usando el taskstatus:
    status.message("20%");
    application.message("this is the row " + i,
    
        JOptionPane.WARNING_MESSAGE);
  3. Cuando lleve recorridos el 30% de los elementos presentara un mensaje de tipo error en la barra de estado informando de ello, además de mostrarlo en el indicador de progreso usando el taskstatus:
    status.message("30%");
    
    application.message("this is the row " + i,
        JOptionPane.ERROR_MESSAGE);
  4. Cuando lleve el 40% presentara un mensaje del tipo ShowMessageDialoginformando de ello:
    status.message("40%");
    application.messageDialog("Just procesing "+i+" items",
    
        this.getName(), JOptionPane.INFORMATION_MESSAGE);

    Aquí el proceso se interrumpirá hasta que el usuario pulse en el botón aceptar, tras lo cual continuara con su ejecución.

  5. Cuando lleve el 50% informará de ello al usuario, y preguntará si éste desea interrumpir nuestro proceso:
    status.message("50%");
    int confirm = application
        .confirmDialog("Quiere cancelar el procesp?",
    
            this.getName(), JOptionPane.YES_NO_OPTION,
            JOptionPane.QUESTION_MESSAGE);
    
    if (confirm == JOptionPane.YES_OPTION) {
      status.cancelRequest();
    
    }

    En caso de que el usuario responda que si quiere cancelar la ejecución, llamamos al método cancelRequest y ya lo procesaremos a la siguiente entrada en nuestro bucle.

  6. Por ultimo, cuando el proceso lleve el 60%, pediremos al usuario que entre una cadena, que luego mostraremos en la barra de estado:
    status.message("60%");
    
    String value = application.inputDialog("Introduce un valor",this.getName());
    if (value != null) {
    
      application.message("Entered: " + value,
          JOptionPane.INFORMATION_MESSAGE);
    
    } else {
      application.message("Input cancelled",
          JOptionPane.INFORMATION_MESSAGE);
    
    }

Pongamos todo esto junto y veamos como nos quedaría nuestro método processItem:

private void processItem(int i) throws InterruptedException {

  ApplicationManager application = ApplicationLocator.getManager();
  SimpleTaskStatus status = (SimpleTaskStatus) this.getTaskStatus();

  switch ((100 * i) / this.maxValue) {

  case 10:
    status.message("10%");
    application.message("this is the row " + i,

        JOptionPane.INFORMATION_MESSAGE);
    break;
  case 20:
    status.message("20%");

    application.message("this is the row " + i,
        JOptionPane.WARNING_MESSAGE);

    break;
  case 30:
    status.message("30%");

    application.message("this is the row " + i,
        JOptionPane.ERROR_MESSAGE);

    break;
  case 40:
    status.message("40%");

    application.messageDialog("Just procesing "+i+" items",
        this.getName(), JOptionPane.INFORMATION_MESSAGE);

    break;
  case 50:
    status.message("50%");

    int confirm = application
        .confirmDialog("Quiere cancelar el procesp?",
            this.getName(), JOptionPane.YES_NO_OPTION,

            JOptionPane.QUESTION_MESSAGE);
    if (confirm == JOptionPane.YES_OPTION) {

      status.cancelRequest();
    }
    break;
  case 60:

    status.message("60%");
    String value = application.inputDialog("Introduce un valor",this.getName());

    if (value != null) {
      application.message("Entered: " + value,

          JOptionPane.INFORMATION_MESSAGE);
    } else {
      application.message("Input cancelled",

          JOptionPane.INFORMATION_MESSAGE);
    }
    break;
  }
  Thread.sleep(this.sleepTime);

}

No se trata de un código real, solo sirve para ilustrar el uso de las funciones más simples a nuestra disposición para interactuar con el usuario. Soy consciente que el control de los porcentajes puede provocar que se muestre algún diálogo más de una vez, pero prefiero no complicar el control de eso y dejar el código de la forma más simple, ya que lo importante creo que es ilustrar cómo podemos invocar a nuestros diálogos de forma simple.

Veamos ahora como podríamos añadir a nuestro proceso algún cuadro de diálogo personalizado. Supongamos que al iniciar nuestro proceso deseamos presentar al usuario un cuadro de diálogo preguntando algunos datos, y por la razón que sea ya no nos encontramos en el hilo del controlador de eventos de swing. Supondremos que tenemos una clase MyDialog que implementa un panel de swing, JPanel, y queremos mostrarla y esperar a que el usuario introduzca algún dato para que pueda continuar nuestro proceso. Nuestro panel MyDialog, espera recibir en su constructor una instancia de MyProcess, para acceder a los parámetros que éste tiene y poder mostrarlos al usuario y luego recuperarlos.

Lo primero que haremos sera añadir a nuestro MyProcess unos setters y getters para acceder a las propiedades privadas que nos interese:

public int getMaxValue() {
  return maxValue;
}

public void setMaxValue(int maxValue) {
  this.maxValue = maxValue;

}
public long getSleepTime() {
  return sleepTime;

}
public void setSleepTime(long sleepTime) {
  this.sleepTime = sleepTime;

}

Luego para mostrar nuestro diálogo, primero crearemos nuestro panel:

Component panel = application.createComponent(MyDialog.class, this);

Notar que no hemos creado el panel directamente invocando a:

Component panel = new MyDialog(this);

Ya que es podría provocar algún efecto extraño al no estar en el hilo de swing. LLamando a createComponent nos aseguramos que la creación de nuestro componente se hará en el hilo de swing, y que nuestro hilo esperara a que éste sea creado y nos lo devolverá.

Y luego para mostrar un cuadro de diálogo con ese panel llamaremos a:

application.showDialog(panel, this.getName());

Que se encargará de presentar una ventana de gvSIG con el panel indicado, y con el título que le hayamos dicho, en este caso el nombre de nuestro hilo, y esperará a que ésta sea cerrada para devolver el control a nuestro hilo.

Todo esto junto en el método preProcess quedaría:

private void preProcess() {
  SimpleTaskStatus status = (SimpleTaskStatus) this.getTaskStatus();

  status.setTitle("Getting parameters");
  Component panel = application.createComponent(MyDialog.class, this);

  application.showDialog(panel, this.getName());
}

En cuanto al método postProcess que hemos comentado en el articulo anterior, en nuestro caso quedaría vacío:

private void postPrecess() {
  // In this example do nothing
}

Con esto más o menos quedan cubiertas las principales acciones que podemos querer hacer desde nuestro hilo y qué funciones aporta gvSIG para ayudarnos en ello.

Respecto a nuestra clase MyDialog podría ser algo como:

public class MyDialog extends MyDialogLayout {
  private static final long serialVersionUID = 1919826881234125305L;

  private MyProcess process = null;

  public MyDialog(MyProcess process) {

    super();
    this.process = process;
    this.inputSleepTime.setValue(this.process.getSleepTime());

    this.inputMaxIterations.setValue(this.process.getMaxValue());
    this.buttonAcept.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent arg0) {
        doAcept();
      }

    });
    this.buttonCancel.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent arg0) {
        doCancel();
      }

    });
  }
  private void doAcept() {
    this.process.setSleepTime(((SpinnerNumberModel) inputSleepTime.getModel()).getNumber().intValue());

    this.process.setMaxValue(((SpinnerNumberModel) inputMaxIterations.getModel()).getNumber().intValue());

    this.closeWindow();
  }
  private void doCancel() {

    this.process.cancelRequest();
    this.closeWindow();
  }

  private void closeWindow() {
    this.setVisible(false);

  }
}

Lo más importante es el método closeWindow. El método showDialog del ApplicationManager espera a que el componente pasado sea ocultado para interpretar que se quiere cerrar la ventana, así que desde nuestro panel, lo único que hacemos es poner visible a false para indicar que queremos cerrar la ventana. Al respecto de MyDialog comentar que suelo separar la creación del GUI de la lógica de éste, así que la clase MyDialog extiende a MyDialogLayout que contiene la creación y posicionamiento de todo los controles, que son usados desde MyDialog. No creo que tenga gran misterio la creación y posicionamiento de los controles, así que no la voy a incluir en este articulo.

Como nota final, me gustaría hacer hincapié en un idea. Aunque aquí haya comentado cómo podemos hacer para desde nuestro proceso interactuar con el usuario, es una mala política hacerlo. No recomiendo hacerlo más allá de interactuar con el componente TaskStatus para ir informando del progreso de nuestro proceso. Solo en caso de que sea necesario hacerlo deberíamos interactuar con el GUI desde un proceso que se espera que esté haciendo algún tipo de tarea en segundo plano.

Espero que haya servido para ilustrar cómo estructurar este tipo de tareas en gvSIG, y qué recursos tenemos a nuestro alcance para facilitarnos hacerlas.

Un saludo

Joaquin

About Joaquin del Cerro

Development and software arquitecture manager at gvSIG Team. gvSIG Association
This entry was posted in development, gvSIG Desktop, spanish and tagged . Bookmark the permalink.

One Response to Hilos y gvSIG (4 de 4): Interactuando con el usuario

  1. Pingback: Hilos y gvSIG (3 de 4): Estructura de nuestro proceso | gvSIG blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s