Contenidos en Parte 3
- Respuesta a cambios en la selección dentro de la tabla.
- Añade funcionalidad de los botones añadir, editar, y borrar.
- Crear un diálogo emergente (popup dialog) a medida para editar un contacto.
- Validación de la entrada del usuario.
Respuesta a cambios en la selección de la Tabla
Todavía no hemos usado la parte derecha de la interfaz de nuestra aplicación. La intención es usar esta parte para mostrar los detalles de la persona seleccionada por el usuario en la tabla.
En primer lugar vamos a añadir un nuevo método dentro de PersonOverviewController
que nos ayude a rellenar las etiquetas con los datos de una sola persona.
Crea un método llamado showPersonDetails(Person person)
. Este método recorrerá todas las etiquetas y establecerá el texto con detalles de la persona usando setText(...)
. Si en vez de una instancia de Person
se pasa null
entonces las etiquetas deben ser borradas.
PersonOverviewController.java
/** * Fills all text fields to show details about the person. * If the specified person is null, all text fields are cleared. * * @param person the person or null */ private void showPersonDetails(Person person) { if (person != null) { // Fill the labels with info from the person object. firstNameLabel.setText(person.getFirstName()); lastNameLabel.setText(person.getLastName()); streetLabel.setText(person.getStreet()); postalCodeLabel.setText(Integer.toString(person.getPostalCode())); cityLabel.setText(person.getCity()); // TODO: We need a way to convert the birthday into a String! // birthdayLabel.setText(...); } else { // Person is null, remove all the text. firstNameLabel.setText(""); lastNameLabel.setText(""); streetLabel.setText(""); postalCodeLabel.setText(""); cityLabel.setText(""); birthdayLabel.setText(""); } }
Convierte la fecha de nacimiento en una cadena
Te darás cuenta de que no podemos usar el atributo birthday
directamente para establecer el valor de una Label
porque se requiere un String
, y birthday
es de tipo LocalDate
. Así pues necesitamos convertir birthday
de LocalDate
a String
.
En la práctica vamos a necesitar convertir entre LocalDate
y String
en varios sitios y en ambos sentidos. Una buena práctica es crear una clase auxiliar con métodos estáticos (static
) para esta finalidad. Llamaremos a esta clase DateUtil
y la ubicaremos una paquete separado denominado ch.makery.address.util
:
DateUtil.java
package ch.makery.address.util; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; /** * Helper functions for handling dates. * * @author Marco Jakob */ public class DateUtil { /** The date pattern that is used for conversion. Change as you wish. */ private static final String DATE_PATTERN = "dd.MM.yyyy"; /** The date formatter. */ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN); /** * Returns the given date as a well formatted String. The above defined * {@link DateUtil#DATE_PATTERN} is used. * * @param date the date to be returned as a string * @return formatted string */ public static String format(LocalDate date) { if (date == null) { return null; } return DATE_FORMATTER.format(date); } /** * Converts a String in the format of the defined {@link DateUtil#DATE_PATTERN} * to a {@link LocalDate} object. * * Returns null if the String could not be converted. * * @param dateString the date as String * @return the date object or null if it could not be converted */ public static LocalDate parse(String dateString) { try { return DATE_FORMATTER.parse(dateString, LocalDate::from); } catch (DateTimeParseException e) { return null; } } /** * Checks the String whether it is a valid date. * * @param dateString * @return true if the String is a valid date */ public static boolean validDate(String dateString) { // Try to parse the String. return DateUtil.parse(dateString) != null; } }
DATE_PATTERN
. Para conocer los diferentes tipos de formato consulta DateTimeFormatter.
Utilización de la clase DateUtil
Ahora necesitamos utilizar la nueva clase DateUtil
en el método showPersonDetails
de PersonOverviewController
. Sustituye el TODO que habíamos añadido con la línea siguiente:
birthdayLabel.setText(DateUtil.format(person.getBirthday()));
Detecta cambios de selección en la tabla
Para enterarse de que el usuario ha seleccionado a un persona en la tabla de contactos, necesitamos escuchar los cambios. Esto se consigue mediante la implementación de un interface de JavaFX que se llama ChangeListener
with one method called changed(...)
. Este método solo tiene tres parámetros: observable
, oldValue
, y newValue
.
En Java 8 la forma más elegante de implementar una interfaz con un único método es mediante una lambda expression. Añadiremos algunas líneas al método initialize()
de PersonOverviewController
. El código resultante se asemejará al siguiente:
PersonOverviewController.java
@FXML private void initialize() { // Initialize the person table with the two columns. firstNameColumn.setCellValueFactory( cellData -> cellData.getValue().firstNameProperty()); lastNameColumn.setCellValueFactory( cellData -> cellData.getValue().lastNameProperty()); // Clear person details. showPersonDetails(null); // Listen for selection changes and show the person details when changed. personTable.getSelectionModel().selectedItemProperty().addListener( (observable, oldValue, newValue) -> showPersonDetails(newValue)); }
Con showPersonDetails(null);
borramos los detalles de una persona.
Con personTable.getSelectionModel...
obtenemos la selectedItemProperty de la tabla de personas, y le añadimos un listener. Cuando quiera que el usuario seleccione a una persona en la table, nuestra lambda expression será ejecutada: se toma la persona recien seleccionada y se le pasa al método showPersonDetails(...)
method.
Intenta ejecutar tu aplicación en este momento. Comprueba que cuando seleccionas a una persona, los detalles sobre esta son mostrados en la parte derecha de la ventana.
Si algo no funciona, puedes comparar tu clase PersonOverviewController
con PersonOverviewController.java.
El botón de borrar (Delete)
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 clasePersonOverviewController
:
PersonOverviewController.java
/** * Called when the user clicks on the delete button. */ @FXML private void handleDeletePerson() { int selectedIndex = personTable.getSelectionModel().getSelectedIndex(); personTable.getItems().remove(selectedIndex); }
Ahora, abre el archivo PersonOverview.fxml
en el SceneBuilder. Selecciona el botón Delete, abre el apartado Code y pon handleDeletePerson
en el menú desplegable denominado On Action.
Gestión de errores
Si ejecutas tu aplicación en este punto deberías ser capaz de borrar personas de la tabla. Pero, ¿qué ocurre si pulsas el botón de borrar sin seleccionar a nadie en la tabla.
Se produce 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.
Ignorar semejante error no es nada recomendable. Deberíamos hacerle saber al usuario que tiene que seleccionar una persona previamente para poderla borrar (incluso mejor sería deshabilitar el botón para que el usuario ni siquiera tenga la oportunidad de realizar una acción incorrecta).
Vamos a añadir un diálogo emergente para informar al usuario. Desafortunadamente no hay componentes para diálogos incluidos en JavaFX 8. Para evitar tener que crearlos manualmente podemos añadir una librería que ya los incluya (Dialogs):
- Descarga este controlsfx-8.0.6_20.jar (también se puede obtener de la página web de ControlsFX).
Importante: La versión de ControlsFX debe ser la8.0.6_20
o superior para que funcione conJDK 8u20
debido a un cambio crítico en esa versión. - Crea una subcarpeta lib dentro del proyecto y coloca dentro del archivo jar.
- Añade la librería al classpath de tu proyecto: En Eclipse se puede hacer mediante clic-derecho sobre el archivo jar | Build Path | Add to Build Path. Ahora Eclipse ya sabe donde encontrar esa librería.
Con algunos cambios en el método handleDeletePerson()
podemos mostrar una simple ventana de diálogo emergente en el caso de que el usuario pulse el botón Delete sin haber seleccionado a nadie en la tabla de contactos:
PersonOverviewController.java
/** * Called when the user clicks on the delete button. */ @FXML private void handleDeletePerson() { int selectedIndex = personTable.getSelectionModel().getSelectedIndex(); if (selectedIndex >= 0) { personTable.getItems().remove(selectedIndex); } else { // Nothing selected. Dialogs.create() .title("No Selection") .masthead("No Person Selected") .message("Please select a person in the table.") .showWarning(); } }
Diálogos para crear y editar contactos
Las acciones de editar y crear nuevo contacto necesitan algo más de elaboración: vamos a necesitar una ventana de diálogo a medida (es decir, un nuevo stage
) con un formulario para preguntar al usuario los detalles sobre la persona.
Diseña la ventana de diálogo
Crea un nuevo archivo fxml llamado
PersonEditDialog.fxml
dentro del paquete view.
Usa un panel de rejilla (
GridPane
), etiquetas (Label
), campos de texto (TextField
) y botones (Button
) para crear una ventana de diálogo como la siguiente:
Si quieres puedes descargar el código desde PersonEditDialog.fxml.
Create the Controller
Crea el controlador para la ventana de edición de personas y llámalo PersonEditDialogController.java
:
PersonEditDialogController.java
package ch.makery.address.view; import javafx.fxml.FXML; import javafx.scene.control.TextField; import javafx.stage.Stage; import org.controlsfx.dialog.Dialogs; import ch.makery.address.model.Person; import ch.makery.address.util.DateUtil; /** * Dialog to edit details of a person. * * @author Marco Jakob */ public class PersonEditDialogController { @FXML private TextField firstNameField; @FXML private TextField lastNameField; @FXML private TextField streetField; @FXML private TextField postalCodeField; @FXML private TextField cityField; @FXML private TextField birthdayField; private Stage dialogStage; private Person person; private boolean okClicked = false; /** * Initializes the controller class. This method is automatically called * after the fxml file has been loaded. */ @FXML private void initialize() { } /** * Sets the stage of this dialog. * * @param dialogStage */ public void setDialogStage(Stage dialogStage) { this.dialogStage = dialogStage; } /** * Sets the person to be edited in the dialog. * * @param person */ public void setPerson(Person person) { this.person = person; firstNameField.setText(person.getFirstName()); lastNameField.setText(person.getLastName()); streetField.setText(person.getStreet()); postalCodeField.setText(Integer.toString(person.getPostalCode())); cityField.setText(person.getCity()); birthdayField.setText(DateUtil.format(person.getBirthday())); birthdayField.setPromptText("dd.mm.yyyy"); } /** * Returns true if the user clicked OK, false otherwise. * * @return */ public boolean isOkClicked() { return okClicked; } /** * Called when the user clicks ok. */ @FXML private void handleOk() { if (isInputValid()) { person.setFirstName(firstNameField.getText()); person.setLastName(lastNameField.getText()); person.setStreet(streetField.getText()); person.setPostalCode(Integer.parseInt(postalCodeField.getText())); person.setCity(cityField.getText()); person.setBirthday(DateUtil.parse(birthdayField.getText())); okClicked = true; dialogStage.close(); } } /** * Called when the user clicks cancel. */ @FXML private void handleCancel() { dialogStage.close(); } /** * Validates the user input in the text fields. * * @return true if the input is valid */ private boolean isInputValid() { String errorMessage = ""; if (firstNameField.getText() == null || firstNameField.getText().length() == 0) { errorMessage += "No valid first name!\n"; } if (lastNameField.getText() == null || lastNameField.getText().length() == 0) { errorMessage += "No valid last name!\n"; } if (streetField.getText() == null || streetField.getText().length() == 0) { errorMessage += "No valid street!\n"; } if (postalCodeField.getText() == null || postalCodeField.getText().length() == 0) { errorMessage += "No valid postal code!\n"; } else { // try to parse the postal code into an int. try { Integer.parseInt(postalCodeField.getText()); } catch (NumberFormatException e) { errorMessage += "No valid postal code (must be an integer)!\n"; } } if (cityField.getText() == null || cityField.getText().length() == 0) { errorMessage += "No valid city!\n"; } if (birthdayField.getText() == null || birthdayField.getText().length() == 0) { errorMessage += "No valid birthday!\n"; } else { if (!DateUtil.validDate(birthdayField.getText())) { errorMessage += "No valid birthday. Use the format dd.mm.yyyy!\n"; } } if (errorMessage.length() == 0) { return true; } else { // Show the error message. Dialogs.create() .title("Invalid Fields") .masthead("Please correct invalid fields") .message(errorMessage) .showError(); return false; } } }
Algunas cuestiones relativas a este controlador:
- El método
setPerson(...)
puede ser invocado desde otra clase para establecer la persona que será editada. - Cuando el usuario pula el botón OK, el método
handleOk()
es invocado. Primero se valida la entrada del usuario mediante la ejecución del métodoisInputValid()
. 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étodosetPerson(...)
! - El método boolean
okClicked
se utiliza para determinar si el usuario ha pulsado el botón OK o el botón Cancel.
Enlaza la vista y el controlador
Una vez creadas la vista (FXML) y el controlador, necesitamos vincular el uno con el otro:
- Abre el archivo
PersonEditDialog.fxml
. - En la sección Controller a la izquierda selecciona
PersonEditDialogController
como clase de control. - 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.
Abriendo la ventana de diálogo
Añade un método para cargar y mostrar el método de edición de una persona dentro de la clase MainApp
:
MainApp.java
/** * Opens a dialog to edit details for the specified person. If the user * clicks OK, the changes are saved into the provided person object and true * is returned. * * @param person the person object to be edited * @return true if the user clicked OK, false otherwise. */ public boolean showPersonEditDialog(Person person) { try { // Load the fxml file and create a new stage for the popup dialog. FXMLLoader loader = new FXMLLoader(); loader.setLocation(MainApp.class.getResource("view/PersonEditDialog.fxml")); AnchorPane page = (AnchorPane) loader.load(); // Create the dialog Stage. Stage dialogStage = new Stage(); dialogStage.setTitle("Edit Person"); dialogStage.initModality(Modality.WINDOW_MODAL); dialogStage.initOwner(primaryStage); Scene scene = new Scene(page); dialogStage.setScene(scene); // Set the person into the controller. PersonEditDialogController controller = loader.getController(); controller.setDialogStage(dialogStage); controller.setPerson(person); // Show the dialog and wait until the user closes it dialogStage.showAndWait(); return controller.isOkClicked(); } catch (IOException e) { e.printStackTrace(); return false; } }
Añade los siguientes métodos a la clase PersonOverviewController
. Esos métodos llamarán al método showPersonEditDialog(...)
desde MainApp
cuando el usuario pulse en los botones new o edit.
PersonOverviewController.java
/** * Called when the user clicks the new button. Opens a dialog to edit * details for a new person. */ @FXML private void handleNewPerson() { Person tempPerson = new Person(); boolean okClicked = mainApp.showPersonEditDialog(tempPerson); if (okClicked) { mainApp.getPersonData().add(tempPerson); } } /** * Called when the user clicks the edit button. Opens a dialog to edit * details for the selected person. */ @FXML private void handleEditPerson() { Person selectedPerson = personTable.getSelectionModel().getSelectedItem(); if (selectedPerson != null) { boolean okClicked = mainApp.showPersonEditDialog(selectedPerson); if (okClicked) { showPersonDetails(selectedPerson); } } else { // Nothing selected. Dialogs.create() .title("No Selection") .masthead("No Person Selected") .message("Please select a person in the table.") .showWarning(); } }
Abre el archivo PersonOverview.fxml
mediante Scene Builder. Elige los métodos correspondientes en el campo On Action para los botones new y edit.
¡Ya está!
Llegados a este punto deberías tener una aplicación de libreta de contactos en funcionamiento. 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.
Espero que los conceptos y estructura de esta aplicación te permitan empezar tu propia aplicación JavaFX. ¡ Disfruta !
Qué es lo siguiente?
En Tutorial Parte 4 introduciremos algo de diseño mediante hojas de estilo CSS.