Objetivo
En esta práctica vamos a crear un conversor de unidades de millas ➡️ kilómetros.

Os dejo aquí el esqueleto de la aplicación
Este es el contenido de MainApp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package org.ieselcaminas.conversor;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class MainApp extends Application {
/**
* Punto de entrada REAL de JavaFX.
* Se llama automáticamente después de launch().
* Aquí se construye la ventana principal (Stage).
*
* @param stage El escenario (ventana) principal que nos proporciona JavaFX.
*/
@Override
public void start(Stage stage) {
// La ventana que vamos a mostrar
ConverterView view = new ConverterView();
// Cogemos del DOM la raíz para crear una escena
Scene scene = new Scene(view.getRoot(), 780, 680);
// El stage es la ventana
stage.setTitle("Conversor de Distancias");
stage.setScene(scene);
stage.setResizable(false);
stage.show();
}
/**
* Método llamado ANTES de start().
* Útil para inicializar recursos: conexión a BD, cargar configuración, etc.
*/
@Override
public void init() {
System.out.println("Aplicación iniciando...");
// Aquí podrías inicializar, por ejemplo:
// - Conexión a base de datos
// - Cargar un fichero de propiedades
// - Preparar un servicio singleton
}
/** Ciclo de vida que ejecuta JavaFX
main()
└─► launch()
├─► init() ← preparar recursos
├─► start() ← construir y mostrar la ventana ← aquí está todo
└─► stop() ← liberar recursos al cerrar
*/
/**
* Método llamado al CERRAR la aplicación.
* Ideal para liberar recursos: cerrar conexiones, guardar estado, etc.
*/
@Override
public void stop() {
System.out.println("Aplicación cerrando. Liberando recursos...");
// Aquí podrías:
// - Cerrar conexión a BD
// - Guardar preferencias del usuario
// - Detener hilos en segundo plano
}
/**
* Punto de entrada del programa (main).
* En JavaFX, main() simplemente llama a launch(),
* que es quien arranca el ciclo de vida de la aplicación.
*
* @param args Argumentos de línea de comandos (raramente usados en JavaFX).
*/
public static void main(String[] args) {
launch(args);
}
}
Creación del panel millas ➡️ kilómetros
Creamos primero la vista:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class ConverterView {
private static final int DECIMALS = 4;
private VBox root;
private TextField millasInput;
private Label kmResultLabel;
public ConverterView() {
// En el constructor se llama a buildUI() por convención
buildUI();
}
private void buildUI() {
// La pantalla principal será una caja vertical con un separación entre cajas de 20
root = new VBox(20);
// Le ponemos padding en el interior para que no se apelotonen los controles
root.setPadding(new Insets(32, 36, 32, 36));
// Y la ponemos en el centro
root.setAlignment(Pos.TOP_CENTER);
// ── Título ──────────────────────────────────────────────
Label titulo = new Label("Conversor de Distancias");
titulo.setFont(Font.font("SansSerif", FontWeight.BOLD, 26));
Label subtitulo = new Label("Millas ↔ Kilómetros");
VBox header = new VBox(4, titulo, subtitulo);
header.setAlignment(Pos.CENTER);
Separator sep1 = new Separator();
// ── Un panel ─────────────────────
VBox panelMillas = buildConversionPanelMillas();
root.getChildren().addAll(
header, sep1,
panelMillas
);
}
private VBox buildConversionPanelMillas() {
// Creamos el título
Label panelTitulo = new Label("🏁 Millas → Kilómetros");
panelTitulo.setFont(Font.font("SansSerif", FontWeight.SEMI_BOLD, 14));
// Creamos una etiqueta para el TextField
Label inputLabel = new Label("Introduce las millas:");
millasInput = new TextField();
millasInput.setPromptText("Ej: 10");
// Lo hacemos máximo para que ocupe toda la caja
millasInput.setMaxWidth(Double.MAX_VALUE);
Label unidadLabel = new Label("kilómetros");
// Creamos un botón que, de momento, no hace nada
Button btnConvertir = new Button("Convertir a km");
btnConvertir.setMaxWidth(Double.MAX_VALUE);
// Creamos un label para mostrar el resultado
kmResultLabel = new Label("—");
kmResultLabel.setFont(Font.font("SansSerif", FontWeight.BOLD, 36));
kmResultLabel.setTextAlignment(TextAlignment.CENTER);
// Y ahora creamos una caja vertical con una separación de 2 con el resultado
VBox resultBox = new VBox(2, kmResultLabel, unidadLabel);
resultBox.setAlignment(Pos.CENTER);
// Y ahora, creamos otra caja vertical para apilar todos los controles
VBox panel = new VBox(10, panelTitulo, inputLabel, millasInput, btnConvertir, resultBox);
panel.setStyle("-fx-background-color: -color-bg-subtle; -fx-background-radius: 8;");
panel.setPadding(new Insets(16));
return panel;
}
public VBox getRoot(){
return root;
}
}
Este debe ser el resultado:

Resumiendo
-
Plantearse qué distribución va a tener, por ejemplo
VBox -
Creamos controles:
1 2 3 4
// Creamos una etiqueta Label inputLabel = new Label("Introduce las millas:"); // Y un control para introducir texto millasInput = new TextField();
-
Y ahora, que no se nos olvide agregarlo al
VBox1 2
// Y ahora creamos una caja vertical con una separación de 2 con el resultado VBox resultBox = new VBox(2, inputLabel, millasInput);
-
Este
resultBoxse debe añadir al nodoroot1 2 3
root.getChildren().addAll( resultBox // más el resto de controles de la ventana si hubiera );
Manejo de eventos
Ahora vamos a crear la lógica de la ventana: al pulsar el botón, debe aparecer la conversión a kilómetros en el millasResultadoLabel
Clase DistanceConverter
Creamos la clase DistanceConverter donde se aloja la lógica para hacer los cálculos y así separamos la lógica de la representación.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DistanceConverter {
private static final double KM_PER_MILE = 1.609344;
/**
* Convierte millas a kilómetros.
*/
public static double milesToKm(double miles) {
return miles * KM_PER_MILE;
}
/**
* Convierte kilómetros a millas.
*/
public static double kmToMiles(double km) {
return km / KM_PER_MILE;
}
/**
* Formatea un número con N decimales.
*/
public static String format(double value, int decimals) {
return String.format("%." + decimals + "f", value);
}
}
Creamos un método convertirMillasAKm para llamar a DistanceConverter.kmToMiles y actualizamos el estado de la ventana:
1
2
3
4
5
6
7
8
9
10
11
private void convertirMillasAKm() {
try {
double millas = Double.parseDouble(millasInput.getText().replace(",", "."));
double km = DistanceConverter.milesToKm(millas);
String resultado = DistanceConverter.format(km, DECIMALS);
kmResultLabel.setText(resultado);
animarLabel(kmResultLabel);
} catch (NumberFormatException e) {
kmResultLabel.setText("Valor inválido");
}
}
Y un método para hacer una chulada:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void animarLabel(Label label) {
ScaleTransition st = new ScaleTransition(Duration.millis(150), label);
st.setFromX(0.85);
st.setFromY(0.85);
st.setToX(1.0);
st.setToY(1.0);
FadeTransition ft = new FadeTransition(Duration.millis(150), label);
ft.setFromValue(0.4);
ft.setToValue(1.0);
st.play();
ft.play();
}
Evento click
En buildConversionPanelMillas, ya podemos convertir a millas
1
2
3
4
// Al hacer clic
btnConvertir.setOnAction(e -> {
convertirMillasAKm();
});
Ahora ya debe funcionar la conversión:

Creación del panel kilómetros ➡️ millas
Es igual que el anterior. Primero creamos el panel.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Código anterior
private TextField kmInput;
private Label millasResultadoLabel;
// Código anterior
private VBox buildConversionPanelKms() {
Label panelTitulo = new Label("📍 Kilómetros → Millas");
panelTitulo.setFont(Font.font("SansSerif", FontWeight.SEMI_BOLD, 14));
Label inputLabel = new Label("Introduce los kilómetros:");
kmInput = new TextField();
kmInput.setPromptText("Ej: 10");
kmInput.setMaxWidth(Double.MAX_VALUE);
Label unidadLabel = new Label("millas");
Button btnConvertir = new Button("Convertir a millas");
btnConvertir.setMaxWidth(Double.MAX_VALUE);
btnConvertir.setOnAction(e -> {
convertirKmAMillas();
});
millasResultadoLabel = new Label("—");
millasResultadoLabel.setFont(Font.font("SansSerif", FontWeight.BOLD, 36));
millasResultadoLabel.setTextAlignment(TextAlignment.CENTER);
VBox resultBox = new VBox(2, millasResultadoLabel, unidadLabel);
resultBox.setAlignment(Pos.CENTER);
VBox panel = new VBox(10, panelTitulo, inputLabel, kmInput, btnConvertir, resultBox);
panel.setStyle("-fx-background-color: -color-bg-subtle; -fx-background-radius: 8;");
panel.setPadding(new Insets(16));
return panel;
}
Creamos el método que convierte:
1
2
3
4
5
6
7
8
9
10
11
private void convertirKmAMillas() {
try {
double km = Double.parseDouble(kmInput.getText().replace(",", "."));
double millas = DistanceConverter.kmToMiles(km);
String resultado = DistanceConverter.format(millas, DECIMALS);
millasResultadoLabel.setText(resultado);
animarLabel(millasResultadoLabel);
} catch (NumberFormatException e) {
millasResultadoLabel.setText("Valor inválido");
}
}
Y, por último, modificamos la vista principal buildUI para añadir este panel:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ── Un panel para millas ─────────────────────
VBox panelMillas = buildConversionPanelMillas();
// ── Otro para los kilómetros ─────────────────
VBox panelKm = buildConversionPanelKms();
// ── Los paneles los ponemos uno al lado del otro
HBox panelesSideBySide = new HBox(16, panelMillas, panelKm);
panelesSideBySide.setAlignment(Pos.CENTER);
// ── Los paneles crecen hasta ocupar todo el ancho
HBox.setHgrow(panelMillas, Priority.ALWAYS);
HBox.setHgrow(panelKm, Priority.ALWAYS);
// ── Añadimos el nuevo panelesSideBySide ──────────
root.getChildren().addAll(
header, sep1,
panelesSideBySide
);
Este es el resultado

Hoja de estilo
Los controles de javaFX se pueden estilizar mediante css. Vamos a utilizar una librería que ya trae una serie de temas predefinidos. En pom.xml añade esta dependencia:
1
2
3
4
5
<dependency>
<groupId>io.github.mkpaz</groupId>
<artifactId>atlantafx-base</artifactId>
<version>2.0.1</version>
</dependency>
Y ahora en MainApp.start()
1
2
3
4
....
import atlantafx.base.theme.NordDark;
....
Application.setUserAgentStylesheet(new NordDark().getUserAgentStylesheet());
Y ahora el look and feel es este:

Los temas incluidos son los siguientes:
-
CupertinoDark, CupertinoLight
-
NordDark, NordLight
-
Dracula
-
PrimerDark, PrimerLight
Historial
Vamos a crear una lista donde se vayan guardando las conversiones realizadas:

Primero creamos la vista:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
private ListView<String> historialList;
...
private VBox buildHistorialPanel() {
// EL título
Label titulo = new Label("📋 Historial de conversiones");
titulo.setFont(Font.font("SansSerif", FontWeight.SEMI_BOLD, 14));
// Una lista para escribir las conversiones
historialList = new ListView<>();
historialList.setPrefHeight(120);
historialList.getStyleClass().add(Styles.DENSE);
historialList.setPlaceholder(new Label("Aún no hay conversiones..."));
// Botón para limpiar la vista
Button btnLimpiar = new Button("Limpiar historial");
btnLimpiar.getStyleClass().add(Styles.DANGER);
btnLimpiar.setOnAction(e -> historialList.getItems().clear());
// Y el panel vertical donde añado todos los controles
VBox panel = new VBox(8, titulo, historialList, btnLimpiar);
panel.setStyle("-fx-background-color: -color-bg-subtle; -fx-background-radius: 8;");
panel.setPadding(new Insets(16));
return panel;
}
Y en buildUI
1
2
3
4
5
6
7
8
9
VBox panelHistorial = buildHistorialPanel();
Separator sep2 = new Separator();
// ── Añadimos el nuevo panelHistorial ───────
root.getChildren().addAll(
header, sep1,
panelesSideBySide, sep2, panelHistorial
);
Un nuevo método para agregar el historial:
1
2
3
4
5
6
7
8
9
10
11
12
13
private void agregarHistorial(String entrada) {
// Si la entrada actual ya está añadida la primera, no la agrego
if (!historialList.getItems().isEmpty() &&
historialList.getItems().getFirst().equals(entrada)) return;
// Siempre la pongo la primera
historialList.getItems().addFirst(entrada);
// Solo caben 20 items
if (historialList.getItems().size() > 20) {
historialList.getItems().remove(20, historialList.getItems().size());
}
}
Y ahora, en convertirKmAMillas
1
agregarHistorial(String.format("%.4f km → %s mi", km, resultado));
Y en convertirMillasAKm
1
agregarHistorial(String.format("%.4f mi → %s km", millas, resultado));
Y este es el resultado:
