Programación Avanzada

Interfaces gráficas de usuario

Swing: El modelo de programación

Introducción

El modelo de programación de la respuesta a eventos en Swing, sigue el patrón de diseño Observador/Observable.

Los componentes Swing son Observables, cuando el usuario interacciona sobre ellos, todos los Observadores son informados.

Swing utiliza la aproximación push, en el momento de informar a los Observadores, se les envía una descripción de lo ocurrido.

Para completar el patrón, es posible registrar y eliminar escuchadores a los componentes.

Bibliografía

Contenidos

  1. El modelo de eventos de Swing.
  2. Cómo responder a los eventos.
  3. Un poco más de Java: clases internas.
  4. Resumen.

El modelo de eventos de Swing

En el capítulo dedicado a la programación dirigida por eventos vimos, de modo muy sucinto, el ciclo de interacción con los componentes Swing:

El modelo de eventos de Swing

La práctica totalidad de los componentes Swing generan eventos cuando el usuario interacciona con ellos.

Los eventos son instancias de clases definidas en el paquete estándar de Java.

Los contenedores también generan eventos, pero no nos ocuparemos de ellos.

Si queremos escuchar un determinado tipo de eventos, debemos definir una clase especializada para ello.

El modelo de eventos de Swing

La descripción por pasos:

  1. Un componente genera un evento -objeto-
  2. El componente envía el evento a todos los escuchadores registrados.
  3. Un escuchador es un objeto de una clase que implementa una interface concreta.

Cómo responder a los eventos

Concretémoslo en un caso particular:

  1. Componente → Botón -JButton-
  2. Evento → ActionEvent
  3. Escuchadora → ... implements ActionListener {
  4. Registro → addActionListener(Escuchadora);

Cómo responder a los eventos

¿Cómo podemos conocer los eventos que lanzan los distintos componentes?

Buscando en esta tabla.

Como puedes ver, distintos componentes pueden generar el mismo tipo de evento.

Fíjate en el nombrado, si el nombre del interface es xxxListener el evento que escucha es xxxEvent, y el método de registro es addxxxListener(xxxListener): ActionListenerActionEventaddActionListener(ActionListener escuchadora).

Cómo responder a los eventos

Escribamos nuestra clase escuchadora:

public class EscuchadoraBoton implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Pulsaste el botón.");
    }
}

Como ves, esta interface sólo declara un método actionPerformed(ActionEvent e) que será llamado cada vez que el usuario pulse el botón.

Cómo responder a los eventos

Registremos una instancia de esta clase como escuchador:

JFrame ventana = new JFrame("Escuchador botón");
JButton boton = new JButton("Púlsame...");

boton.addActionListener(new EscuchadoraBoton());//Registro escuchador

ventana.getContentPane().add(boton);
ventana.setSize(500, 500);
ventana.setVisible(true);

Cómo responder a los eventos

Ahora, cada vez que pulsemos sobre el botón:

Obtendremos un mensaje por consola.

Cómo responder a los eventos

Como has visto, es sólo una técnica. Nuestro trabajo como programadores consiste es unir todas las piezas, y sobre todo, escribir el código de respuesta a los eventos que nos interesan.

En el ejemplo anterior el código de respuesta era, simplemente, mostrar un mensaje por consola.

Cómo responder a los eventos

Resumamos, de nuevo, el procedimiento:

  1. Decide qué componente, o contenedor, quieres utilizar en la interface gráfica.
  2. Busca los eventos y escuchadores que se le pueden registrar.
  3. De entre todos, elige el evento, o los eventos, que te interese.
  4. Escribe una clase que implemente los escuchadores que has elegido -recuerda, esto implica que tu clase implemente algunas interface-
  5. Tu código de respuesta lo escribirás al definir los métodos que declara la interface.
  6. Registra una instancia de tu clase escuchadora sobre el componente.

Cómo responder a los eventos

¿Recuerdas que la ventana no se cerraba al pulsar el botón con el aspa?

Vamos a programar que se cierre.

1. Decide qué componente, o contenedor, quieres utilizar en la interface gráfica.

Nuestro contenedor, en este caso, es JFrame.

2. Busca los eventos y escuchadores que se le pueden registrar.

Cómo responder a los eventos

Cómo responder a los eventos

3. De entre todos, elige el, o los, que te interese.

En este caso sólo tenemos uno (WindowListener). Leemos la documentación y confirmamos que es lo que necesitamos.

Cómo responder a los eventos

4. Escribe una clase que implemente los escuchadores que has elegido -recuerda, esto implica que tu clase implemente algunas interface-

public class EscuchadorVentana implements WindowListener {
    @Override
    public void windowActivated(WindowEvent e) {    }
    @Override
    public void windowClosed(WindowEvent e) {    }
    @Override
    public void windowClosing(WindowEvent e) {    }
    @Override
    public void windowDeactivated(WindowEvent e) {    }
    @Override
    public void windowDeiconified(WindowEvent e) {    }
    @Override
    public void windowIconified(WindowEvent e) {    }
    @Override
    public void windowOpened(WindowEvent e) {    }
}

Cómo responder a los eventos

5. Tu código de respuesta lo escribirás al definir los métodos que declara la interface.

@Override
public void windowClosing(WindowEvent e) {
    System.exit(0);
}

Cómo responder a los eventos

6. Registra una instancia de tu clase escuchadora sobre el componente.

public class VentanaQueSeCierra {
    ...
    private void ejecuta() {
        JFrame ventana = new JFrame("Ventana que se cierra");
        ventana.addWindowListener(new EscuchadorVentana());
        ...
    }
    ...
}

Cómo responder a los eventos

Te habrás dado cuenta que el interface WindowListener declara 7 métodos, y sólo en 1 hemos escrito código, el resto los hemos dejado vacios.

Este modo de implementar un escuchador es tedioso.

Afortunadamente, en estos casos, el paquete estandar de Java nos ofrece un atajo: las clases adaptadoras.

Las clases adaptadoras implementan interfaces, que declaran un gran número de métodos, dejando la definición de los métodos vacía.

Ahora, en vez de hacer que nuestra clase escuchadora implements una interface, haremos que extends la clase adaptadora.

Cómo responder a los eventos

public class EscuchadorVentanaAdaptado extends WindowAdapter {
    @Override
    public void windowClosing(WindowEvent e) {
        System.exit(0);
    }
}

En este caso, el código es mucho más legible, sólo muestra detalles de los métodos que nos importan.

Fíjate, de nuevo, en el nombrado de la clase adaptadora: si la interface es xxxListener la clase adaptadora será xxxAdapter. En nuestro caso interface WindowListener la clase adaptadora class WindowAdapter.

Cómo responder a los eventos

Cómo responder a los eventos

Hemos visto dos técnicas distintas para conseguir un mismo fin: que nuestra aplicación se cierre al cerrar la ventana.

Existe una tercera técnica, muy sencilla, que consiste en utilizar un método de la clase JFrame:

ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

Acabar la ejecución de la aplicación al cerrar la ventana principal es algo tan habitual, que la clase JFrame nos ofrece un método para conseguirlo.

Cómo responder a los eventos

Resumiendo, las tres técnicas que hemos visto para que nuestra aplicación acabe, cuando cerramos la ventana principal son:

  1. Implementar WindowListener.
  2. Extender WindowAdapter.
  3. Usar el método setDefaultCloseOperation(...) de la clase JFrame.

Cómo responder a los eventos

¿Cuál utilizar en cada caso? La guía es:

  • Si no vas a hacer nada especial antes de cerrar la aplicación -cerrar ficheros, sockets, conexiones a Internet- usa el método setDefaultCloseOperation(...).
  • Si la clase escuchadora ya extiende a otra, no te queda más remedio que implementar WindowListener.
  • Si no tienes la restricción anterior, extiende WindowAdapter.

Un poco más de Java: clases internas

Antes de pasar a ver algunos de los componentes Swing, veamos un par de técnicas más para definir clases escuchadoras.

Hasta ahora, las clases escuchadoras las hemos definido como clases de primer nivel, es decir, siguiendo el esquema de definición de cualquier clase: creamos un fichero donde escribimos la definición de la clase.

No obstante, la sintaxis de Java nos proporciona un par más de opciones para definir clases:

  1. Clases internas.
  2. Clases internas anónimas.

Un poco más de Java: clases internas

Recuerda que la encapsulación es uno de los pilares de la POO.

Los objetos encapsulan datos y comportamiento. Y no todos los datos, o no todo el comportamiento, es totalmente visible.

Como sabes, podemos utilizar los modificadores de acceso, para restringir la visibilidad tanto de los atributos como de los métodos de una clase.

Si un método contiene detalles de implementación que no queremos hacer visibles a otras clases, lo podemos modificar como private, de modo que sólo la clase donde está definido pueda utilizarlo.

Un poco más de Java: clases internas

Lo mismo ocurre con los atributos.

Puede que algún atributo de una clase esté definido sólo por necesidad de la implementación, no porque sea una característica de los objetos.

Igual que en el caso de los métodos, podemos modificar esos atributos como private.

Un poco más de Java: clases internas

Lo mismo ocurre con las clases, muchas veces con las clases escuchadoras.

Las clases escuchadoras sólo son de interés a las clases que necesitan utilizarlas para atender a las acciones del usuario, pero son transparentes al resto de clases, es decir, nunca las van a utilizar.

¿Me proporciona Java algún mecanismo para poder encapsular la definición de una clase dentro de otra?

La respuesta es sí: las clases internas.

Un poco más de Java: clases internas

Volvamos al ejemplo de la ventana que se cierra. Encapsulemos la definición de la clase escuchadora.

public class VentanaQueSeCierra {
    ...
    private void ejecuta() {
        JFrame ventana = new JFrame("Ventana que se cierra");
        ventana.addWindowListener(new Escuchador());
        ventana.setSize(300, 300);
        ventana.setVisible(true);
    }
    private class Escuchador extends WindowAdapter {
        @Override
        public void windowClosing(WindowEvent e) {
            System.exit(0);
        }
    }
    ...
}

Un poco más de Java: clases internas

public class VentanaQueSeCierra {
    ...
    private class Escuchador extends WindowAdapter {
        @Override
        public void windowClosing(WindowEvent e) {
            System.exit(0);
        }
    }
    ...
}

Una clase dentro de otra, y la podemos modificar como private para que no se vea desde fuera, lo único que es visible desde fuera es su efecto: acabar la ejecución de la aplicación.

Un poco más de Java: clases internas

Una propiedad muy interesante -y cómoda- de las clases internas es que tienen acceso a todos los miembros -atributos y métodos- de la clase que las contiene.

La clase contenedora ahora no tiene que ofrecer servicios que sólo vaya a utilizar una clase escuchadora interna, y por lo tanto, los puede modificar como private para que no sean visibles desde fuera de la clase.

Un poco más de Java: clases internas

¿Cómo nombra el compilador de Java a las clases internas?

Sabemos que el compilador genera un fichero con extensión .class por cada fichero de definición de clase pública con extensión .java.

Usemos la vista Navigator de Eclipse:

El nombre de la clase interna está formado por el nombre de la clase que la contiene, el símbolo de $ y el nombre de la clase interna.

Un poco más de Java: clases internas

Vayamos un poco más allá. Fíjate en este trozo de código:

ventana.addWindowListener(new WindowListener() {
    @Override
    public void windowOpened(WindowEvent e) { }
    @Override
    public void windowIconified(WindowEvent e) { }
    @Override
    public void windowDeiconified(WindowEvent e) { }
    @Override
    public void windowDeactivated(WindowEvent e) { }
    @Override
    public void windowClosing(WindowEvent e) { System.exit(0); }
    @Override
    public void windowClosed(WindowEvent e) { }
    @Override
    public void windowActivated(WindowEvent e) { }
});

Un poco más de Java: clases internas

¿Observas algo extraño en la siguiente línea de código?

ventana.addWindowListener(new WindowListener() {
...

Una ayuda, WindowListener es un interface.

Los interface en Java no se pueden instanciar.

¿Qué está ocurriendo?

Veamos la vista Navigator:

Un poco más de Java: clases internas

Pues no sé si me ayuda, ¿qué significa ese nombre de clase VentanaQueSeCierra$1?

¿De donde sale esa clase?

Un poco más de Java: clases internas

Volvamos al código que generó esa clase:

ventana.addWindowListener(new WindowListener() {
...

Lo que está cocurriendo es que estamos definiendo una clase que iplementa WindowListener Y NO TIENE NOMBRE.

Estamos definiendo una clase interna anónima

  1. Interna porque la estamos definiendo dentro de otra.
  2. Anónima porque no tiene nombre.
  3. Y estamos creando una instancia de ella en el momento de la definición, de ahí el new.

Un poco más de Java: clases internas

A las clases internas anónimas, el compilador las nombre como: nombre de la clase que la contiene + $ + ordinal.

El ordinal será: 1, 2, 3,... según el orden y número de la clase dentro de la que está contenida.

Un poco más de Java: clases internas

Pues este trozo de código también te va a gustar:

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

¿Qué estamos haciendo?

Pues lo mismo que en el caso anterior, creando una clase interna anónima, pero esta vez esa clase está extendiendo a la clase WindowAdapter y sobreescribe su método windowClosing(WindowEvent e).

Un poco más de Java: clases internas

Podemos crear clases internas anónimas que implementen algún interface o que extienda a otra clase.

Si podemos usar los dos, ¿por cual nos decidimos?

La guía esta vez es, si tienes una clase adaptadora, aprovéchala. Si no tienes clase adaptadora, bueno, utiliza el interface.

Un poco más de Java: clases internas

Para generar las vistas Navigator no he incluido el siguiente trozo de código:

SwingUtilities.invokeLater(new Runnable() {
    @Override
    public void run() {
        new VentanaQueSeCierra().ejecuta();
    }
});

Si lo incluyo, la vista Navigator nos muestra:

Un poco más de Java: clases internas

¿Donde está definida la clase VentanaQueSeCierra$2?

Runnable es también un interface. Igual que antes, si ves el operador new delante de un interface en una construcción como las anteriores, es que se está definiendo una clase interna anónima.

Un poco más de Java: clases internas

Resumamos las técnicas que hemos utilizado crear un escuchador:

  1. Crear una clase de primer nivel.
  2. Crear una clase interna.
  3. Crear una clase interna anónima.

Un poco más de Java: clases internas

¿Cuando utilizar una u otra técnicas? La guía es:

  • Si la clase escuchadora la va a utilizar más de una clase cliente, utiliza una clase de primer nivel.
  • Si la clase sólo la va a utilizar una clase cliente, encapsúlala como clase interna.
    • Si vas a necesitar más de una instancia de la clase interna, nombrala.
    • Si sólo la vas a instanciar una única vez, hazla anónima.

Resumen

El modelo de programación Swing se basa en el patrón de diseño Observador/Observable.

Cada componente de Swing puede lazar eventos de uno o más tipos.

Las clases observadoras o escuchadoras deben implementar el interface adecuado para ser notificadas cuando el usuario interacciona con la interfaz gráfica.

Se debe registrar un instancia de la clase escuhadora al componente de interés.

Enlaces web