Saltar la navegación

Interaccionando con el usuario

Hasta ahora hemos creado aplicaciones que nos muestran una interfaz de usuario gráfica utilizando JavaFX pero que no tiene ninguna funcionalidad. En este apartado nos vamos a dedicar a ver cómo podemos reaccionar ante las interacciones del usuario con dicha interfaz.

Cuando el usuario pulsa un botón, marca un radio botón, elige una opción de caja de elección, mueve un deslizador o una barra de desplazamiento, selecciona una fila en una tabla o un árbol, mueve el ratón en el espacio de la aplicación, presiona una tecla en un campo de texto, etc. se genera un evento el cuál nuestra aplicación puede escuchar, para que cuando se produzca pueda reaccionar al mismo de forma adecuada.

Por tanto, un evento no es más que la ocurrencia de algo que sucede y que es de interés para nuestra aplicación. Los eventos generados son objetos que heredan de la clase javafx.event.Event. Más adelante veremos los diferentes tipos de eventos que nos proporciona JavaFX. Un evento posee propiedades tales como su tipo, el origen del mismo y el destino. Dependiendo del tipo de evento, éste puede poseer propiedades adiciones como, por ejemplo, un evento de ratón nos indicará qué botón se ha pulsado, cuántas veces, la posición del puntero, etc.

Los tipos de eventos más típicos son (aunque hay más que no me detendré ni siquiera a nombrar, para ello puedes buscar en la documentación de Oracle):

  • ActionEvent: se produce al pulsar un botón, un elemento de menú es seleccionado, una opción de una caja de elección es seleccionada, etc. Es el evento más común para los controles simples.
  • KeyEvent: se produce al pulsar una tecla del teclado.
  • MouseEvent: se produce al mover el ratón o pulsar una de sus teclas.
  • WindowEvent: se produce cuando interaccionamos con la ventana; la minimizamos, la maximizamos, la queremos cerrar, etc.

Para poder reaccionar a un evento deberemos crear un método denominado manejador de evento que será al que se llamará cuando dicho evento se produzca. A este método se le suele pasar el evento que se ha producido. Para que cuando se produzca el evento se llame a dicho método deberemos indicárselo a la aplicación y a eso es a lo que se llama registrar el manejador. Al registrar el manejador para un evento dado, estamos indicando que queremos esperar a que se produzcan eventos de ese tipo y que cuando se produzcan nos avise llamando a dicho método manejador (o realizando una acción dada).

Para registrar un manejador cada nodo tiene métodos del tipo setOnTipoEvento que nos permite indicarle a qué método debe llamar cuando se produzca ese tipo de evento sobre dicho nodo. Dicho método acepta como parámetro un objeto cuya clase implementa la interfaz EventHandler. Esta interfaz es una interfaz funcional (que sólo obliga a implementar un único método) que nos obliga a implementar el método handle para la misma. Por lo que podríamos crear una clase como ya sabemos hacerlo que implemente dicha interfaz, implementar el método handle, crear un objeto de dicha clase y pasárselo al método como parámetro. Pero todo esto sería demasiado engorroso para registrar cada uno de los manejadores que queremos utilizar en nuestra aplicación.

package javafx.manejadores;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class ManejadorClase extends Application {
	
	private class MiManejador implements EventHandler<ActionEvent> {

		@Override
		public void handle(ActionEvent event) {
			System.out.println("Botón pulsado!!!");
		}
		
	}

	@Override
	public void start(Stage escenarioPrincipal) {
		try {
			HBox raiz = new HBox();
			raiz.setAlignment(Pos.CENTER);
			
			Button btEjemplo = new Button("Púlsame!!!!");
			btEjemplo.setOnAction(new MiManejador());
			
			raiz.getChildren().add(btEjemplo);
			
			Scene escena = new Scene(raiz, 350, 100);
			escenarioPrincipal.setTitle("Manejador Clase");
			escenarioPrincipal.setScene(escena);
			escenarioPrincipal.show();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
		launch(args);
	}
}

Una forma algo menos engorrosa sería utilizando una clase anónima para registrar el manejador.

package javafx.manejadores;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class ManejadorClaseAnonima extends Application {

	@Override
	public void start(Stage escenarioPrincipal) {
		try {
			HBox raiz = new HBox();
			raiz.setAlignment(Pos.CENTER);
			
			Button btEjemplo = new Button("Púlsame!!!!");
			btEjemplo.setOnAction(new EventHandler<ActionEvent>() {

				@Override
				public void handle(ActionEvent event) {
					System.out.println("Botón pulsado!!!");					
				}

			});
			
			raiz.getChildren().add(btEjemplo);
			
			Scene escena = new Scene(raiz, 350, 100);
			escenarioPrincipal.setTitle("Manejador Clase Anónima");
			escenarioPrincipal.setScene(escena);
			escenarioPrincipal.show();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
		launch(args);
	}
}

Pero la forma más cómoda es el uso de expresiones lambda de java 8.

  • Definiendo un método al que llamaremos en la propia expresión lambda que se pasa como parámetro del método setOnTipoEvento del nodo.
    private void botonPulsado(ActionEvent event) {
         System.out.println("Botón pulsado");
    }
    ...
    ...
    btEjemplo.setOnAction(e -> botonPulsado(e));
    El parámetro del método es opcional definirlo. Si lo definimos podremos acceder al objeto evento dentro del método. Si no lo definimos, como es normal, tampoco se le pasará como parámetro.
    En el caso especial en que en la función lambda simplemente invoca a un método al que se le pasa el parámetro o parámetros, podemos sustituir la función lambda por un selector de métodos:
    private void botonPulsado(ActionEvent event) {
         System.out.println("Botón pulsado");
    }
    ...
    ...
    btEjemplo.setOnAction(this::botonPulsado);
  • Definiendo las acciones a realizar en la misma expresión lambda que se pasa como parámetro del método setOnTipoEvento del nodo.
    btEjemplo.setOnAction(e -> {
        System.out.println("Botón pulsado");
    });
    Si sólo se trata de una sola sentencia podríamos ponerla sin las llaves.

También es posible registrar manejadores de eventos mediante el método addEventHandler que acepta como parámetros el tipo de evento y el manejador para dicho evento.

Propiedades

En JavaFX existen las llamadas propiedades. Simplemente son clases envoltorio que permiten encapsular los diferentes atributos de un objeto y así nos pueden informar de los cambios que se producen en las mismas, enlazar diferentes propiedades de diferentes nodos para que los cambios en una de ellas automáticamente se vea reflejado en las otras, etc.

Las propiedades son clases abstractas que tienen nombres muy parecidos a los tipos que envuelven: StringProperty, IntegerProperty, DoubleProperty, ObjectProperty<T>, etc. para las de lectura-escritura y: ReadOnlyStringProperty, ReadOnlyIntegerProperty, etc para las de sólo lectura. Luego existen dos implementaciones para cada una de ellas: para las de de lectura-escritura SimpleXXXProperty y para las de sólo lectura ReadOnlyXXXWrapper. Además, dependiendo del caso, existen los método get y getValue (el primero obtiene el tipo primitivo y el segundo el objeto -para una propiedad del tipo SimpleIntegerProperty, get() devolvería un int y getValue() un Integer-) y set y setValue (el primero establece su valor mediante el tipo primitivo y el seguno mediante el objeto).

Veamos cómo implementar una clase para un Personaje utilizando propiedades de JavaFX.

package javafx.eventos.clases;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class PersonajePropiedades {
	public enum Estrategia {
		RISA,
		MALHUMOR,
		ATAQUE
	}
	
	private final StringProperty nombre = new SimpleStringProperty();
	private final IntegerProperty poder = new SimpleIntegerProperty();
	private final BooleanProperty superpoder = new SimpleBooleanProperty();
	private final ObjectProperty<Estrategia> estrategia = new SimpleObjectProperty<>();
	
	public PersonajePropiedades(String nombre, int poder, boolean superpoder, Estrategia estrategia) {
		this.nombre.set(nombre);
		this.poder.set(poder);
		this.superpoder.set(superpoder);
		this.estrategia.set(estrategia);
	}

	public final String getNombre() {
		return nombre.get();
	}
	
	public final void setNombre(String nombre) {
		this.nombre.set(nombre);
	}
	
	public final StringProperty nombreProperty() {
		return nombre;
	}
	
	public final int getPoder() {
		return poder.get();
	}
	
	public final void setPoder(int poder) {
		this.poder.set(poder);
	}
	
	public final IntegerProperty poderProperty() {
		return poder;
	}
	
	public final boolean isSuperpoder() {
		return superpoder.get();
	}
	
	public final void setSuperpoder(boolean superpoder) {
		this.superpoder.set(superpoder);
	}
	
	public final BooleanProperty superpoderProperty() {
		return superpoder;
	}
	
	public final Estrategia getEstrategia() {
		return estrategia.get();
	}
	
	public final void setEstrategia(Estrategia estrategia) {
		this.estrategia.set(estrategia);
	}
	
	public final ObjectProperty<Estrategia> estrategiaProperty() {
		return estrategia;
	}
	
	public String toString() {
		return String.format("Nombre: %s, Poder: %d, Superpoder: %b, Estrategia: %s",
				nombre.get(), poder.get(), superpoder.get(), estrategia.get());
	}
}

Como ya hemos comentado, la ventaja de las propiedades es que podemos ser notificados cuando cambian sus valores. Por lo que otra posibilidad para interactuar correctamente con nuestra interfaz es escuchar los cambios que se producen en las propiedades de los modelos que muchos controles tienen asociados. Eso lo podemos hacer añadiendo un ChangeListener a la propiedad asociada al control. Un ChangeListener no es más que una interfaz que implementan dichas propiedades y que acepta como parámetros el valor de la propiedad, el valor antiguo antes del cambio y el valor nuevo que acabamos de cambiar.

Todo esto lo iremos viendo en los ejemplos que ahora os dejo y que os explico con detalle en los siguientes apartados.

Pues pasemos a ver algunos ejemplos, ahora sí totalmente funcionales, que nos permiten controlar las interacciones del usuario con nuestra aplicación y en la que iremos añadiendo algunos conceptos nuevos.