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:
- View → Controller: la vista captura eventos del usuario y los delega al controlador
- Controller → Model: el controlador modifica el modelo directamente
- 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