jairogarcíarincón

LibretaDirecciones: Interacción con el usuario


4.33K



En este apartado nos centraremos en las funciones relativas a la visualización de los detalles de cada persona, así como en las opciones de añadir, borrar y editar personas, incluyendo validación de datos de entrada.

Mostrar detalles de una persona

El objetivo será que cuando seleccionemos una persona en la tabla de la izquierda, nos muestre los detalles en la parte de la derecha.

Para ello, creamos el siguiente método llamado mostrarDetallesPersona(Persona persona) dentro de VistaPersonaController.java:


//Muestra los detalles de la persona seleccionada
    private void mostrarDetallesPersona(Persona persona) {

        if (persona != null) {
            //Relleno los labels desde el objeto persona
            nombreLabel.setText(persona.getNombre());
            apellidosLabel.setText(persona.getApellidos());
            direccionLabel.setText(persona.getDireccion());
            codigoPostalLabel.setText(Integer.toString(persona.getCodigoPostal()));
            ciudadLabel.setText(persona.getCiudad());
            //TODO: Tenemos que convertir la fecha de nacimiento en un String
            //fechaDeNacimientoLabel.setText(...);
        } else {
            //Persona es null, vacío todos los labels.
            nombreLabel.setText("");
            apellidosLabel.setText("");
            direccionLabel.setText("");
            codigoPostalLabel.setText("");
            ciudadLabel.setText("");
            fechaDeNacimientoLabel.setText("");
        }
    }


Trabajar con fechas

Dado que nuestra propiedad fechaDeNacimiento es de tipo LocalDate, no podemos trabajar con ella directamente, sino que tenemos que realizar una conversión de LocalDate a String.

No obstante, ya que en muchos sitios (y en futuros proyectos) vamos a necesitar esta conversión en ambos sentidos, vamos a crear una clase auxiliar que contenga los métodos estáticos necesarios para realizar estas conversiones.

Para ellos, vamos a crear una clase llamada UtilidadDeFechas dentro de un nuevo paquete (package) llamado util:


package util;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

public class UtilidadDeFechas {

    //El patrón utilizado para la conversión
    private static final String FECHA_PATTERN = "dd/MM/yyyy";

    //El formateador de fecha
    private static final DateTimeFormatter FECHA_FORMATTER = DateTimeFormatter.ofPattern(FECHA_PATTERN);

    //Devuelve la fecha de entrada como un string formateado
    public static String formato(LocalDate fecha){

        if (fecha == null){
            return null;
        }
        return FECHA_FORMATTER.format(fecha);

    }

    //Convierte un string en un objeto de tipo LocalDate (o null si no puede convertirlo)
    public static LocalDate convertir(String fecha) {
        try {
            return FECHA_FORMATTER.parse(fecha, LocalDate::from);
        } catch (DateTimeParseException e) {
            return null;
        }
    }

    //Comprueba si un string de fecha es una fecha válida y devuelve 1 o 0
    //Usamos el método anterior para la comprobación
    public static boolean fechaValida(String fecha) {

        return UtilidadDeFechas.convertir(fecha) != null;

    }
}


Como vemos, hemos utilizado el formato de fecha dd/MM/yyyy, si bien podríamos utilizar cualquier otro consultando als diferentes opciones que nos ofrece DateTimeFormatter.

Y por último, una vez creada la clase anterior, ya podemos sustituir el TODO del método mostrarDetallesPersona(Persona persona) para que quede como sigue:


fechaDeNacimientoLabel.setText(UtilidadDeFechas.formato(persona.getFechaDeNacimiento()));


Detectar cambios en la selección de la tabla

Para saber cuando el usario ha seleccionado a una persona de la tabla y mostrar sus detalles, necesitamos "escuchar" dichos cambios.

Para ello, implementaremos el interface de JavaFX ChangeListener con el método changed(...), que solo tiene 3 parámetros: observable, oldValue y newValue.

Esto lo vamos a hacer añadiendo al método initialize() de VistaPersonaController una lambda expression:


//Inicializa la clase controller y es llamado justo DESPUÉS de cargar el archivo FXML
    @FXML
    private void initialize() {

        //Inicializo la tabla con las dos primera columnas
        String nombre = "nombre";
        String apellidos = "apellidos";
        nombreColumn.setCellValueFactory(new PropertyValueFactory<>(nombre));
        apellidosColumn.setCellValueFactory(new PropertyValueFactory<>(apellidos));

        //Borro los detalles de la persona
        mostrarDetallesPersona(null);

        //Escucho cambios en la selección de la tabla y muestro los detalles en caso de cambio
        tablaPersonas.getSelectionModel().selectedItemProperty().addListener(
            (observable, oldValue, newValue) -> mostrarDetallesPersona(newValue));
    }


Con la instrucción mostrarDetallesPersona(null) borramos los detalles de una persona.

Con tablaPersonas.getSelectionModel()... obtenemos la selectedItemProperty de la tabla de personas, y le añadimos un listener. De este modo, cuando el usuario seleccione a una persona de la tabla, nuestra lambda expression será ejecutada, cogiendo a la persona recién seleccionada y pasándosela al método mostrarDetallesPersona(...).

Si ahora ejecutamos nuestra aplicación (ejecutando Clean and build previamente si es necesario), deberíamos conseguir la funcionalidad implementada y al ir seleccionando diferentes personas en la tabla de la izquierda veremos todos los detalles a la derecha.



Borrar una persona

Nuestro interfaz de usuario ya contiene un botón de borrar, pero sin funcionalidad.

Podemos seleccionar la acción a ejecutar al pulsar un botón desde el Scene Builder. Cualquier método de nuestro controlador anotado con @FXML (o declarado como public) es accesible desde Scene Builder. Así pues, empecemos añadiendo el método de borrado al final de nuestra clase VistaPersonaController:


//Borro la persona seleccionada cuando el usuario hace clic en el botón de Borrar
    @FXML
    private void borrarPersona() {
        //Capturo el indice seleccionado y borro su item asociado de la tabla
        int indiceSeleccionado = tablaPersonas.getSelectionModel().getSelectedIndex();
        tablaPersonas.getItems().remove(indiceSeleccionado);
    }


Para terminar, abrimos el archivo VistaPersona.fxml con Scene Builder, seleccionamos el botón Borrar, y en la sección Code (derecha) seleccionamos borrarPersona como método para la opción On Action.

Gestión de errores

Si bien en este punto deberíamos poder ejecutar nuestra aplicación y borrar elementos de la tabla ¿Qué ocurre si pulsamos el botón de Borrar sin ningún elemento seleccionado? Pues que obtendremos un error de tipo ArrayIndexOutOfBoundsException porque no puede borrar una persona en el índice -1, que es el valor devuelto por el método getSelectedIndex() cuando no hay ningún elemento seleccionado.

Para resolver el error simplemente modificaremos el método borrarPersona para asegurarnos de que el índice seleccionado es mayor o igual a 0, mostrando un mensaje de alerta en caso contrario:


//Borro la persona seleccionada cuando el usuario hace clic en el botón de Borrar
    @FXML
    private void borrarPersona() {
        //Capturo el indice seleccionado y borro su item asociado de la tabla
        int indiceSeleccionado = tablaPersonas.getSelectionModel().getSelectedIndex();
        if (indiceSeleccionado >= 0){
            //Borro item
            tablaPersonas.getItems().remove(indiceSeleccionado);

        } else {
            //Muestro alerta
            Alert alerta = new Alert(AlertType.WARNING);
            alerta.setTitle("Atención");
            alerta.setHeaderText("Persona no seleccionada");
            alerta.setContentText("Por favor, selecciona una persona de la tabla");
            alerta.showAndWait();

        }
    }


Toda la información relativa al uso de diálogos en JavaFX la puedes encontrar haciendo clic AQUÍ o en la Documentación oficial de JavaFX.

Editar y añadir una persona

Para poder editar y/o añadir personas, vamos a necesitar una nueva ventana de diálogo a medida (es decir, un nuevo escenario o stage) que incluya un formulario con los campos de los detalles de la persona.

Vista EditarPersona

Dentro del paquete view, añadimos un nuevo archivo EditarPersona.fxml y, usando un panel de rejilla (GridPane), etiquetas(Label), campos de texto (TextField) y botones (Button) creamos una ventana de diálogo similar a la siguiente:



Controlador EditarPersona



package view;

import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import model.Persona;
import util.UtilidadDeFechas;

public class EditarPersonaController {

    //TextField para los campos
    @FXML
    private TextField nombreTextField;
    @FXML
    private TextField apellidosTextField;
    @FXML
    private TextField direccionTextField;
    @FXML
    private TextField codigoPostalTextField;
    @FXML
    private TextField ciudadTextField;
    @FXML
    private TextField fechaDeNacimientoTextField;

    private Stage escenarioEdicion; //Escenario de edición
    private Persona persona; // Referencia a la clase persona
    private boolean guardarClicked = false;

    //Inicializa la clase controller y es llamado justo DESPUÉS de cargar el archivo FXML
    @FXML
    private void initialize() {
    }

    //Establece el escenario de edición
    public void setEscenarioEdicion(Stage escenarioEdicion) {
        this.escenarioEdicion = escenarioEdicion;
    }

    //Establece la persona a editar
    public void setPersona(Persona persona) {
        this.persona = persona;

        nombreTextField.setText(persona.getNombre());
        apellidosTextField.setText(persona.getApellidos());
        direccionTextField.setText(persona.getDireccion());
        codigoPostalTextField.setText(Integer.toString(persona.getCodigoPostal()));
        ciudadTextField.setText(persona.getCiudad());
        fechaDeNacimientoTextField.setText(UtilidadDeFechas.formato(persona.getFechaDeNacimiento()));
        fechaDeNacimientoTextField.setPromptText("dd/mm/yyyy");

    }

    //Devuelve true si se ha pulsado Guardar, si no devuelve false
    public boolean isGuardarClicked() {
        return guardarClicked;
    }

    //LLamado cuando se pulsa Guardar
    @FXML
    private void guardar() {

        if (datosValidos()) {

            //Asigno datos a propiedades de persona
            persona.setNombre(nombreTextField.getText());
            persona.setApellidos(apellidosTextField.getText());
            persona.setDireccion(direccionTextField.getText());
            persona.setCodigoPostal(Integer.parseInt(codigoPostalTextField.getText()));
            persona.setCiudad(ciudadTextField.getText());
            persona.setFechaDeNacimiento(UtilidadDeFechas.convertir(fechaDeNacimientoTextField.getText()));

            guardarClicked = true; //Cambio valor booleano
            escenarioEdicion.close(); //Cierro el escenario de edición

        }
    }

    //LLamado cuando se pulsa Cancelar
    @FXML
    private void cancelar() {
        escenarioEdicion.close();
    }

    //Validación de datos
    private boolean datosValidos(){

        //Inicializo string para mensajes
        String mensajeError = "";

        //Compruebo los campos
        if (nombreTextField.getText() == null || nombreTextField.getText().length() == 0) {
            mensajeError += "Nombre no válido.\n";
        }
        if (apellidosTextField.getText() == null || apellidosTextField.getText().length() == 0) {
            mensajeError += "Apellidos no válidos.\n";
        }
        if (direccionTextField.getText() == null || direccionTextField.getText().length() == 0) {
            mensajeError += "Dirección no válida.\n";
        }

        if (codigoPostalTextField.getText() == null || codigoPostalTextField.getText().length() == 0) {
            mensajeError += "Código postal no válido.\n";
        } else {
            //Convierto el código postal a entero
            try {
                Integer.parseInt(codigoPostalTextField.getText());
            } catch (NumberFormatException e) {
                mensajeError += "Código postal no válido (debe ser un entero).\n";
            }
        }

        if (ciudadTextField.getText() == null || ciudadTextField.getText().length() == 0) {
            mensajeError += "Ciudad no válida.\n";
        }

        if (fechaDeNacimientoTextField.getText() == null || fechaDeNacimientoTextField.getText().length() == 0) {
            mensajeError += "Fecha de nacimiento no válida.\n";
        } else {
            if (!UtilidadDeFechas.fechaValida(fechaDeNacimientoTextField.getText())) {
                mensajeError += "Fecha de nacimiento no válida (debe tener formato dd/mm/yyyy).\n";
            }
        }

        //Si no hay errores devuelvo true, si no, una alerta con los errores y false
        if (mensajeError.length() == 0) {
            return true;
        } else {
            //Muestro alerta y devuelvo false
            Alert alerta = new Alert(Alert.AlertType.ERROR);
            alerta.setTitle("Error");
            alerta.setHeaderText("Datos no válidos");
            alerta.setContentText("Por favor, corrige los errores");
            alerta.showAndWait();
            return false;
        }

    }

}


Algunas cuestiones relativas a este controlador:

  • El método setPersona(...) lo invocaremos desde otra clase para establecer la persona que será editada.
  • Cuando el usuario pulse Guardar, el método guardar() es invocado. Primero se valida la entrada del usuario mediante la ejecución del método datosValidos().
  • Sólo si la validación tiene éxito el objeto Persona es modificado con los datos introducidos por el usuario. Esos cambios son aplicados directamente sobre el objeto pasado como argumento del método setPersona(...)
  • El método boolean isGuardarClicked() se utiliza para determinar si el usuario ha pulsado el botón Guardar o el botón Cancelar.

Enlazar vista y controlador

  • Abre el archivo EditarPersona.fxml en Scene Builder.
  • En la sección Controller a la izquierda selecciona EditarPersonaController como clase de control (si no aparece, tendrás que cerrar el archivo en Scene Builder y volver a abrirlo)
  • En la sección Code, a la derecha, establece el campo fx:id de todas los TextField con los identificadores de los atributos del controlador correspondientes.
  • Especifica el campo onAction de los dos botones con los métodos del controlador correspondientes a cada acción (guardar() y cancelar())

Abrir la vista EditarPersona

La vista EditarPersona la abriremos desde un método nuevo llamado mostrarEditarPersona dentro de LibretaDirecciones.java:


//Vista editarPersona
    public boolean muestraEditarPersona(Persona persona) {

        //Cargo la vista persona a partir de VistaPersona.fxml
        FXMLLoader loader = new FXMLLoader();
        URL location = LibretaDirecciones.class.getResource("../view/EditarPersona.fxml");
        loader.setLocation(location);
        try {
            editarPersona = loader.load();
        } catch (IOException ex) {
            Logger.getLogger(LibretaDirecciones.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }

        //Creo el escenario de edición (con modal) y establezco la escena
        Stage escenarioEdicion = new Stage();
        escenarioEdicion.setTitle("Editar Persona");
        escenarioEdicion.initModality(Modality.WINDOW_MODAL);
        escenarioEdicion.initOwner(escenarioPrincipal);
        Scene escena = new Scene(editarPersona);
        escenarioEdicion.setScene(escena);

        //Asigno el escenario de edición y la persona seleccionada al controlador
        EditarPersonaController controller = loader.getController();
        controller.setEscenarioEdicion(escenarioEdicion);
        controller.setPersona(persona);

        //Muestro el diálogo ahjsta que el ussuario lo cierre
        escenarioEdicion.showAndWait();

        //devuelvo el botón pulsado
        return controller.isGuardarClicked();

    }


Añade los siguientes métodos a la clase VistaPersonaController. Esos métodos llamarán al método anterior muestraEditarpersona(...) de LibretaDirecciones.java cuando el usuario pulse en los botones Crear o Editar:


//Muestro el diálogo editar persona cuando el usuario hace clic en el botón de Crear
    @FXML
    private void crearPersona() {
        Persona temporal = new Persona();
        boolean guardarClicked = libretaDirecciones.muestraEditarPersona(temporal);
        if (guardarClicked) {
            libretaDirecciones.getDatosPersona().add(temporal);
        }
    }

    //Muestro el diálogo editar persona cuando el usuario hace clic en el botón de Editar
    @FXML
    private void editarPersona() {
        Persona seleccionada = tablaPersonas.getSelectionModel().getSelectedItem();
        if (seleccionada != null) {
            boolean guardarClicked = libretaDirecciones.muestraEditarPersona(seleccionada);
            if (guardarClicked) {
                mostrarDetallesPersona(seleccionada);
            }

        } else {
            //Muestro alerta
            Alert alerta = new Alert(Alert.AlertType.WARNING);
            alerta.setTitle("Alerta");
            alerta.setHeaderText("Persona no seleccionada");
            alerta.setContentText("Por favor, selecciona una persona");
            alerta.showAndWait();
        }
    }


Para finalizar, abre el archivo VistaPersona.fxml mediante Scene Builder y elige los métodos correspondientes (crearPersona() y editarPersona() para el campo On Action de la sección Code (derecha) de los botones Crear y Editar.

Llegados a este punto deberíamos tener nuestra aplicación LibretaDirecciones en funcionamiento, con un aspecto similar al de la captura mostrada al inicio de este capítulo. Esta aplicación es capaz de añadir, editar y borrar personas. Tiene incluso algunas capacidades de validación para evitar que el usuario introduzca información incorrecta.

Otras funcionalidades para agregar a tu proyecto

Funcionalidad que nos permita añadir la fecha de nacimiento mediante un desplegable de selección de fechas, usando para ello un JavaFX DatePicker. Puedes seguir ESTE TUTORIAL.
Funcionalidad que nos permita ordenar y filtrar los datos de la tabla, usando para ello las clases SortedList y FilteredList. Puedes seguir ESTE TUTORIAL.
Funcionalidad que nos permita renderizar las celdas de la tabla en función de su contenido, usando para ello Cell Factory y Cell Value Factory. Puedes seguir ESTE TUTORIAL.


Publicado el 30 de Enero de 2025

xmlinterfacesjavafx