Volver al blog

Desarrollo de Interfaces en DAM: JavaFX, FXML e informes con ejercicios de examen resueltos

Desarrollo de Interfaces en DAM: JavaFX, FXML e informes con ejercicios de examen resueltos

Desarrollo de Interfaces (DI) es el módulo de segundo de DAM en el que aprendes a construir la parte visible de una aplicación de escritorio: ventanas, formularios, tablas y diálogos, y además componentes propios, informes, ayuda, instaladores y pruebas. En la mayoría de centros se da con JavaFX, y para aprobarlo necesitas dominar cinco ejercicios tipo: un formulario con validación, una tabla enlazada a una ObservableList, una vista en FXML con su controlador, un componente reutilizable y una tarea en segundo plano que no congele la ventana.

Lo difícil no es la lógica, que suele ser sencilla. Es que cuando algo falla, el error casi nunca señala la línea culpable: un fx:id mal escrito da un NullPointerException en otro sitio y una ruta mal puesta da un Location is required. Aquí tienes qué entra, cinco ejercicios resueltos y explicados, los errores que más vemos en clase y cómo prepararlo.

Qué entra en Desarrollo de Interfaces

Desarrollo de Interfaces es el módulo 0488 del título de DAM (Real Decreto 450/2010) y se da en segundo curso. El decreto fija un temario con ocho resultados de aprendizaje, pero el orden y sobre todo la tecnología cambian según la comunidad y el centro: lo más habitual es JavaFX con Scene Builder, en materiales antiguos aparece Swing, y hay centros que usan C# (WPF o Windows Forms) o Kotlin con Compose.

Bloque Qué te piden Lo que más falla
Interfaces con editor visual Ventanas, controles, layouts y eventos, con Scene Builder o el diseñador del IDE Colocar todo con posiciones absolutas
Interfaces basadas en XML FXML (o XAML en C#), controlador, enlace de datos fx:id y métodos que no coinciden con el controlador
Componentes visuales Crear un control propio con propiedades y eventos, reutilizarlo Componentes que no se pueden usar desde FXML ni Scene Builder
Usabilidad y accesibilidad Pautas de diseño, navegación con teclado, mensajes claros Dejarla para el final o no hacerla
Informes JasperReports con Jaspersoft Studio (o la herramienta del centro) Campos del informe que no coinciden con los atributos
Documentación y ayuda Manual de usuario, ayuda integrada, tooltips Documentar solo el código y no al usuario
Distribución Empaquetar la aplicación y crear un instalador La aplicación funciona en el IDE pero no fuera
Pruebas Pruebas de la lógica y de la interfaz, pruebas con usuarios No separar la lógica y no poder probarla

Los dos primeros bloques concentran casi toda la parte práctica. Los ejercicios de abajo están en JavaFX con Java 17; en C# las ideas son las mismas.

FXML o código, y dónde encaja MVC

En JavaFX una interfaz se construye de dos formas. Con código, creas los controles con new, los metes en layouts y les asignas eventos, todo en Java. Con FXML, describes la ventana en un fichero XML (normalmente con Scene Builder) y la lógica va en una clase aparte, el controlador, enlazada mediante fx:controller, fx:id y @FXML.

FXML encaja con el patrón MVC (Modelo-Vista-Controlador): el modelo son tus clases de datos (Alumno), la vista es el .fxml y el controlador recibe los eventos y actualiza el modelo. La ventaja que suelen preguntar: cambias el diseño sin tocar la lógica, y la lógica se prueba sin abrir ventanas.

Cinco ejercicios típicos de Desarrollo de Interfaces resueltos

La mayoría de prácticas y exámenes de DI son una variación de estos. Intenta cada uno antes de mirar la solución.

1. Formulario con validación y un layout que se adapta

Enunciado: ventana de alta de alumno con nombre, email y edad. Guardar solo se activa con los tres campos rellenos. Al pulsarlo, valida el formato del email y que la edad sea un número entre 16 y 99, y muestra el error en la ventana. Debe poder usarse solo con el teclado.

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class FormularioAlumno extends Application {

    @Override
    public void start(Stage stage) {
        TextField txtNombre = new TextField();
        TextField txtEmail = new TextField();
        TextField txtEdad = new TextField();
        txtEdad.setPromptText("Entre 16 y 99");
        txtEdad.setTooltip(new Tooltip("Edad en años, solo números"));

        Label lblNombre = etiqueta("_Nombre:", txtNombre);
        Label lblEmail = etiqueta("_Email:", txtEmail);
        Label lblEdad = etiqueta("E_dad:", txtEdad);

        Label lblError = new Label();
        lblError.setStyle("-fx-text-fill: #b00020;");

        Button btnGuardar = new Button("_Guardar");
        btnGuardar.setDefaultButton(true); // Enter pulsa Guardar
        // No se puede pulsar mientras falte algún dato
        btnGuardar.disableProperty().bind(
                txtNombre.textProperty().isEmpty()
                        .or(txtEmail.textProperty().isEmpty())
                        .or(txtEdad.textProperty().isEmpty()));

        btnGuardar.setOnAction(e -> {
            String error = validar(txtNombre.getText(), txtEmail.getText(), txtEdad.getText());
            if (error != null) {
                lblError.setText(error);
                return;
            }
            lblError.setText("");
            Alert ok = new Alert(Alert.AlertType.INFORMATION,
                    "Alumno guardado: " + txtNombre.getText().trim());
            ok.setHeaderText(null);
            ok.showAndWait();
        });

        GridPane grid = new GridPane();
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(20));
        grid.addRow(0, lblNombre, txtNombre);
        grid.addRow(1, lblEmail, txtEmail);
        grid.addRow(2, lblEdad, txtEdad);
        grid.add(lblError, 0, 3, 2, 1);  // ocupa dos columnas
        grid.add(btnGuardar, 1, 4);

        stage.setTitle("Alta de alumno");
        stage.setScene(new Scene(grid));
        stage.show();
    }

    private static Label etiqueta(String texto, TextField campo) {
        Label lbl = new Label(texto);
        lbl.setMnemonicParsing(true); // en Label viene desactivado
        lbl.setLabelFor(campo);       // Alt + letra lleva el foco al campo
        return lbl;
    }

    static String validar(String nombre, String email, String edadTexto) {
        if (nombre.isBlank()) {
            return "El nombre es obligatorio";
        }
        if (!email.trim().matches("[^@\\s]+@[^@\\s]+\\.[a-zA-Z]{2,}")) {
            return "El email no es válido";
        }
        try {
            int edad = Integer.parseInt(edadTexto.trim());
            if (edad < 16 || edad > 99) {
                return "La edad debe estar entre 16 y 99";
            }
        } catch (NumberFormatException ex) {
            return "La edad tiene que ser un número";
        }
        return null;
    }

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

Lo que hay que entender: la jerarquía de JavaFX es Stage (la ventana), Scene (el contenido) y un árbol de nodos que empieza en un layout. El GridPane coloca los controles en filas y columnas, y si agrandas la ventana todo se recoloca solo; con coordenadas fijas, no.

El bind del botón es lo que más se pregunta: disableProperty() queda enlazada a una expresión que JavaFX recalcula cada vez que cambia cualquier campo, sin listeners a mano. La validación va en un método que no toca la interfaz, así que se puede probar sin abrir la ventana. Y el guion bajo de _Nombre crea el atajo Alt+N, mientras que setLabelFor indica a qué campo va el foco y qué etiqueta tiene para un lector de pantalla: dos líneas que cubren la accesibilidad del enunciado.

2. Tabla con TableView y ObservableList

Enunciado: tabla de alumnos con nombre y nota, campos para añadir uno nuevo, un botón para borrar el seleccionado (activo solo si hay selección) y el total de alumnos.

Primero, el modelo, con propiedades de JavaFX para que la tabla se entere de los cambios:

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Alumno {
    private final StringProperty nombre = new SimpleStringProperty();
    private final DoubleProperty nota = new SimpleDoubleProperty();

    public Alumno(String nombre, double nota) {
        this.nombre.set(nombre);
        this.nota.set(nota);
    }

    public String getNombre() { return nombre.get(); }
    public void setNombre(String valor) { nombre.set(valor); }
    public StringProperty nombreProperty() { return nombre; }

    public double getNota() { return nota.get(); }
    public void setNota(double valor) { nota.set(valor); }
    public DoubleProperty notaProperty() { return nota; }
}
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class TablaAlumnos extends Application {

    private final ObservableList<Alumno> alumnos = FXCollections.observableArrayList(
            new Alumno("Ana", 7.5),
            new Alumno("Luis", 4.25),
            new Alumno("Marta", 9.0));

    @Override
    public void start(Stage stage) {
        TableView<Alumno> tabla = new TableView<>(alumnos);

        TableColumn<Alumno, String> colNombre = new TableColumn<>("Nombre");
        colNombre.setCellValueFactory(fila -> fila.getValue().nombreProperty());

        TableColumn<Alumno, Number> colNota = new TableColumn<>("Nota");
        colNota.setCellValueFactory(fila -> fila.getValue().notaProperty());

        tabla.getColumns().add(colNombre);
        tabla.getColumns().add(colNota);

        TextField txtNombre = new TextField();
        txtNombre.setPromptText("Nombre");
        TextField txtNota = new TextField();
        txtNota.setPromptText("Nota");
        Label lblMensaje = new Label();

        Button btnAnadir = new Button("Añadir");
        btnAnadir.setOnAction(e -> {
            try {
                double nota = Double.parseDouble(txtNota.getText().trim().replace(',', '.'));
                if (txtNombre.getText().isBlank() || nota < 0 || nota > 10) {
                    lblMensaje.setText("Nombre obligatorio y nota entre 0 y 10");
                    return;
                }
                alumnos.add(new Alumno(txtNombre.getText().trim(), nota)); // la tabla se actualiza sola
                txtNombre.clear();
                txtNota.clear();
                lblMensaje.setText("");
            } catch (NumberFormatException ex) {
                lblMensaje.setText("La nota tiene que ser un número");
            }
        });

        Button btnBorrar = new Button("Borrar seleccionado");
        btnBorrar.disableProperty().bind(
                tabla.getSelectionModel().selectedItemProperty().isNull());
        btnBorrar.setOnAction(e -> alumnos.remove(tabla.getSelectionModel().getSelectedItem()));

        Label lblTotal = new Label();
        lblTotal.textProperty().bind(Bindings.size(alumnos).asString("Total: %d alumnos"));

        HBox formulario = new HBox(10, txtNombre, txtNota, btnAnadir, btnBorrar);
        VBox raiz = new VBox(10, tabla, formulario, lblMensaje, lblTotal);
        raiz.setPadding(new Insets(15));

        stage.setTitle("Alumnos");
        stage.setScene(new Scene(raiz, 560, 400));
        stage.show();
    }

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

Lo que hay que entender: la tabla no guarda los datos, observa una ObservableList. Por eso alumnos.add(...) o alumnos.remove(...) repintan la tabla solos, sin refresh(). Con un ArrayList normal, la tabla no se entera de nada.

setCellValueFactory indica qué propiedad muestra cada columna. La trampa está en el tipo: DoubleProperty es un observable de Number, no de Double, y por eso la columna es TableColumn<Alumno, Number> (o usas notaProperty().asObject()). Muchos apuntes usan new PropertyValueFactory<>("nota"), que busca el método por su nombre al ejecutar: si te equivocas en una letra, la columna sale vacía sin ningún error. Con la lambda, el fallo aparece al compilar.

3. Vista en FXML con su controlador

Enunciado: crea una pantalla de acceso con usuario y contraseña diseñada en FXML. El botón Entrar solo se activa con los dos campos rellenos. Si los datos son incorrectos, muestra un mensaje; al tercer fallo, bloquea el botón.

La vista, que puedes generar con Scene Builder, va en src/main/resources/es/instituto/di/login.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>

<VBox xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="es.instituto.di.LoginController" spacing="10">
    <padding>
        <Insets top="20" right="20" bottom="20" left="20"/>
    </padding>
    <Label text="Usuario"/>
    <TextField fx:id="txtUsuario"/>
    <Label text="Contraseña"/>
    <PasswordField fx:id="txtClave"/>
    <Button fx:id="btnEntrar" text="Entrar" defaultButton="true" onAction="#entrar"/>
    <Label fx:id="lblMensaje"/>
</VBox>

El controlador:

package es.instituto.di;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;

public class LoginController {

    // Los nombres tienen que coincidir EXACTAMENTE con los fx:id del FXML
    @FXML private TextField txtUsuario;
    @FXML private PasswordField txtClave;
    @FXML private Button btnEntrar;
    @FXML private Label lblMensaje;

    private int intentos = 0;

    // Se ejecuta solo, después de inyectar los @FXML. Aquí ya no son null.
    @FXML
    private void initialize() {
        btnEntrar.disableProperty().bind(
                txtUsuario.textProperty().isEmpty().or(txtClave.textProperty().isEmpty()));
    }

    // El nombre coincide con onAction="#entrar"
    @FXML
    private void entrar(ActionEvent evento) {
        if (comprobar(txtUsuario.getText(), txtClave.getText())) {
            lblMensaje.setText("Bienvenido, " + txtUsuario.getText());
        } else {
            intentos++;
            lblMensaje.setText("Usuario o contraseña incorrectos (" + intentos + "/3)");
            txtClave.clear();
            if (intentos >= 3) {
                btnEntrar.disableProperty().unbind();
                btnEntrar.setDisable(true);
            }
        }
    }

    private boolean comprobar(String usuario, String clave) {
        // En un proyecto real, esto se consulta en la base de datos
        return usuario.equals("admin") && clave.equals("1234");
    }
}

La clase que arranca la aplicación y el module-info.java:

package es.instituto.di;

import java.io.IOException;
import java.net.URL;
import java.util.Objects;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class App extends Application {

    @Override
    public void start(Stage stage) throws IOException {
        URL vista = Objects.requireNonNull(
                getClass().getResource("/es/instituto/di/login.fxml"),
                "No encuentro login.fxml: revisa la ruta");
        Parent raiz = FXMLLoader.load(vista);
        stage.setTitle("Acceso");
        stage.setScene(new Scene(raiz));
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
module es.instituto.di {
    requires javafx.controls;
    requires javafx.fxml;

    opens es.instituto.di to javafx.fxml;  // deja a FXMLLoader rellenar los @FXML privados
    exports es.instituto.di;                // deja a JavaFX crear la clase App
}

Lo que hay que entender: FXMLLoader crea los controles, crea el controlador de fx:controller, rellena cada atributo @FXML cuyo nombre coincide con un fx:id y, por último, llama a initialize(). Ese orden explica dos errores clásicos: en el constructor del controlador los controles todavía son null, y si un fx:id no coincide con el atributo, ese atributo se queda a null sin avisar y el NullPointerException salta más tarde.

@FXML permite que atributos y métodos sean private, y el opens del module-info.java deja a FXMLLoader acceder a ellos; sin él, la carga falla. El requireNonNull cambia el críptico Location is required por un mensaje que dice qué fichero falta.

En la práctica final, el usuario se comprobará contra una base de datos, como se ve en Acceso a Datos, con JDBC e Hibernate: el controlador llama a una clase DAO y no lleva SQL dentro.

4. Un componente propio reutilizable

Enunciado: crea un componente CampoValidado (etiqueta, campo de texto y mensaje de error) que reciba una regla de validación y exponga una propiedad valido observable. Úsalo en un formulario con email y teléfono.

package es.instituto.di;

import java.util.function.Predicate;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;

public class CampoValidado extends VBox {

    private final Label etiqueta = new Label();
    private final TextField campo = new TextField();
    private final Label error = new Label();
    private final ReadOnlyBooleanWrapper valido = new ReadOnlyBooleanWrapper(false);

    private Predicate<String> validador = texto -> !texto.isBlank();
    private String mensajeError = "Campo obligatorio";

    // Constructor sin argumentos: lo necesitan FXML y Scene Builder
    public CampoValidado() {
        setSpacing(4);
        error.setStyle("-fx-text-fill: #b00020;");
        etiqueta.setLabelFor(campo);
        getChildren().addAll(etiqueta, campo, error);
        campo.textProperty().addListener((obs, anterior, nuevo) -> comprobar());
        comprobar();
    }

    // Constructor cómodo para usarlo desde código
    public CampoValidado(String textoEtiqueta, Predicate<String> validador, String mensajeError) {
        this();
        setEtiqueta(textoEtiqueta);
        setValidador(validador, mensajeError);
    }

    private void comprobar() {
        boolean ok = validador.test(campo.getText());
        valido.set(ok);
        // Mientras el campo está vacío no se muestra el error: aún no ha escrito nada
        error.setText(ok || campo.getText().isEmpty() ? "" : mensajeError);
    }

    public String getEtiqueta() { return etiqueta.getText(); }

    public void setEtiqueta(String texto) {
        etiqueta.setText(texto);
        campo.setAccessibleText(texto); // lo lee el lector de pantalla
    }

    public void setValidador(Predicate<String> validador, String mensajeError) {
        this.validador = validador;
        this.mensajeError = mensajeError;
        comprobar();
    }

    public String getTexto() { return campo.getText(); }

    public boolean isValido() { return valido.get(); }

    // Desde fuera se puede observar, pero no modificar
    public ReadOnlyBooleanProperty validoProperty() { return valido.getReadOnlyProperty(); }
}

Usarlo en una ventana es como usar cualquier control de JavaFX:

package es.instituto.di;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class AltaContacto extends Application {

    @Override
    public void start(Stage stage) {
        CampoValidado email = new CampoValidado("Email",
                t -> t.trim().matches("[^@\\s]+@[^@\\s]+\\.[a-zA-Z]{2,}"), "Email no válido");
        CampoValidado telefono = new CampoValidado("Teléfono",
                t -> t.trim().matches("\\d{9}"), "Tienen que ser 9 cifras");

        Button btnGuardar = new Button("Guardar");
        btnGuardar.disableProperty().bind(
                email.validoProperty().and(telefono.validoProperty()).not());
        btnGuardar.setOnAction(e -> System.out.println(email.getTexto() + " " + telefono.getTexto()));

        VBox raiz = new VBox(12, email, telefono, btnGuardar);
        raiz.setPadding(new Insets(20));
        stage.setScene(new Scene(raiz, 320, 240));
        stage.setTitle("Nuevo contacto");
        stage.show();
    }

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

Y también desde FXML, porque tiene constructor sin argumentos y la pareja getEtiqueta/setEtiqueta:

<?import es.instituto.di.CampoValidado?>
<!-- ... -->
<CampoValidado fx:id="campoNombre" etiqueta="Nombre"/>

Lo que hay que entender: un componente propio es una clase que hereda de un nodo de JavaFX (aquí VBox) y encapsula los controles que lo forman. Si eso te suena raro, repasa la programación orientada a objetos explicada desde cero: este ejercicio es POO aplicada.

Lo que se evalúa es la interfaz pública. valido se expone como ReadOnlyBooleanProperty: el formulario puede enlazarse a ella, pero no cambiarla. El constructor sin argumentos y los get/set con la convención de nombres permiten usarlo en FXML con etiqueta="Nombre". Para verlo en la paleta de Scene Builder, se importa el .jar del componente.

5. Cargar datos sin congelar la ventana

Enunciado: un botón carga diez alumnos desde una fuente lenta. La ventana sigue respondiendo, una barra muestra el progreso y el botón queda desactivado hasta que termine.

import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class CargaAlumnos extends Application {

    @Override
    public void start(Stage stage) {
        Button btnCargar = new Button("Cargar alumnos");
        ProgressBar barra = new ProgressBar(0);
        Label lblEstado = new Label();
        ListView<String> lista = new ListView<>();

        btnCargar.setOnAction(e -> {
            Task<List<String>> tarea = new Task<>() {
                @Override
                protected List<String> call() throws Exception {
                    // Esto se ejecuta en otro hilo: aquí NO se toca la interfaz
                    List<String> resultado = new ArrayList<>();
                    for (int i = 1; i <= 10; i++) {
                        Thread.sleep(300); // simula una consulta lenta
                        resultado.add("Alumno " + i);
                        updateProgress(i, 10);
                        updateMessage("Cargando " + i + " de 10");
                    }
                    return resultado;
                }
            };

            barra.progressProperty().bind(tarea.progressProperty());
            lblEstado.textProperty().bind(tarea.messageProperty());
            btnCargar.disableProperty().bind(tarea.runningProperty());

            // setOnSucceeded y setOnFailed se ejecutan ya en el hilo de JavaFX
            tarea.setOnSucceeded(ev -> lista.getItems().setAll(tarea.getValue()));
            tarea.setOnFailed(ev -> {
                lblEstado.textProperty().unbind(); // una propiedad enlazada no admite set
                lblEstado.setText("Error: " + tarea.getException().getMessage());
            });

            Thread hilo = new Thread(tarea);
            hilo.setDaemon(true); // no impide cerrar la aplicación
            hilo.start();
        });

        VBox raiz = new VBox(10, btnCargar, barra, lblEstado, lista);
        raiz.setPadding(new Insets(15));
        stage.setScene(new Scene(raiz, 320, 400));
        stage.setTitle("Carga de alumnos");
        stage.show();
    }

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

Lo que hay que entender: JavaFX dibuja la ventana y atiende los clics en un único hilo, el hilo de aplicación de JavaFX. Si haces algo lento dentro de un evento, la ventana se congela, así que el trabajo lento va a otro hilo. Pero la interfaz solo se puede modificar desde el hilo de JavaFX: si tocas un control desde otro hilo, unas veces salta IllegalStateException: Not on FX application thread y otras falla de forma aleatoria.

Task resuelve las dos cosas: call() corre en segundo plano, y updateProgress, updateMessage y setOnSucceeded llevan los resultados al hilo de JavaFX por ti. Con un Thread normal, lo haces a mano con Platform.runLater(() -> lista.getItems().add(nombre)), que encola ese código para el hilo de JavaFX. Los hilos se estudian a fondo en PSP, con ejercicios de hilos y sockets resueltos.

Informes, ayuda, instaladores y pruebas

Se dejan para el final del curso, pero cuentan en la nota y suelen pedirse como entregables.

Informes. Lo habitual es diseñar el informe en Jaspersoft Studio (un .jrxml con campos, parámetros, grupos y totales) y rellenarlo desde Java:

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;

public class InformeAlumnos {

    public static void generarPdf(List<Alumno> alumnos) throws JRException {
        // Diseño hecho en Jaspersoft Studio
        JasperReport informe = JasperCompileManager.compileReport("informes/alumnos.jrxml");

        Map<String, Object> parametros = new HashMap<>();
        parametros.put("TITULO", "Alumnos de 2º DAM");

        // Cada $F{nombre} del informe se lee con getNombre() de cada Alumno
        JasperPrint relleno = JasperFillManager.fillReport(
                informe, parametros, new JRBeanCollectionDataSource(alumnos));

        JasperExportManager.exportReportToPdfFile(relleno, "alumnos.pdf");
    }
}

Los campos del informe se llaman igual que los atributos del objeto ($F{nombre} busca getNombre()), y los parámetros igual que las claves del mapa. En muchos centros el informe se rellena con una conexión JDBC y una consulta SQL dentro del .jrxml. Ojo con las versiones: desde JasperReports 7, exportar a PDF necesita además la dependencia jasperreports-pdf, y el formato del .jrxml cambió. Usa la misma versión en Jaspersoft Studio y en el proyecto.

Usabilidad y accesibilidad. Coherencia entre ventanas, mensajes de error que dicen qué hacer, contrastes legibles y que todo se pueda hacer con el teclado. Los setLabelFor, setAccessibleText y atajos de los ejercicios 1 y 4 van por aquí.

Ayuda y documentación. Manuales de usuario e instalación y ayuda dentro de la aplicación (tooltips, una ventana de ayuda).

Distribución. El JDK incluye jpackage, que crea un instalador nativo (.msi o .exe en Windows, .dmg o .pkg en macOS, .deb o .rpm en Linux) con su propio Java dentro, pero solo para el sistema en el que lo ejecutas. Algunos centros usan otras herramientas, como Inno Setup.

Pruebas. La lógica separada de la interfaz (como validar del ejercicio 1) se prueba con JUnit, que se aprende en Entornos de Desarrollo, y la interfaz con TestFX, que simula clics y escritura. Casi siempre piden también un plan de pruebas con usuarios.

Los errores que más suspenden en Desarrollo de Interfaces

NullPointerException en un control del FXML. Casi siempre es un fx:id que no coincide exactamente con el atributo (mayúsculas incluidas), un atributo sin @FXML o un control usado en el constructor del controlador en lugar de en initialize(). También pasa si creas tú el controlador con new: ese objeto no lo ha rellenado nadie. Pídeselo al FXMLLoader con getController().

Controlador mal referenciado. fx:controller lleva el nombre completo de la clase, con su paquete, y no se actualiza solo si la mueves. Un onAction="#guardar" sin método guardar da Error resolving onAction, y si el FXML no tiene fx:controller, No controller specified.

Location is required (o Location is not set). getResource ha devuelto null: la ruta del .fxml está mal o el fichero no está donde Java lo busca. En Maven, los .fxml van en src/main/resources, con la misma estructura de carpetas que el paquete.

Tocar la interfaz desde otro hilo. Cargar datos dentro de un evento congela la ventana, y actualizar controles desde otro hilo provoca errores aleatorios. Usa Task o Platform.runLater.

JavaFX runtime components are missing. Desde Java 11, JavaFX no viene en el JDK y hay que añadirlo como librería. Con Maven, usa las dependencias de org.openjfx y arranca con mvn javafx:run. Sin Maven, añade en las opciones de la máquina virtual --module-path con la carpeta lib del SDK de JavaFX y --add-modules javafx.controls,javafx.fxml.

Usar listas normales. FXCollections.observableArrayList(miLista) crea una copia: si después añades a miLista, la tabla no cambia. Trabaja siempre sobre la ObservableList de la tabla.

Posiciones absolutas. Scene Builder propone un AnchorPane y te deja arrastrar controles a coordenadas fijas. Queda bien en tu pantalla y se rompe al redimensionar. Construye anidando VBox, HBox, GridPane y BorderPane.

Ignorar la accesibilidad. Ventanas que no se usan sin ratón, errores marcados solo en rojo, campos sin etiqueta asociada. Es un resultado de aprendizaje y se evalúa: prueba tu aplicación solo con Tab, Enter y los atajos.

Meter toda la lógica en el controlador. Validaciones, SQL y cálculos dentro del botón: funciona, pero no se puede probar y rompe el MVC.

Cómo prepararlo

Asegura la base de Java y de POO. DI da por hecho que manejas herencia, interfaces, lambdas y excepciones. Si no, repasa los errores de Java más comunes en DAM y DAW antes de JavaFX.

Deja el entorno funcionando antes del examen. Un proyecto Maven con JavaFX que arranque, Scene Builder enlazado con el IDE y un FXML que cargue. Perder media hora del examen con la configuración pasa más de lo que parece.

Haz cada ejercicio de las dos formas. El formulario del ejercicio 1 en código y en FXML con controlador. Así entiendes qué hace FXMLLoader y te sirve para la pregunta sobre MVC.

Aprende los bindings. bind, isEmpty(), or, not, Bindings.size. Te ahorran la mitad de los listeners.

No dejes informes e instalador para la última semana. JasperReports y jpackage dan problemas de versiones y rutas que no tienen que ver con tu código. Pruébalos con un ejemplo pequeño a mitad de curso.

Lee el error desde la causa. Baja hasta el Caused by y busca la primera línea que mencione una clase tuya. Si te pierdes, sigue este método para depurar paso a paso.

Todo esto lo reutilizas en el proyecto final de DAM y en Programación Multimedia y Dispositivos Móviles, donde las interfaces Android siguen las mismas ideas.

Cuándo pedir ayuda

Si haces los ejercicios y entiendes por qué salen los errores, vas bien. Merece la pena buscar ayuda cuando:

  • Tu proyecto no arranca fuera del ordenador de clase y llevas días con módulos y opciones de la máquina virtual.
  • El ejemplo de FXML de los apuntes funciona, pero al añadir una pantalla nueva todo da NullPointerException.
  • Toda la lógica está dentro de los botones y no sabes cómo separarla.
  • Tienes Desarrollo de Interfaces pendiente y lo necesitas para las prácticas en empresa o el proyecto.

En nuestras clases de refuerzo para DAM y DAW trabajamos con tus enunciados y en tu propio proyecto: buscamos juntos dónde se rompe y lo arreglas tú, para que en el examen no dependas de nadie. Si falla la base, empezamos por ahí con clases de Java online, o de C# online si tu centro usa C#. Son clases online uno a uno a 25 € la hora, sin matrícula ni permanencia.

Preguntas frecuentes

¿Qué se da en Desarrollo de Interfaces en 2º de DAM?

Desarrollo de Interfaces es el módulo 0488 de segundo de DAM. Incluye crear interfaces gráficas con editores visuales y con XML (por ejemplo FXML), crear componentes visuales propios, aplicar criterios de usabilidad, generar informes, documentar aplicaciones, prepararlas para su distribución y evaluarlas con pruebas.

¿Es difícil Desarrollo de Interfaces?

Suele costar menos que Acceso a Datos o PSP, porque la lógica es sencilla y ves el resultado en pantalla. Lo que más bloquea es la configuración (JavaFX, módulos, rutas de los FXML). Con buena base de Java y POO, se aprueba practicando los ejercicios tipo.

¿Desarrollo de Interfaces se da en JavaFX, Swing o C#?

Depende del centro. Lo más habitual es Java con JavaFX y Scene Builder, porque continúa el Java de primero. En materiales antiguos aparece Swing, y algunos centros usan C# con WPF o Windows Forms, o Kotlin con Compose. Los conceptos (layouts, eventos, enlace de datos, separar vista y lógica) son los mismos.

¿Qué es Scene Builder y es obligatorio usarlo?

Scene Builder es un editor visual gratuito que genera ficheros FXML arrastrando controles y se enlaza con IntelliJ, Eclipse o NetBeans. No es imprescindible, porque el FXML se puede escribir a mano, pero el módulo pide trabajar con un editor visual y es el más usado con JavaFX.

¿Cómo es el examen de Desarrollo de Interfaces?

Lo más habitual es un examen práctico en el ordenador: construir una o varias ventanas con formularios, tablas y validaciones a partir de un enunciado. Muchos centros evalúan además entregables como un informe, un instalador o un plan de pruebas.

¿Qué diferencia hay entre Desarrollo de Interfaces de DAM y Diseño de Interfaces Web de DAW?

Desarrollo de Interfaces, de DAM, trata aplicaciones de escritorio (y a veces móviles) programadas en Java, C# o Kotlin. Diseño de Interfaces Web, de DAW, trata páginas web con HTML, CSS y diseño adaptable. Comparten ideas de usabilidad y accesibilidad, pero las tecnologías son distintas.

WhatsApp