🎯 Práctica Guiada

Gestor de Datos Demográficos con JavaFX (Patrón MVC)

📋 Introducción

En esta práctica vas a desarrollar una aplicación JavaFX que gestiona datos demográficos de personas. La aplicación permitirá añadir personas con su nombre, edad y localidad, y visualizar estadísticas mediante gráficos de barras.

⚠️ Arquitectura: Esta aplicación sigue el patrón MVC (Modelo-Vista-Controlador) con FXML para las interfaces y CSS para los estilos.
📁 Estructura del proyecto:
src/dam/
├── Main.java (clase principal)
├── model/
│ ├── Persona.java
│ └── GestorDatosModel.java
├── controller/
│ ├── RootController.java
│ ├── DatosController.java
│ └── GraficaController.java
└── vista/
  ├── root.fxml
  ├── root.css
  ├── Datos.fxml
  ├── datos.css
  ├── grafica.fxml
  └── grafica.css

1 Capa Vista (FXML)

📄 root.fxml

Contenedor principal con menú y pestañas

  • Elemento raíz: BorderPane con styleClass="root" y enlaza root.css
  • Asocia el controlador: RootController
  • En la región top: coloca un MenuBar
  • Dentro del MenuBar crea un Menu "Archivo"
  • Dentro del menú añade un MenuItem "Cerrar" que ejecute el método cerrarApp()
  • En la región center: coloca un TabPane con fx:id="tabPane"
  • Dentro del TabPane crea dos pestañas:
    • Tab "Datos" (fx:id="tabDatos", closable=false) con un AnchorPane vacío
    • Tab "Gráfica" (fx:id="tabGrafica") con un AnchorPane vacío
💡 Los AnchorPane están vacíos porque se llenarán dinámicamente desde el controlador.

📄 Datos.fxml

Formulario para dar de alta personas

  • Elemento raíz: VBox con spacing=12, styleClass="panel" y enlaza datos.css
  • Asocia el controlador: DatosController
  • Añade padding de 12 en todos los lados
  • Dentro del VBox:
    • Un Label con styleClass="title" y texto "Alta de personas"
    • Un GridPane (hgap=10, vgap=10) con:
      • Fila 0: Label "Nombre:" y TextField (fx:id="txtNombre", promptText="Introduce el nombre")
      • Fila 1: Label "Localidad:" y ChoiceBox (fx:id="chLocalidad")
      • Fila 2: Label "Edad:" y TextField (fx:id="txtEdad", promptText="Número")
      • Fila 3: HBox con spacing=10, alignment=CENTER, y columnSpan=2 conteniendo:
        • Button "Añadir" (styleClass="btn-primary", onAction="#addPersona")
        • Button "Vaciar" (styleClass="btn-danger", onAction="#vaciar")

📄 grafica.fxml

Visualización de gráficos estadísticos

  • Elemento raíz: VBox con spacing=12, styleClass="panel" y enlaza grafica.css
  • Asocia el controlador: GraficaController
  • Añade padding de 12 en todos los lados
  • Dentro del VBox:
    • Un Label con styleClass="title" y texto "Gráfico"
    • Un Label explicativo con wrapText=true y texto "Selecciona los datos a mostrar en el gráfico"
    • Un HBox (alignment=CENTER_LEFT, spacing=10) con dos RadioButtons:
      • RadioButton "Edad" (fx:id="rbEdad", onAction="#eventMostrarEdad")
      • RadioButton "Localidad" (fx:id="rbLocalidad", onAction="#eventMostrarLocalidad")
    • Un Separator
    • Un BarChart (fx:id="grafico", title="Número de personas por localidad/edad") con:
      • xAxis: CategoryAxis (fx:id="xAxis", side="BOTTOM")
      • yAxis: NumberAxis (fx:id="yAxis", side="LEFT")

2 Clase Principal

📄 UD03T02_GestorDatosDemográficos.java

package dam;

Objetivo: Punto de entrada de la aplicación JavaFX.

  • La clase debe extender Application de JavaFX
  • Método estático que recibe String[] args
  • Dentro del método, simplemente llama a launch(args)
💡 El método launch() arranca el ciclo de vida de JavaFX y llama al método start().

Este método se ejecuta cuando JavaFX está listo. Debe:

  • Crear un FXMLLoader apuntando a "/dam/vista/root.fxml"
  • Crear una Scene cargando el FXML con loader.load(), estableciendo dimensiones de 1000x680
  • Establecer el título de la ventana
  • Asignar la escena al stage
  • Mostrar la ventana
⚠️ No olvides declarar que el método lanza Exception.

3 Estilos CSS

🎨 root.css

Estilos para el contenedor principal y navegación

Tipografía: "Segoe UI", Arial, sans-serif
Color de fondo: #f8f9fa (gris muy claro)

Para .menu-bar:

Color de fondo: #ffffff (blanco)
Color del borde: #dee2e6
Ancho del borde: 0 0 1 0 (solo inferior)

Para .menu-bar .label:

Color del texto: #212529 (negro suave)
Tamaño de fuente: 13px

Para .menu-item .label:

Color del texto: #212529

Para .tab-pane:

Color de fondo: transparent

Para .tab-pane .tab-header-area:

Color de fondo: #ffffff
Color del borde: #dee2e6
Ancho del borde: 0 0 1 0

Para .tab-pane .tab-header-area .tab:

Color de fondo: transparent
Padding: 8 14 8 14

Para .tab-pane .tab-header-area .tab:selected:

Color de fondo: #e9ecef (gris claro)
Radio del borde: 8 8 0 0 (redondeado arriba)

Para .tab-pane .tab-label:

Color del texto: #212529
Peso de fuente: bold

Para .tab-pane .tab-content-area:

Color de fondo: transparent
Padding: 16

🎨 datos.css

Estilos estilo Bootstrap para el formulario

Color de fondo: #ffffff
Radio del fondo: 12px
Radio del borde: 12px
Color del borde: #dee2e6
Padding: 16px
Efecto: dropshadow (tipo: gaussian, color: rgba(0,0,0,0.08), radio: 18, spread: 0, offsetX: 0, offsetY: 4)
Tamaño de fuente: 20px
Peso de fuente: bold
Color del texto: #212529
Color del texto: #495057
Peso de fuente: bold

Estado normal (.text-field):

Color de fondo: #ffffff
Color del borde: #ced4da
Radio del borde: 8px
Radio del fondo: 8px
Padding: 8px 10px 8px 10px
Color del texto del prompt: #adb5bd

Estado enfocado (.text-field:focused):

Color del borde: #86b7fe (azul claro)
Efecto: dropshadow (gaussian, rgba(13,110,253,0.20), 18, 0, 0, 0)

Estado normal (.choice-box):

Color de fondo: #ffffff
Color del borde: #ced4da
Radio del borde: 8px
Radio del fondo: 8px
Padding: 2px 6px 2px 6px

Estado enfocado (.choice-box:focused):

Color del borde: #86b7fe
Efecto: dropshadow (gaussian, rgba(13,110,253,0.20), 18, 0, 0, 0)

Estado normal:

Color de fondo: #0d6efd (azul Bootstrap)
Color del texto: white
Peso de fuente: bold
Radio del fondo: 10px
Padding: 8px 16px 8px 16px
Cursor: hand

Estado hover (.btn-primary:hover):

Color de fondo: #0b5ed7

Estado pressed (.btn-primary:pressed):

Color de fondo: #0a58ca

Estado normal:

Color de fondo: #dc3545 (rojo Bootstrap)
Color del texto: white
Peso de fuente: bold
Radio del fondo: 10px
Padding: 8px 16px 8px 16px
Cursor: hand

Estado hover (.btn-danger:hover):

Color de fondo: #bb2d3b

Estado pressed (.btn-danger:pressed):

Color de fondo: #b02a37

🎨 grafica.css

Estilos Bootstrap-like para gráficos y controles

Color de fondo: #ffffff
Radio del fondo: 12px
Radio del borde: 12px
Color del borde: #dee2e6
Padding: 16px
Efecto: dropshadow (gaussian, rgba(0,0,0,0.08), 18, 0, 0, 4)
Tamaño de fuente: 20px
Peso de fuente: bold
Color del texto: #212529
Color del texto: #495057

Para .radio-button:

Color del texto: #212529
Peso de fuente: bold

Para .radio-button:selected .radio .dot:

Color de fondo: #0d6efd (azul Bootstrap)

Para .chart:

Color de fondo: transparent

Para .chart-title:

Color del texto: #212529
Tamaño de fuente: 14px
Peso de fuente: bold

Para .chart-plot-background:

Color de fondo: #f8f9fa
Radio del fondo: 10px

Para .axis-label:

Color del texto: #495057
Peso de fuente: bold

Para .axis-tick-label:

Color del texto: #495057

Para .chart-bar:

Color de relleno de la barra: #0d6efd
Radio del fondo: 6 6 0 0 (redondeado solo arriba)

4 Capa Controlador

📄 RootController.java

package dam.controller;

Objetivo: Coordinar la aplicación principal y cargar dinámicamente las vistas.

  • TabPane tabPane - referencia al contenedor de pestañas
  • Tab tabDatos - referencia a la pestaña de datos
  • Tab tabGrafica - referencia a la pestaña de gráfica
  • Crea una instancia final de GestorDatosModel llamada "modelo"
💡 Este modelo se compartirá entre todos los controladores hijos.

Este método se ejecuta automáticamente al cargar el FXML. Aquí debes:

1. Cargar la vista Datos.fxml:

  • Crea un FXMLLoader apuntando a "/dam/vista/Datos.fxml"
  • Carga el layout con load() en una variable Parent
  • Obtén el controlador con getController() (tipo DatosController) (DatosController datosController = loaderDatos.getController();)
  • Llama a setModelo(modelo) en el controlador (datosController.setModelo(modelo);)
  • Asigna el layout como contenido de tabDatos

2. Cargar la vista grafica.fxml:

  • Crea otro FXMLLoader apuntando a "/dam/vista/grafica.fxml"
  • Carga el layout con load()
  • Obtén el controlador (tipo GraficaController)
  • Llama a setModelo(modelo) en el controlador
  • Asigna el layout como contenido de tabGrafica

3. Manejo de excepciones:

  • Envuelve todo en un bloque try-catch para IOException

Este método se ejecuta al hacer clic en "Cerrar" del menú:

  • Crea un Alert de tipo CONFIRMATION
  • Establece el título "Salir"
  • Establece el texto del encabezado "¿Deseas salir de la aplicación?"
  • Establece el contenido "Se perderán los datos no guardados."
  • Muestra el diálogo con showAndWait() y captura el resultado (Optional<ButtonType> result = alert.showAndWait();)
  • Si el usuario pulsa OK, ejecuta Platform.exit()

📄 DatosController.java

package dam.controller;

Objetivo: Gestionar el formulario de alta de personas.

  • ChoiceBox<String> chLocalidad - selector de localidad
  • TextField txtEdad - campo para la edad
  • TextField txtNombre - campo para el nombre
  • GestorDatosModel modelo - referencia al modelo (sin inicializar)

Puede estar vacío o puedes dejarlo sin implementación relevante (se llama automáticamente).

Llamado desde RootController después de cargar el FXML:

  • Asigna el parámetro al atributo this.modelo
  • Vincula las localidades que se encuentran en el modelo (en un ObservableList) al ChoiceBox

Se ejecuta al pulsar el botón "Añadir":

1. Obtener y validar datos:

  • Obtén el texto de txtNombre (si es null conviértelo a "", luego aplica trim())
  • Obtén el texto de txtEdad (si es null conviértelo a "", luego aplica trim())
  • Obtén el valor seleccionado de chLocalidad

2. Validaciones (si fallan, llama a aviso() y termina):

  • Si nombre está vacío → aviso("Nombre vacío", "Introduce un nombre.")
  • Si localidad es null o está en blanco → aviso("Localidad no seleccionada", "Selecciona una localidad.")
  • Intenta convertir edadTxt a int, si falla (NumberFormatException) → aviso("Edad inválida", "La edad debe ser un número entero.")
  • Si edad < 0 o edad > 120 → aviso("Edad fuera de rango", "Introduce una edad entre 0 y 120.")

3. Guardar en modelo:

  • Llama a al método del modelo que te permita crear una nueva persona

4. Limpiar formulario:

  • Limpia txtNombre
  • Limpia txtEdad
  • Deselecciona el ChoiceBox
  • Pone el foco en txtNombre

Se ejecuta al pulsar el botón "Vaciar":

  • Limpia txtNombre
  • Limpia txtEdad
  • Deselecciona el chLocalidad

Método auxiliar para mostrar alertas:

  • Crea un Alert de tipo WARNING
  • Establece el título recibido
  • Establece headerText a null
  • Establece el contenido con el mensaje recibido
  • Muestra el diálogo con showAndWait()

📄 GraficaController.java

package dam.controller;

Objetivo: Gestionar la visualización de gráficos estadísticos.

  • BarChart<String, Number> grafico - el gráfico de barras
  • RadioButton rbEdad - opción para mostrar por edad
  • RadioButton rbLocalidad - opción para mostrar por localidad
  • NumberAxis yAxis - eje Y (numérico)
  • CategoryAxis xAxis - eje X (categorías)
  • GestorDatosModel modelo - referencia al modelo
  • ToggleGroup grupo (final) - inicializado con new ToggleGroup()

Se ejecuta automáticamente al cargar el FXML:

  • Asigna rbEdad al ToggleGroup
  • Asigna rbLocalidad al ToggleGroup
  • Limpia el gráfico con grafico.getData().clear()
💡 El ToggleGroup asegura que solo un RadioButton esté seleccionado a la vez.
  • Asigna el parámetro al atributo this.modelo

Se ejecuta al seleccionar el RadioButton "Edad":

  • Llama a actualizarGrafico()

Se ejecuta al seleccionar el RadioButton "Localidad":

  • Llama a actualizarGrafico()

Actualiza el contenido del gráfico según la opción seleccionada:

1. Validaciones iniciales:

  • Si modelo es null, termina (return)
  • Limpia el gráfico
  • Si no hay selección en el grupo termina

2. Si está seleccionado rbLocalidad:

  • Añade al gráfico los datos con grafico.getData().addAll(modelo.getPersonasLocalidad())
  • Establece el título del gráfico: "Número de personas por localidad"
  • Establece la etiqueta del yAxis: "Localidad"
  • Establece la etiqueta del xAxis: "Nº personas"

3. Si está seleccionado rbEdad:

  • Añade al gráfico los datos con grafico.getData().addAll(modelo.getPersonasEdad())
  • Establece el título del gráfico: "Número de personas por edad"
  • Establece la etiqueta del yAxis: "Edad"
  • Establece la etiqueta del xAxis: "Nº personas"

5 Capa Modelo

📄 Clase Persona.java

package dam.model;

Objetivo: Representar a una persona con sus datos básicos.

  • Crea tres atributos privados y finales: nombre (String), edad (int), localidad (String)
  • Implementa un constructor que reciba estos tres parámetros
  • Crea métodos getter para los tres atributos (getNombre, getEdad, getLocalidad)
💡 Los atributos son finales porque una vez creada la persona, sus datos no cambiarán.

📄 Clase GestorDatosModel.java

package dam.model;

Objetivo: Gestionar la lógica de negocio y los datos de la aplicación.

  • Crea una ObservableList de Persona llamada "personas" (inicialízala vacía)
  • Crea una ObservableList de String llamada "localidades" con las 4 provincias gallegas: A Coruña, Lugo, Ourense, Pontevedra
  • Define una enumeración GrupoEdad con 4 valores: MENORES_18, DE_18_A_30, DE_31_A_50, MAYORES_50
  • getPersonas(): devuelve la lista observable de personas
  • getLocalidades(): devuelve la lista observable de localidades
  • addPersona(String nombre, int edad, String localidad): añade una nueva persona a la lista

Este método clasifica a una persona según su edad:

  • Si edad < 18 → devuelve "MENORES_18"
  • Si edad entre 18 y 30 → devuelve "DE_18_A_30"
  • Si edad entre 31 y 50 → devuelve "DE_31_A_50"
  • Si edad > 50 → devuelve "MAYORES_50"

Genera datos para el gráfico de barras por grupos de edad:

  • Crea una XYChart.Series llamada "personaEdad"
  • Recorre todos los valores del enum GrupoEdad
  • Para cada grupo, cuenta cuántas personas pertenecen a ese rango (usa analizarRango)
  • Añade un XYChart.Data con el nombre del grupo y el contador
  • Asigna el nombre "Edad" a la serie
  • Devuelve una lista con esta única serie encapsulada dentro de un objeto de tipo Lista (List.of(personaEdad);)

Genera datos para el gráfico de barras por localidades:

  • Crea una XYChart.Series llamada "personaLocalidad"
  • Recorre todas las localidades
  • Para cada localidad, cuenta cuántas personas son de esa localidad
  • Añade un XYChart.Data con el nombre de la localidad y el contador
  • Asigna el nombre "Localidad" a la serie
  • Devuelve una lista con esta única serie

6 Pruebas y Verificación

✅ Lista de verificación

  • ☑️ La aplicación inicia sin errores
  • ☑️ Se muestran las dos pestañas: "Datos" y "Gráfica"
  • ☑️ El menú "Archivo > Cerrar" funciona y muestra confirmación
  • ☑️ El ChoiceBox de localidades está cargado con las 4 provincias
  • ☑️ Se puede escribir un nombre
  • ☑️ Se puede seleccionar una localidad
  • ☑️ Se puede introducir una edad numérica
  • ☑️ El botón "Añadir" valida correctamente los campos
  • ☑️ Aparecen alertas cuando faltan datos o la edad es inválida
  • ☑️ El botón "Vaciar" limpia todos los campos
  • ☑️ Después de añadir, el formulario se limpia automáticamente
  • ☑️ Al inicio el gráfico está vacío
  • ☑️ Al seleccionar "Localidad" aparece el gráfico por localidades
  • ☑️ Al seleccionar "Edad" aparece el gráfico por grupos de edad
  • ☑️ Los datos del gráfico se actualizan al cambiar de opción
  • ☑️ Los ejes muestran las etiquetas correctas según la opción
  • ☑️ Los paneles tienen sombra y bordes redondeados
  • ☑️ Los botones tienen colores Bootstrap (azul y rojo)
  • ☑️ Los campos de texto muestran efecto de enfoque (borde azul)
  • ☑️ Las pestañas se resaltan cuando están seleccionadas
  • ☑️ El gráfico tiene fondo gris claro y barras azules

🧪 Casos de prueba sugeridos

  • Intenta añadir sin completar ningún campo → debe aparecer alerta "Nombre vacío"
  • Escribe un nombre pero no selecciones localidad → debe aparecer alerta "Localidad no seleccionada"
  • Introduce texto en edad → debe aparecer alerta "Edad inválida"
  • Introduce -5 en edad → debe aparecer alerta "Edad fuera de rango"
  • Introduce 150 en edad → debe aparecer alerta "Edad fuera de rango"
  • Añade una persona de 15 años de A Coruña
  • Añade una persona de 25 años de Lugo
  • Añade una persona de 40 años de Ourense
  • Añade una persona de 60 años de Pontevedra
  • Ve a la pestaña "Gráfica"
  • Selecciona "Localidad" → debe mostrar 1 persona por cada provincia
  • Selecciona "Edad" → debe mostrar 1 en MENORES_18, 1 en DE_18_A_30, 1 en DE_31_A_50, 1 en MAYORES_50

🎓 Conclusión

¡Enhorabuena! Has completado la implementación de una aplicación JavaFX completa usando el patrón MVC. Has aprendido a:

  • ✅ Separar la lógica de negocio (Modelo) de la presentación (Vista) y control (Controlador)
  • ✅ Trabajar con FXML para diseñar interfaces de usuario declarativas
  • ✅ Aplicar estilos CSS personalizados a componentes JavaFX
  • ✅ Utilizar ObservableList para mantener datos reactivos
  • ✅ Crear gráficos dinámicos con BarChart
  • ✅ Implementar validaciones y manejo de errores
  • ✅ Coordinar múltiples controladores desde un controlador principal