Swing y JFC: guía completa para interfaces gráficas en Java 21

Antes de escribir una sola línea de código, la pregunta razonable es: ¿para qué aprender Swing cuando existen JavaFX, Electron o directamente apps web? La respuesta corta es que Swing no ha desaparecido. IntelliJ IDEA, Eclipse, NetBeans y buena parte del software empresarial que llevas años usando están escritos con Swing. Si trabajas en una empresa con herramientas internas en Java de escritorio, es muy probable que acabes tocando código Swing tarde o temprano.

Dicho esto, para proyectos nuevos donde el equipo tiene libertad de elección, JavaFX es la alternativa más natural dentro del ecosistema Java. Tiene un modelo de componentes más moderno y CSS para estilos. Si el producto va a vivir en el navegador, frameworks web como React o Angular tienen más sentido todavía. Swing brilla cuando hay que mantener aplicaciones existentes, cuando el entorno está restringido a Java 8+ sin JavaFX, o cuando necesitas integración muy estrecha con APIs de bajo nivel del sistema operativo.

JFC, las Java Foundation Classes, es el paraguas que agrupa todo esto: la librería Swing de componentes visuales, la Java 2D API para gráficos, las APIs de accesibilidad y el soporte de drag and drop. Cuando alguien dice "programar con Swing", en realidad está usando JFC completo.

La ventana principal: JFrame

Todo en Swing parte de una ventana. La clase base es JFrame, y su uso más básico es así:

import javax.swing.*;

public class MiVentana {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Mi primera ventana");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setSize(600, 400);
            frame.setLocationRelativeTo(null); // centrada en pantalla
            frame.setVisible(true);
        });
    }
}

Hay dos formas de dar tamaño a la ventana: setSize(ancho, alto) fija las dimensiones directamente en píxeles. pack() en cambio calcula el tamaño mínimo necesario para que quepan todos los componentes que hayas añadido. Para ventanas con contenido variable o durante el desarrollo, pack() es más cómodo. Para ventanas con diseño fijo, setSize() da más control.

El setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) hace que al cerrar la ventana se termine el proceso Java. Sin esa línea, la ventana desaparece pero el proceso sigue corriendo en segundo plano. Y el setVisible(true) siempre al final, después de haber añadido todos los componentes; si lo pones antes, la ventana puede aparecer vacía o con parpadeos.

Componentes básicos

Swing tiene un catálogo amplio de componentes listos para usar. Los más habituales:

  • JLabel: texto o imagen estática, sin interacción del usuario.
  • JButton: el botón de toda la vida. Admite texto, icono o ambos.
  • JTextField: campo de texto de una línea.
  • JTextArea: área de texto multilínea. Casi siempre va envuelta en un JScrollPane para que aparezca la barra de desplazamiento cuando el texto supera el tamaño visible.
  • JCheckBox y JRadioButton: para selecciones. Los radio buttons van agrupados en un ButtonGroup para que solo se pueda marcar uno a la vez.
  • JComboBox<T>: lista desplegable. El tipo genérico define qué contiene.
  • JSpinner: campo numérico con flechas de incremento/decremento.

Un formulario de registro básico con validación:

import javax.swing.*;
import java.awt.*;

public class FormularioRegistro extends JFrame {

    public FormularioRegistro() {
        setTitle("Registro de usuario");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLayout(new GridLayout(4, 2, 10, 10));

        JLabel lblNombre = new JLabel("Nombre:");
        JTextField txtNombre = new JTextField();

        JLabel lblEmail = new JLabel("Email:");
        JTextField txtEmail = new JTextField();

        JLabel lblAdmin = new JLabel("Administrador:");
        JCheckBox chkAdmin = new JCheckBox();

        JButton btnGuardar = new JButton("Guardar");
        btnGuardar.addActionListener(e -> {
            String nombre = txtNombre.getText().trim();
            String email = txtEmail.getText().trim();

            if (nombre.isEmpty() || email.isEmpty()) {
                JOptionPane.showMessageDialog(this,
                    "Nombre y email son obligatorios.",
                    "Error de validación",
                    JOptionPane.ERROR_MESSAGE);
                return;
            }

            if (!email.contains("@")) {
                JOptionPane.showMessageDialog(this,
                    "El email no parece válido.",
                    "Error",
                    JOptionPane.WARNING_MESSAGE);
                return;
            }

            JOptionPane.showMessageDialog(this,
                "Usuario " + nombre + " registrado correctamente.");
        });

        add(lblNombre); add(txtNombre);
        add(lblEmail);  add(txtEmail);
        add(lblAdmin);  add(chkAdmin);
        add(new JLabel()); add(btnGuardar);

        pack();
        setLocationRelativeTo(null);
        setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(FormularioRegistro::new);
    }
}

Gestores de diseño (Layout Managers)

En Swing no posicionas los componentes a mano con coordenadas. Los layout managers se encargan de eso. Cada JPanel o JFrame tiene uno asignado y organiza automáticamente los componentes cuando la ventana cambia de tamaño.

BorderLayout

Divide el espacio en cinco zonas: NORTH, SOUTH, EAST, WEST y CENTER. Las zonas Norte y Sur ocupan todo el ancho disponible y se ajustan en altura al componente que contienen. Este y Oeste hacen lo opuesto: ancho ajustado al contenido, altura máxima. El Centro ocupa lo que sobra. Es el layout por defecto de JFrame y el más habitual para estructurar ventanas principales.

GridLayout

Cuadrícula regular donde todas las celdas tienen el mismo tamaño. Útil para calculadoras, teclados virtuales o cualquier cosa donde quieras que los componentes estén perfectamente alineados en filas y columnas.

FlowLayout

Coloca los componentes en fila, de izquierda a derecha, y pasa a la siguiente línea cuando no caben más. Es el layout por defecto de JPanel. Para barras de botones o grupos pequeños de controles va bien.

GridBagLayout

El más potente y también el más verboso. Permite que los componentes ocupen varias filas o columnas, tengan márgenes individuales y se expandan de forma distinta. Para layouts complejos es la única opción real, aunque el código se vuelve bastante largo. Si lo usas, aisla cada bloque de configuración en un método auxiliar para no perder el hilo.

BoxLayout

Apila componentes en una sola dirección, horizontal o vertical. Más sencillo que GridBagLayout para casos donde solo necesitas alinear elementos en una fila o columna sin proporciones complicadas.

En la práctica, la mejor estrategia es combinar varios JPanel, cada uno con su propio layout, y anidarlos. Un BorderLayout en la ventana principal, con un FlowLayout para los botones de la parte inferior y un GridBagLayout para el formulario central.

Eventos y listeners

El modelo de eventos de Swing tiene tres actores: la fuente (el componente que genera el evento), el listener (el objeto que lo recibe) y el evento en sí (el objeto que describe qué pasó). Cuando el usuario hace clic en un botón, ese botón notifica a todos sus listeners registrados.

Antes de Java 8 había que implementar interfaces con clases anónimas, lo que generaba mucho ruido sintáctico. Ahora, con lambdas, queda mucho más limpio:

JButton btn = new JButton("Enviar");

// Con lambda (Java 8+)
btn.addActionListener(e -> System.out.println("Clic: " + e.getActionCommand()));

// Equivalente con clase anónima (pre-Java 8)
btn.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Clic: " + e.getActionCommand());
    }
});

Para teclado y ratón existen KeyListener y MouseListener. KeyListener captura pulsaciones de tecla sobre un componente concreto. Para atajos globales de ventana es mejor usar KeyBinding a través del input map del componente. WindowListener permite interceptar eventos de la ventana: cuando se minimiza, maximiza, cierra o recupera el foco.

El Event Dispatch Thread (EDT)

Swing no es thread-safe. Todas las modificaciones a la interfaz gráfica deben ejecutarse en el Event Dispatch Thread, el hilo que gestiona los eventos y el pintado de componentes. Si modificas un componente desde otro hilo, el comportamiento es impredecible: puede funcionar la mayor parte del tiempo y fallar aleatoriamente.

Para encolar una tarea en el EDT desde cualquier hilo:

SwingUtilities.invokeLater(() -> {
    label.setText("Actualizado desde otro hilo");
});

Cuando tienes una tarea pesada (una descarga, una consulta a base de datos) y necesitas actualizar la UI mientras progresa, SwingWorker es la solución. Ejecuta el trabajo en un hilo de fondo y publica actualizaciones que llegan al EDT de forma segura:

import javax.swing.*;
import java.util.List;

public class DescargaWorker extends SwingWorker<String, Integer> {

    private final JProgressBar progressBar;
    private final JLabel statusLabel;

    public DescargaWorker(JProgressBar bar, JLabel label) {
        this.progressBar = bar;
        this.statusLabel = label;
    }

    @Override
    protected String doInBackground() throws Exception {
        // Este código corre en hilo de fondo, NO en el EDT
        for (int i = 0; i <= 100; i += 10) {
            Thread.sleep(300); // simulamos trabajo
            publish(i);        // envía progreso al EDT
        }
        return "Descarga completada";
    }

    @Override
    protected void process(List<Integer> chunks) {
        // Esto sí corre en el EDT, seguro para actualizar UI
        int ultimo = chunks.get(chunks.size() - 1);
        progressBar.setValue(ultimo);
        statusLabel.setText("Progreso: " + ultimo + "%");
    }

    @Override
    protected void done() {
        try {
            statusLabel.setText(get()); // resultado final
        } catch (Exception e) {
            statusLabel.setText("Error: " + e.getMessage());
        }
    }
}

// Uso:
JProgressBar bar = new JProgressBar(0, 100);
JLabel status = new JLabel("Esperando...");
JButton btnDescargar = new JButton("Descargar");
btnDescargar.addActionListener(e -> new DescargaWorker(bar, status).execute());

JTable y JTree

JTable

JTable muestra datos en filas y columnas. Lo más sencillo es usar DefaultTableModel:

String[] columnas = {"Nombre", "Email", "Rol"};
Object[][] datos = {
    {"Ana García",   "[email protected]",   "Admin"},
    {"Luis Pérez",   "[email protected]",  "Editor"},
    {"María López",  "[email protected]", "Lector"}
};

DefaultTableModel modelo = new DefaultTableModel(datos, columnas) {
    @Override
    public boolean isCellEditable(int row, int col) {
        return col != 2; // la columna Rol no es editable
    }
};

JTable tabla = new JTable(modelo);
JScrollPane scroll = new JScrollPane(tabla);

Para colorear filas según alguna condición, implementa TableCellRenderer:

tabla.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
    @Override
    public Component getTableCellRendererComponent(
            JTable t, Object value, boolean selected,
            boolean focused, int row, int col) {
        Component c = super.getTableCellRendererComponent(
                t, value, selected, focused, row, col);
        if (!selected) {
            c.setBackground(row % 2 == 0
                ? Color.WHITE
                : new Color(240, 245, 255));
        }
        return c;
    }
});

JTree

JTree muestra jerarquías. Un explorador de directorios básico:

import javax.swing.*;
import javax.swing.tree.*;
import java.io.File;

public class ExploradorDirectorios extends JFrame {

    public ExploradorDirectorios() {
        setTitle("Explorador");
        setDefaultCloseOperation(EXIT_ON_CLOSE);

        File raiz = new File(System.getProperty("user.home"));
        DefaultMutableTreeNode nodoRaiz = construirNodo(raiz, 2);

        JTree arbol = new JTree(nodoRaiz);
        add(new JScrollPane(arbol));

        setSize(400, 500);
        setLocationRelativeTo(null);
        setVisible(true);
    }

    private DefaultMutableTreeNode construirNodo(File dir, int profundidad) {
        DefaultMutableTreeNode nodo = new DefaultMutableTreeNode(dir.getName());
        if (profundidad == 0 || !dir.isDirectory()) return nodo;

        File[] hijos = dir.listFiles();
        if (hijos == null) return nodo;

        for (File hijo : hijos) {
            nodo.add(construirNodo(hijo, profundidad - 1));
        }
        return nodo;
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(ExploradorDirectorios::new);
    }
}

Menús y diálogos

La barra de menú de una aplicación Swing se construye con JMenuBar, JMenu y JMenuItem:

JMenuBar menuBar = new JMenuBar();

JMenu menuArchivo = new JMenu("Archivo");
menuArchivo.setMnemonic('A'); // Alt+A abre el menú

JMenuItem itemAbrir = new JMenuItem("Abrir");
itemAbrir.setAccelerator(
    KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));
itemAbrir.addActionListener(e -> abrirFichero());

JMenuItem itemSalir = new JMenuItem("Salir");
itemSalir.addActionListener(e -> System.exit(0));

menuArchivo.add(itemAbrir);
menuArchivo.addSeparator();
menuArchivo.add(itemSalir);
menuBar.add(menuArchivo);
frame.setJMenuBar(menuBar);

Para diálogos, JOptionPane cubre la mayoría de casos sin tener que crear una ventana nueva:

// Mensaje informativo
JOptionPane.showMessageDialog(frame, "Operación completada.");

// Confirmación
int opcion = JOptionPane.showConfirmDialog(frame,
    "¿Seguro que quieres borrar esto?",
    "Confirmar borrado",
    JOptionPane.YES_NO_OPTION);
if (opcion == JOptionPane.YES_OPTION) { borrar(); }

// Entrada de texto
String nombre = JOptionPane.showInputDialog(frame, "Introduce tu nombre:");

Para abrir y guardar ficheros, JFileChooser es la clase estándar:

JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new FileNameExtensionFilter("Archivos de texto", "txt"));

if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
    File fichero = chooser.getSelectedFile();
    System.out.println("Seleccionado: " + fichero.getAbsolutePath());
}

Pintado personalizado con Graphics2D

Para dibujar formas, texto o imágenes directamente en un componente, se extiende JPanel y se sobreescribe paintComponent:

import javax.swing.*;
import java.awt.*;
import java.awt.geom.*;

public class PanelGrafico extends JPanel {

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g); // limpia el fondo antes de dibujar

        Graphics2D g2 = (Graphics2D) g;

        // Antialiasing para bordes suaves
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);

        // Degradado de fondo
        GradientPaint degradado = new GradientPaint(
            0, 0, new Color(70, 130, 180),
            getWidth(), getHeight(), new Color(30, 60, 90));
        g2.setPaint(degradado);
        g2.fillRect(0, 0, getWidth(), getHeight());

        // Círculo blanco semitransparente
        g2.setColor(new Color(255, 255, 255, 80));
        g2.fill(new Ellipse2D.Double(50, 50, 150, 150));

        // Texto
        g2.setColor(Color.WHITE);
        g2.setFont(new Font("SansSerif", Font.BOLD, 18));
        g2.drawString("Pintado con Graphics2D", 30, 240);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Graphics2D");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            PanelGrafico panel = new PanelGrafico();
            panel.setPreferredSize(new Dimension(300, 280));
            frame.add(panel);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

Para forzar que el componente se redibuje cuando cambian los datos, llama a repaint(). Nunca llames a paintComponent directamente.

Modernizar la apariencia con FlatLaf

El look & feel por defecto de Swing tiene pinta de aplicación de los años 2000. FlatLaf lo resuelve: es una librería de look & feel moderno que da a tus aplicaciones Swing una apariencia limpia y compatible con temas claro y oscuro, sin tocar ni una línea de tu código de componentes.

Si usas Maven, añade la dependencia:

<dependency>
    <groupId>com.formdev</groupId>
    <artifactId>flatlaf</artifactId>
    <version>3.4.1</version>
</dependency>

Y actívalo antes de crear cualquier componente:

import com.formdev.flatlaf.FlatLightLaf;
import com.formdev.flatlaf.FlatDarkLaf;
import javax.swing.*;

public class App {
    public static void main(String[] args) {
        // Tema claro
        FlatLightLaf.setup();

        // O tema oscuro
        // FlatDarkLaf.setup();

        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("App con FlatLaf");
            // ... resto de la aplicación
            frame.setVisible(true);
        });
    }
}

También puedes cambiar el tema en tiempo de ejecución con UIManager.setLookAndFeel() y un SwingUtilities.updateComponentTreeUI(frame) para que se aplique a los componentes ya creados. Útil si quieres ofrecer un botón de "cambiar a modo oscuro" dentro de tu propia aplicación.

FlatLaf tiene variantes de pago (IntelliJ themes) que replican exactamente el aspecto de IntelliJ IDEA, pero la versión libre ya da un resultado muy decente para uso profesional.

Arquitectura: separa lógica de presentación

Un error habitual cuando se empieza con Swing es meter toda la lógica dentro de los listeners. El código crece y acaba siendo imposible de mantener. El patrón MVC (Model-View-Controller) o MVP (Model-View-Presenter) va bien aquí. La vista solo sabe dibujar componentes y capturar eventos; el modelo gestiona los datos; el controlador o presenter conecta ambos.

Si necesitas profundizar en cómo estructurar esto, los patrones de diseño en Java aplicables a la arquitectura de una aplicación Swing te dan las herramientas necesarias para hacerlo bien desde el principio.

Y si estás valorando qué versión de Java usar como base para un proyecto nuevo con Swing, ten en cuenta que Java 25 LTS, la versión de referencia actual, es la opción con soporte garantizado a largo plazo. Swing es totalmente compatible.

La documentación oficial del paquete Swing está en Oracle JavaSE 21 API, donde encontrarás la referencia completa de cada clase con sus constructores, métodos y ejemplos.

Imagen: Pexels

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP