Saltar al contenido principal

Patrones avanzados

MVC en Swing

Swing implementa el patrón MVC de forma flexible. En aplicaciones grandes conviene aplicarlo explícitamente para separar responsabilidades.

En el MVC, el flujo correcto es:

  1. View → Controller: la vista captura eventos del usuario y los delega al controlador
  2. Controller → Model: el controlador modifica el modelo directamente
  3. Model → View: el modelo notifica a la vista (patrón Observer) para que se refresque

En Swing, el patrón MVC se implementa típicamente registrando los listeners del controlador directamente sobre los componentes de la vista (addActionListener, etc.), y el modelo implementa el patrón Observer (o usa PropertyChangeSupport) para avisar a la vista cuando sus datos cambian. La vista nunca toca el modelo directamente.

Ejemplo: lista de tareas con MVC

TareasModel.java
import java.util.*;

public class TareasModel {
private final List<String> tareas = new ArrayList<>();
private final List<Runnable> listeners = new ArrayList<>();

public void addTarea(String tarea) {
tareas.add(tarea);
notificar();
}

public void removeTarea(int indice) {
tareas.remove(indice);
notificar();
}

public List<String> getTareas() {
return Collections.unmodifiableList(tareas);
}

public void addListener(Runnable l) { listeners.add(l); }

private void notificar() {
listeners.forEach(Runnable::run);
}
}
TareasView.java
public class TareasView extends JFrame {
JTextField txtNuevaTarea = new JTextField(20);
JButton btnAnadir = new JButton("Añadir");
JButton btnEliminar = new JButton("Eliminar");
DefaultListModel<String> listModel = new DefaultListModel<>();
JList<String> lista = new JList<>(listModel);

public TareasView() {
super("Gestor de Tareas");
setDefaultCloseOperation(EXIT_ON_CLOSE);

JPanel top = new JPanel(new FlowLayout());
top.add(txtNuevaTarea);
top.add(btnAnadir);

JPanel bottom = new JPanel(new FlowLayout(FlowLayout.RIGHT));
bottom.add(btnEliminar);

setLayout(new BorderLayout(5, 5));
add(top, BorderLayout.NORTH);
add(new JScrollPane(lista), BorderLayout.CENTER);
add(bottom, BorderLayout.SOUTH);

pack();
setSize(400, 300);
setLocationRelativeTo(null);
}

public void actualizarLista(List<String> tareas) {
listModel.clear();
tareas.forEach(listModel::addElement);
}
}
TareasController.java
public class TareasController {
private final TareasModel model;
private final TareasView view;

public TareasController(TareasModel model, TareasView view) {
this.model = model;
this.view = view;

// Sincronizar vista cuando el modelo cambia
model.addListener(() -> SwingUtilities.invokeLater(
() -> view.actualizarLista(model.getTareas())
));

// Eventos de la vista → acciones en el modelo
view.btnAnadir.addActionListener(e -> {
String texto = view.txtNuevaTarea.getText().trim();
if (!texto.isEmpty()) {
model.addTarea(texto);
view.txtNuevaTarea.setText("");
view.txtNuevaTarea.requestFocus();
}
});

view.btnEliminar.addActionListener(e -> {
int idx = view.lista.getSelectedIndex();
if (idx >= 0) model.removeTarea(idx);
});

view.txtNuevaTarea.addActionListener(e -> view.btnAnadir.doClick());
}
}
App.java
public class App {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
TareasModel model = new TareasModel();
TareasView view = new TareasView();
new TareasController(model, view);
view.setVisible(true);
});
}
}

Persistencia de preferencias con Preferences

import java.util.prefs.Preferences;

public class PreferenciasApp {
private static final Preferences prefs = Preferences.userNodeForPackage(PreferenciasApp.class);

// Guardar estado de la ventana
public static void guardarVentana(JFrame ventana) {
prefs.putInt("ventana.x", ventana.getX());
prefs.putInt("ventana.y", ventana.getY());
prefs.putInt("ventana.ancho", ventana.getWidth());
prefs.putInt("ventana.alto", ventana.getHeight());
}

// Restaurar estado
public static void restaurarVentana(JFrame ventana) {
int x = prefs.getInt("ventana.x", 100);
int y = prefs.getInt("ventana.y", 100);
int ancho = prefs.getInt("ventana.ancho", 800);
int alto = prefs.getInt("ventana.alto", 600);
ventana.setBounds(x, y, ancho, alto);
}

// Otras preferencias
public static void guardarUltimaRuta(String ruta) {
prefs.put("ultima.ruta", ruta);
}

public static String getUltimaRuta() {
return prefs.get("ultima.ruta", System.getProperty("user.home"));
}
}

// Uso en la ventana principal:
addWindowListener(new WindowAdapter() {
@Override public void windowClosing(WindowEvent e) {
PreferenciasApp.guardarVentana(MiVentana.this);
}
});
PreferenciasApp.restaurarVentana(this);

Drag and Drop

// Hacer un componente arrastrable
JLabel lblArrastrable = new JLabel("Arrástrme");
lblArrastrable.setTransferHandler(new TransferHandler("text"));
lblArrastrable.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
JComponent c = (JComponent) e.getSource();
TransferHandler handler = c.getTransferHandler();
handler.exportAsDrag(c, e, TransferHandler.COPY);
}
});

// Hacer un componente receptor de drop
JTextField txtDestino = new JTextField(20);
txtDestino.setTransferHandler(new TransferHandler("text") {
@Override
public boolean canImport(TransferSupport support) {
return support.isDataFlavorSupported(DataFlavor.stringFlavor);
}

@Override
public boolean importData(TransferSupport support) {
try {
String texto = (String) support.getTransferable()
.getTransferData(DataFlavor.stringFlavor);
txtDestino.setText(texto);
return true;
} catch (Exception e) {
return false;
}
}
});

// Activar drop en el componente
txtDestino.setDropTarget(new DropTarget(txtDestino, DnDConstants.ACTION_COPY_OR_MOVE, null, true));

Internacionalización (i18n)

// Archivos de recursos:
// src/resources/mensajes.properties (español, por defecto)
// src/resources/mensajes_en.properties (inglés)
// src/resources/mensajes_fr.properties (francés)

// mensajes.properties:
// boton.aceptar=Aceptar
// boton.cancelar=Cancelar
// dialogo.titulo=Configuración

// Cargar el bundle
ResourceBundle bundle = ResourceBundle.getBundle("resources/mensajes",
new Locale("es", "ES"));

// O con la locale del sistema:
ResourceBundle bundle = ResourceBundle.getBundle("resources/mensajes");

// Usar
btnAceptar.setText(bundle.getString("boton.aceptar"));
setTitle(bundle.getString("dialogo.titulo"));

// Cambiar idioma en tiempo de ejecución
Locale nuevaLocale = new Locale("en", "US");
ResourceBundle nuevoBundle = ResourceBundle.getBundle("resources/mensajes", nuevaLocale);
actualizarTextos(nuevoBundle);

Animaciones con Timer

public class PanelAnimado extends JPanel {
private float angulo = 0f;
private final Timer timer;

public PanelAnimado() {
setBackground(Color.WHITE);
setPreferredSize(new Dimension(300, 300));

timer = new Timer(16, e -> { // ~60 fps
angulo += 2f;
if (angulo >= 360f) angulo -= 360f;
repaint();
});
timer.start();
}

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

int cx = getWidth() / 2;
int cy = getHeight() / 2;
int r = 80;

// Dibujar puntos en círculo rotando
for (int i = 0; i < 12; i++) {
double theta = Math.toRadians(angulo + i * 30);
int x = (int) (cx + r * Math.cos(theta));
int y = (int) (cy + r * Math.sin(theta));
float alpha = (i + 1) / 12f;
g2.setColor(new Color(0f, 0.5f, 1f, alpha));
int size = (int) (4 + 10 * alpha);
g2.fillOval(x - size/2, y - size/2, size, size);
}

g2.dispose();
}

public void detener() { timer.stop(); }
public void iniciar() { timer.start(); }
}

Accesibilidad

// Añadir descripciones para lectores de pantalla
btnGuardar.getAccessibleContext().setAccessibleName("Guardar documento");
btnGuardar.getAccessibleContext().setAccessibleDescription("Guarda el documento actual en disco");

// Tooltip
btnGuardar.setToolTipText("Guardar (Ctrl+S)");

// Mnemónicos en botones (Alt+tecla)
btnGuardar.setMnemonic(KeyEvent.VK_G); // Alt+G

// Tab order automático (orden de adición a los paneles)
// Orden personalizado:
panel.setFocusTraversalPolicy(new FocusTraversalPolicy() {
@Override public Component getComponentAfter(Container c, Component comp) { /* ... */ return null; }
@Override public Component getComponentBefore(Container c, Component comp) { /* ... */ return null; }
@Override public Component getFirstComponent(Container c) { return txtNombre; }
@Override public Component getLastComponent(Container c) { return btnGuardar; }
@Override public Component getDefaultComponent(Container c) { return txtNombre; }
});

Action — Acciones reutilizables

Action encapsula un comando con su texto, icono, atajo de teclado, etc. Se puede asignar a botones, ítems de menú y atajos de teclado simultáneamente.

public class GuardarAction extends AbstractAction {

private final DocumentoModel documento;

public GuardarAction(DocumentoModel documento) {
super("Guardar"); // Texto
this.documento = documento;
putValue(SMALL_ICON,
new ImageIcon(getClass().getResource("/icons/save.png")));
putValue(SHORT_DESCRIPTION, "Guardar el documento");
putValue(ACCELERATOR_KEY,
KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK));
putValue(MNEMONIC_KEY, KeyEvent.VK_G);
}

@Override
public void actionPerformed(ActionEvent e) {
documento.guardar();
}
}
// Uso:
GuardarAction guardarAction = new GuardarAction(miDocumento);

JButton btnGuardar = new JButton(guardarAction);
JMenuItem itmGuardar = new JMenuItem(guardarAction);

// El atajo Ctrl+S se registra automáticamente en el componente raíz
toolbar.add(btnGuardar);
menuArchivo.add(itmGuardar);

// Deshabilitar todos de golpe:
guardarAction.setEnabled(false); // Deshabilita botón, menú y atajo a la vez