viernes, 18 de diciembre de 2009

Tratamiento de imágenes en Java, Parte 2: Controlador y Modelo


¡Felices fiestas!

¡Saludos nuevamente blogueros!

Ahora terminaremos con el código que quedó pendiente en el artículo anterior, que consiste en la programación del Controlador y el Modelo de nuestra aplicación.

El Controlador tendrá la obligación de manejar los eventos que ocurran en la interfaz de usuario, de modo que recibirá las acciones del usuario en el menú de opciones y las paletas de edición, los cuales ya creamos en el artículo anterior.

Por otro lado, el Modelo tendrá responsabilidades a más bajo nivel, encargándose de implementar la carga, la edición y el guardado de las imágenes. Separando las diferentes funcionalidades en dos capas de nivel distinto, como si de las capas de Negocio y Acceso a datos se tratara.

El Controlador

El Controlador de la aplicación consistirá en una única clase que implementará las interfaces ActionListener y ChangeListener, para implementar en control de eventos del menú y de las barras JSlider respectivamente.

El código es el siguiente:

Controlador.java
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JMenuItem;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.JPanel;
/**
* @Desc Clase que implementa la gestión de evento en la interfaz de usuario
* @author Beto González
*
*/
public class Controlador implements ActionListener, ChangeListener{
ManejadorDeImagenes manejador;
PanelSwing panel;

public Controlador(PanelSwing panel) {
this.panel = panel;
manejador = new ManejadorDeImagenes();
}
/**
* @Desc Método que capturará los eventos ocurridos en el menú principal del sistema
*/
public void actionPerformed(ActionEvent ie) {
JMenuItem i = (JMenuItem)ie.getSource();
if(i.getText() == "Abrir"){
boolean estado = manejador.cargaArchivoDeImagen(panel, panel.lienzo);
if(estado) {
panel.guardar.setEnabled(true);
panel.brillo.setEnabled(true);
panel.color.setEnabled(true);
panel.escala.setEnabled(true);
panel.esqueInf.show(panel.panelBajo, "carta1");
}
}
else if(i.getText() == "Guardar")
    manejador.guardaArchivoDeImagen(panel);
else if(i.getText() == "Salir")
System.exit(0);
else if(i.getText() == "Ajustar Brillo")
{
manejador.restableceImagen(panel.lienzo);
panel.jslBrillo.setValue(0);
panel.esqueInf.show(panel.panelBajo, "carta2");
}
else if(i.getText() == "Ajustar Colores")
{
manejador.restableceImagen(panel.lienzo);
panel.esqueInf.show(panel.panelBajo, "carta3");
}
else if(i.getText() == "Escala de Grises")
{
panel.esqueInf.show(panel.panelBajo, "carta1");
manejador.muestraEscalaDeGrises(panel.lienzo);
}
}
/**
* @Desc Método que captarará los eventos ocurridos en los componentes JSlider de la interfaz de usuario
*/
public void stateChanged(ChangeEvent e)
{
JSlider slider = (JSlider) e.getSource();
if(slider == panel.jslBrillo)
manejador.muestraBrillo(panel.lienzo, slider.getValue());
else if(slider == panel.jslRojo) {
manejador.muestraColores(panel.lienzo, slider.getValue(), panel.jslVerde.getValue(), panel.jslAzul.getValue());
}
else if(slider == panel.jslVerde)
manejador.muestraColores(panel.lienzo, panel.jslRojo.getValue(), slider.getValue(), panel.jslAzul.getValue());
else if(slider == panel.jslAzul)
manejador.muestraColores(panel.lienzo, panel.jslRojo.getValue(), panel.jslVerde.getValue(), slider.getValue());
}
}


Como podemos ver, en la implementación del método actionPerformed() lleva a cabo las acciones relacionadas con el menú de opciones, tanto para las acciones de la opción "Archivo" (Abrir, Guardar y Salir) como para la "Edición" (Ajustar Brillo, Ajustar Colores, Escala de Grises). Las opciones de edición solo se activarán una vez que un archivo de imagen haya sido cargado por el editor, para lo cual se apoyará en un objeto de la clase ManejadorDeImagenes, la cual veremos más adelante.

En la implementación del método stateChanged(), se reciben las acciones ocurridas en los controles de tipo JSlider, de forma que podremos capturar el valor del brillo o los colores que el usuario capture en la interface, y llamará a alguno de los métodos del objeto de la clase ManejadorDeImagenes para aplicar estos cambios a las imagenes cargadas.

El Modelo

Como ya explicamos antes, el modelo consistirá estará separado en dos capas, que en la programación veremos expresado como dos clases diferentes. La primera ManejadorDeImagenes, que equivaldrá a la capa de negocio en las aplicaciones de manejo de datos. Esta clase será un intermediario entre el Controlador y la Vista de la aplicación con el nivel de Acceso a datos, que en este caso consiste en la apertura, modificación y salvado de las imágenes. Estas operaciones de bajo nivel serán llevadas a cabo por la clase ProcesadorDeImagenes, la cual estará completamente abstraída del resto de la aplicación, ya que en ella ocurrirán las operaciones a nivel de pixeles sobre las imágenes, y además poseerá métodos para cargar una imagen desde el archivo y salvar las imágenes resultantes del procesamiento hacia un archivo nuevo.

El código de la primera de estas clases aparece a continuación:

ManejadorDeImagenes.java
import java.awt.*;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
/**
* @Desc Clase del nivel de la capa de negocios, que implementa las operaciones que son llamadas desde el Controlador de la aplicación
* para poder cargar las imagenes, alamacenarlas y modificaralas, apoyandose en un objeto la clase de más bajo nivel, es decir ProcesadorDeImagenes
* @author Beto González
*
*/
public class ManejadorDeImagenes {
ProcesadorDeImagenes procesador;
boolean editado = false;
// Constructor de la clase
public ManejadorDeImagenes() {
procesador = new ProcesadorDeImagenes();
}
/**
* @Desc Método que lleva a cabo la carga de un archivo de imagen
* @param contenedor
* @param lienzo
* @return
*/
public boolean cargaArchivoDeImagen(JPanel contenedor, PanelDeImagen lienzo)
{
String nombreArchivo = "";
boolean estado = true;
if(editado)
{
//new MsgError("Confirmacion","Aqui debemos pedir confirmación",200,180);
int resultado = JOptionPane.showConfirmDialog((Component)null, "¿Deseas guardar los cambios en este documento?","Confirmación",JOptionPane.YES_NO_OPTION);
if(resultado==JOptionPane.YES_OPTION)
guardaArchivoDeImagen(contenedor);
}
JFileChooser selector = new JFileChooser();
selector.addChoosableFileFilter(new FiltrodeArchivo("gif","Archivos Gif"));
String lista[] = {"jpeg","jpg"};
selector.addChoosableFileFilter(new FiltrodeArchivo(lista,"Archivos JPEG"));
selector.setDialogTitle("Abrir archivo de imagen");
selector.setDialogType(JFileChooser.OPEN_DIALOG);
int resultado = selector.showOpenDialog(null);
if(resultado == JFileChooser.APPROVE_OPTION)
{
nombreArchivo = selector.getSelectedFile().getName();
String ruta = selector.getSelectedFile().getPath();
Image imagen = procesador.cargaImagen(ruta, nombreArchivo);
lienzo.estableceImagen(imagen);
lienzo.repaint();
editado = false;
}
else
estado = false;
return estado;
}
/**
* @Desc Método que lleva a cabo la operación de salvar el archivo de imagen cargado
* @param contenedor
* @return
*/
public boolean guardaArchivoDeImagen(JPanel contenedor)
{
boolean estado = true;
JFileChooser selector = new JFileChooser();
selector.addChoosableFileFilter(new FiltrodeArchivo("gif","Archivos Gif"));
String lista[] = {"jpeg","jpg"};
selector.addChoosableFileFilter(new FiltrodeArchivo(lista,"Archivos JPEG"));
selector.setDialogTitle("Guardar archivo de imagen");
selector.setDialogType(JFileChooser.SAVE_DIALOG);
int resultado = selector.showSaveDialog(contenedor);
if(resultado == JFileChooser.APPROVE_OPTION)
{
//guardar archivo en la ruta especificada
String nombreArchivo = selector.getSelectedFile().getName();
String ruta = selector.getSelectedFile().getPath();
estado = procesador.guardaImagen(ruta, nombreArchivo);
if(!estado)
JOptionPane.showMessageDialog((Component)null,"Error del sistema : "+procesador.devuelveMensajeDeError(),"Error de Imagen",JOptionPane.OK_OPTION);
editado = false;
}
else
estado = false;
return estado;
}
/**
* @Desc Método que lleva a cabo la transformación de la imagen cargada a una imagen de escala de grises y la despliega en pantalla
* @param lienzo
*/
public void muestraEscalaDeGrises(PanelDeImagen lienzo)
{
procesador.escalaDeGrises();
lienzo.estableceImagen(procesador.devuelveImagenModificada());
lienzo.repaint();
}
/**
* @Desc Método que lleva a cabo la modificación del brillo de la imagen cargada y despliega la imagen resultante en pantalla
* @param lienzo
* @param valor
*/
public void muestraBrillo(PanelDeImagen lienzo, int valor)
{
procesador.modificaBrillo(valor);
lienzo.estableceImagen(procesador.devuelveImagenModificada());
lienzo.repaint();
editado = true;
}
/**
* @Desc @Desc Método que lleva a cabo la modificación de los colores de la imagen cargada y despliega la imagen resultante en pantalla
* @param lienzo
* @param rojo
* @param verde
* @param azul
*/
public void muestraColores(PanelDeImagen lienzo, int rojo, int verde, int azul)
{
procesador.modificaColor(rojo,verde,azul);
lienzo.estableceImagen(procesador.devuelveImagenModificada());
lienzo.repaint();
editado = true;
}
/**
* @Desc Método que coloca en la pantalla la imagen original que se cargó con el método cargarArchivoDeImagen
* @param lienzo
*/
public void restableceImagen(PanelDeImagen lienzo)
{
lienzo.estableceImagen(procesador.devuelveImagenBase());
lienzo.repaint();
editado = false;
}
}


Como vemos en los métodos cargaArchivoDeImagen() y guardaArchivoDeImagen(), la funcionalidad para elegir los archivos que serán cargados y guardados se facilita con el uso de la clase JFileChooser, la cual permite llamar a una ventana de selección de archivos, con la cual resulta más agradable esta tarea.

También podemos apreciar que se utiliza un objeto de la clase FiltroDeArchivo, para establecer el filtro que tendrá el objeto selector de la clase JFileChooser. De esta forma nos aseguramos que solo puedan ser seleccionadas imágenes de tipo JPEG y GIF. El código de esta clase auxiliar aparece a continuación:

FiltroDeArchivo.java
import java.io.*;
/**
* @Desc Clase que permite crear un filtro de archivos para utilizarlo con un selector de archivos
* @author Beto González
*
*/
public class FiltrodeArchivo extends javax.swing.filechooser.FileFilter{
String descrip = "";
String listTipos[];
FiltrodeArchivo(String tipo, String descripcion)
{
listTipos = new String[1];
listTipos[0] = tipo;
descrip = descripcion;
}
FiltrodeArchivo(String listTipos[], String descripcion)
{
this.listTipos = listTipos;
descrip = descripcion;
}
public boolean accept(File fileobj){
boolean extIgual = false;
String extension = "";
int ind=0;
if(fileobj.getPath().lastIndexOf(".") > 0)
{
extension = fileobj.getPath().substring(fileobj.getPath().lastIndexOf(".")+1).toLowerCase();
}
if(extension!="")
{
while(ind<listTipos.length && !extIgual)
{
extIgual = extension.equals(listTipos[ind].toLowerCase());
ind++;
}
return extIgual;
}
else
return fileobj.isDirectory();
}
public String getDescription()
{
if (descrip != "")
return descrip.concat(concatTipos());
else
return "";
}
private String concatTipos()
{
StringBuffer tipos = new StringBuffer(" (*."+listTipos[0]);
for(int i=1;i<listTipos.length;i++)
tipos.append(",*."+listTipos[i]);
tipos.append(")");
return tipos.toString();
}
}


Tanto estos dos métodos como el resto serán llamados desde el Controlador de la aplicación para cada acción que el usuario vaya realizando en la interfaz gráfica. De modo que el método muestraEscalaDeGrises() será llamado cuando el usuario seleccione la opcion "Escala de Grises" en el menú principal. Mientas que el método muestraBrillo() es llamado cuando el usuario realice un cambio en el brillo de la imagen, utilizando la barra JSlider jslBrillo para ajustar el brillo. Algo similar ocurrirá con el método muestraColores(), que se llamará cuando alguna de las barras JSlider de colores (jslRojo, jslVerde, jslAzul) sea utilizada. Estos métodos además llevan a cabo la actualización en la pantalla de las imágenes una vez que son modificadas. En el caso de que el usuario aumente el brillo o modifique los colores, la acción ocurrirá tan rápido que no podrá darse cuenta de todo el trabajo que la PC llevó a cabo para cambiar la apariencia de la imagen capturada.

El método restableceImagen() se utiliza para volver a colocar en la pantalla la imagen original que fue cargada, lo cual es simplemente una forma de asegurarnos que las tres acciones de edición (aumentar brillo, modificar colores y convertir a escala de grises) siempre ocurran sobre la misma imagen que fue cargada.

El código de la clase ProcesadorDeImagenes a la que nos hemos referido aparece a continuación:

ProcesadorDeImagenes.java
import java.awt.Canvas;
import java.awt.image.*;
import java.awt.Image;
import java.awt.Toolkit;
import java.io.IOException;
import java.io.File;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
/**
* @Desc Clase que implementa el procesamiento básico de imágenes digitales
* @author Beto González
*
*/
public class ProcesadorDeImagenes extends Canvas {
Image imagenBase;
Image imagenModificada;
String mensajeDeError = "";
String tipoDeImagen = "";
/**
* @Desc Método que permite agregar una imagen al procesador que es recibida como parámetro
* @param imagen
*/
public void estableceImagen(Image imagen)
{
imagenBase = imagen;
imagenModificada = null;
tipoDeImagen = (String)imagenBase.getProperty("type", this);
}
/**
* @Desc Método que permite agregar una imagen al procesador directamente desde un archivo de imagen
* @param imagen
*/
public Image cargaImagen(String ruta, String nombreDeArchivo)
{
imagenBase = Toolkit.getDefaultToolkit().getImage(ruta);
imagenModificada = null;
String[] partes = null;
partes = nombreDeArchivo.split("\\.");
int tope = partes.length;
if(tope > 1)
tipoDeImagen = partes[tope - 1];
return imagenBase;
}
/**
* @Desc Método que modifica el brillo de la imagen base contenida a partir del valor de intesidad recibido
* @param intensidad
* @return Verdadero si todo salió bien, falso en caso de error
*/
public boolean modificaBrillo(int intensidad)
{
boolean estado = true;
int p, rojo, verde, azul;
int a = imagenBase.getWidth(this);  //Ancho
    int h = imagenBase.getHeight(this); //Alto
    int totalDePixeles = a * h;   
int pixeles[] = new int[totalDePixeles];   //Arreglo de pixeles
PixelGrabber pg = new PixelGrabber(imagenBase,0,0,a,h,pixeles,0,a);
try
{
pg.grabPixels();
for(int i = 0; i < totalDePixeles; i++)
{
p = pixeles[i]; //Valor de un pixel
rojo = (0xff & (p>>16)) + intensidad;  //Desplaza el entero p 16 bits a la derecha y aplica la operacion AND a los primeros 8 bits
verde = (0xff & (p>>8)) + intensidad;  //Desplaza el entero p 8 bits a la derecha  y aplica la operacion AND a los siguientes 8 bits
azul = (0xff & p) + intensidad;        //Aplica la operacion AND a los siguientes 8 bits
if(rojo>255) rojo=255;
if(verde>255) verde=255;
if(azul>255) azul=255;
if(rojo<0) rojo=0;
if(verde<0) verde=0;
if(azul<0) azul=0;
pixeles[i]=(0xff000000|rojo<<16|verde<<8|azul);
}
imagenModificada  = createImage(new MemoryImageSource(a,h,pixeles,0,a));
}catch(InterruptedException e)
{
//JOptionPane.showMessageDialog((Component)null,"Error del sistema : "+e.getMessage(),"Error de Imagen",JOptionPane.OK_OPTION);
estado = false;
this.mensajeDeError = e.getMessage();
}
return estado;
}
/**
* @Desc Método que convierte la imagen base contenida en una imagen a escala de grises
* @return Verdadero si todo salió bien, falso en caso de error
*/
public boolean escalaDeGrises()
{
    boolean estado = true;
int p, promedio, rojo, verde, azul;
int a = imagenBase.getWidth(this);  //Ancho
    int h = imagenBase.getHeight(this); //Alto
    int totalDePixeles = a * h;
int pixeles[] = new int[totalDePixeles];   //Arreglo de pixeles
PixelGrabber pg = new PixelGrabber(imagenBase,0,0,a,h,pixeles,0,a);
try
{
pg.grabPixels();
for(int i = 0; i < totalDePixeles; i++)
{
p = pixeles[i]; //Valor de un pixel
rojo = (0xff & (p>>16));  //Desplaza el entero p 16 bits a la derecha y aplica la operacion AND a los primeros 8 bits
verde = (0xff & (p>>8));  //Desplaza el entero p 8 bits a la derecha  y aplica la operacion AND a los siguientes 8 bits
azul = (0xff & p) ;        //Aplica la operacion AND a los siguientes 8 bits
promedio = (int) ((rojo+verde+azul)/3);
pixeles[i]=(0xff000000|promedio<<16|promedio<<8|promedio);
}
imagenModificada  = createImage(new MemoryImageSource(a,h,pixeles,0,a));
}catch(InterruptedException e)
{
//JOptionPane.showMessageDialog((Component)null,"Error del sistema : "+e.getMessage(),"Error de Imagen",JOptionPane.OK_OPTION);
estado = false;
this.mensajeDeError = e.getMessage();
}
return estado;
}
/**
* @Desc Método que modifica los colores de la imagen base contenida a partir de los valores de intensidad de los colores rojo, verde y amarillo recibidos
* @param iRojo
* @param iVerde
* @param iAzul
* @return Verdadero si todo salió bien, falso en caso de error
*/
public boolean modificaColor(int iRojo, int iVerde, int iAzul)
{
boolean estado = true;
int p, rojo=0, verde=0, azul=0;
int a = imagenBase.getWidth(this);  //Ancho
    int h = imagenBase.getHeight(this); //Alto
    int totalDePixeles = a * h;
int pixeles[] = new int[totalDePixeles];   //Arreglo de pixeles
PixelGrabber pg = new PixelGrabber(imagenBase,0,0,a,h,pixeles,0,a);
try
{
pg.grabPixels();
for(int i = 0;i<(a*h);i++)
{
p = pixeles[i]; //Valor de un pixel
rojo = (0xff & (p>>16)) + iRojo;
verde = (0xff & (p>>8)) + iVerde;
azul = (0xff & p) + iAzul;
if(rojo>255) rojo=255;
if(verde>255) verde=255;
if(azul>255) azul=255;
if(rojo<0) rojo=0;
if(verde<0) verde=0;
if(azul<0) azul=0;
pixeles[i]=(0xff000000|rojo<<16|verde<<8|azul);
}
imagenModificada = createImage(new MemoryImageSource(a,h,pixeles,0,a));
}catch(InterruptedException e)
{
estado = false;
this.mensajeDeError = e.getMessage();
}
return estado;
}
/**
* @Desc Método que almacena la imagen contenida en una archivo de imagen, de acuerdo a la información del archivo que recibe como parámetro
* @param ruta
* @param nombreDeArchivo
* @param tipoDeImagen
* @return
*/
public boolean guardaImagen(String ruta, String nombreDeArchivo, String tipoDeImagen)
{
boolean estado = true;
BufferedImage imagen = creaBufferedImage((imagenModificada != null) ? imagenModificada : imagenBase);
try {
ImageIO.write(imagen, tipoDeImagen, new File(ruta));
} catch (IOException e) {
estado = false;
this.mensajeDeError = e.getMessage();
}
return estado;
}
/**
* @Desc Versión por defecto del método guardaImagen
* @param ruta
* @param nombreDeArchivo
* @return
*/
public boolean guardaImagen(String ruta, String nombreDeArchivo)
{
String[] partes = null;
partes = nombreDeArchivo.split("\\.");
int tope = partes.length;
if(tope > 1)
tipoDeImagen = partes[tope - 1];
return guardaImagen(ruta, nombreDeArchivo, tipoDeImagen);
}
/**
* @Desc Método que devuelve la imagen modificada por el procesador en un objeto de la clase Image
* @return
*/
public Image devuelveImagenModificada()
{
return imagenModificada;
}
/**
* @Desc Método que devuelve la imagen base dentro de un objeto de la clase Image
* @return
*/
public Image devuelveImagenBase()
{
return imagenBase;
}
/**
* @Desc Método que retora el último mensaje de error producido por los métodos de la clase
* @return
*/
public String devuelveMensajeDeError()
{
return mensajeDeError;
}
/**
* @Desc Versión por defecto del método creaBufferedImage
* @param imageIn
* @return El objeto BufferedImage
*/
public BufferedImage creaBufferedImage(Image imagenDeEntrada) {
return creaBufferedImage(imagenDeEntrada, BufferedImage.TYPE_INT_RGB);
}
/**
* @Desc Método para convertir un objeto Image a un objeto BufferedImage
* @param imageIn
* @param imageType
* @return El objeto BufferedImage
*/
public BufferedImage creaBufferedImage(Image imagenDeEntrada, int imageType) {
    BufferedImage bufferedImageDeSalida = new BufferedImage(imagenDeEntrada.getWidth(this), imagenDeEntrada.getHeight(this), imageType);
    Graphics g = bufferedImageDeSalida.getGraphics();
    g.drawImage(imagenDeEntrada, 0, 0, null);
    return bufferedImageDeSalida;
}
}


Como ya mencionamos esta clase trabajo al nivel más bajo de la aplicación directamente con los pixeles de las imagenes. Para lograr esto utiliza dos objetos de las clase Image (imagenBase e imagenModificada), para almacenar tanto las imágenes que son leídas tanto como la imagen que resulta del procesamiento (respectivamente). Los métodos estableceImagen() y cargarImagen() le permiten tener una imagen sobre la cual llevar a cabo el procesamiento que ocurre en los otros métodos.

Los métodos modificaBrillo(), escalaDeGrises() y modificaColores() llevan a cabo los cambios en los pixeles de la imagen, aplicando una serie de operaciones a cada uno de sus valores de intensidad de color, almacenando los valores de los pixeles dentro de un arreglo de número enteros y recorriéndolo dentro de un ciclo for. De esta forma, cada uno de los pixeles es tratado como un número entero, para el cual los valores de la intensidad del color estarán contenidos en sus primeros tres grupos de 8 bits. Es decir, los primero 8 bits menos significativos del entero representarán la intensidad del azul, los siguientes 8 bits los del verde y los 8 bits que le siguen los del rojo. Es por esto que en el código vemos que se lleva a cabo un desplazamiento a nivel de bits del valor de un pixel, para junto con la operación AND obtener los 8 bits del valor de cada uno de los tres colores del pixel:

p = pixeles[i]; //Valor de un pixel
rojo = (0xff & (p>>16)) + intensidad; //Desplaza el entero p 16 bits a la derecha y aplica la operacion AND a los primeros 8 bits
verde = (0xff & (p>>8)) + intensidad; //Desplaza el entero p 8 bits a la derecha y aplica la operacion AND a los siguientes 8 bits
azul = (0xff & p) + intensidad; //Aplica la operacion AND a los siguientes 8 bits


El código anterior es el que encontramos en el método modificaBrillo(), y lo que vemos que se hace es justamente sumarle una misma cantidad a cada uno de los valores de color del pixel para de esta forma obtener un pixel más iluminado, para después almacenar este pixel obtenido dentro del arreglo de pixeles.

En los otros dos métodos se sigue una lógica parecida, con la única diferencia de que para modificaColores, se aumentará la intensidad de los colores de los pixeles con un valor distinto para cada color, mientras que en escalaDeGrises se promediará el valor de los tres colores de cada pixel y se almacenará el mismo resultado dentro de los tres valores de color del pixel, para de esta forma obtener el equivalente en escala de grises del color original.

Implementación final

Ahora bien, visto el código del Controlador y el Modelo de la aplicación, solo nos queda juntarlo todo con la Vista para echar a correr el programa final. Para hacer eso, tendremos que modificar el código de la clase EditorImg que realizamos en el artículo anterior, agregando las líneas que se remarcan en el siguiente código:

EditorImg.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
* Descripción: Esta clase implementa un editor básico de imagenes .jpg y .gif,
* utilizando componentes Swing y clases de la libreria image.
* @author Alberto González Espinoza
* @version 1.0
* @category Multimedia
*/
public class EditorImg extends JFrame{
static final long serialVersionUID=10000;
PanelSwing panel;
Controlador controlador;
public static void main(String[] args) {
// TODO Auto-generated method stub
EditorImg editor = new EditorImg();
editor.setBounds(120, 120, 800, 600);
editor.setVisible(true);
editor.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
editor.addWindowListener(new WindowAdapter()
{
public void WindowCloser(WindowEvent e)
{
System.exit(0);
}
});
}

/**
* @Desc Constructor de la clase
*/
EditorImg() {
super("Editor básico de imagenes");
Container contentPane = getContentPane();
panel = new PanelSwing(this);
controlador = new Controlador(panel);
panel.abrir.addActionListener(controlador);
panel.guardar.addActionListener(controlador);
panel.salir.addActionListener(controlador);
panel.brillo.addActionListener(controlador);
panel.escala.addActionListener(controlador);
panel.color.addActionListener(controlador);
panel.jslBrillo.addChangeListener(controlador);
panel.jslRojo.addChangeListener(controlador);
panel.jslVerde.addChangeListener(controlador);
panel.jslAzul.addChangeListener(controlador);

contentPane.add(panel);
}
}


Y como vemos, lo que se hace es simplemente agregar el Controlador a la aplicación, instanciando un objeto de la clase Controlador, y asignando la gestión de eventos del menú principal y de los componentes JSlider a este objeto.

Una vez listo el paso anterior, podemos probar correr el programa y deberemos ver la misma pantalla que en el artículo anterior, con la diferencia de que ahora ya podremos cargar, editar y modificar las imágenes.

Por ejemplo, así es como se visualiza el cargado de las imágenes:

Aquí vemos una imagen ya cargada:

Si modificamos los colores:

O el brillo:

Y aquí como se ve en escala de grises:

Al final solo tenemos que salvar la imagen para concluir con el proceso de edición.


Bien, el código está totalmente a su disposición y es obviamente propenso a mejoras :D, ya que las funciones de procesamiento que aquí implementamos son algunas de las que se consideran básicas, pero con estos principios es posible incursionar en conceptos más avanzados de procesamiento de imágenes, en particular le veo mucho potencial a la clase ProcesadorDeImagenes para seguirla desarrollando e incluir un amplio abanico de operaciones de procesamiento de imágenes digitales, y que gracias a la abstracción de su diseño, pueda ser llevada a otras aplicaciones de forma transparente.

Y por hoy es todo, posiblemente este sea el último post de este año 2009, ya que la próxima semana ya estaré de vacaciones y tendré mi agenda un poco ocupada, pero espero que para el año que entra tendré mayor tiempo y decisión XD para seguir explorando las posibilidades de las herramientas de software libre.

Saludos desde la cabaña y hasta la próxima.

martes, 15 de septiembre de 2009

Tratamiento de imágenes en Java, Parte 1: Introducción y Vista


¡Vamos a ponerle color a Java!


Hola de nuevo blogueros, después de una larga sequía de varios meses ahora traigo el código fuente para la creación de un sencillo editor de imágenes en Java, el cual servirá para dos propósitos, como son el ilustrar el potencial del manejo de archivos e imágenes con Java y conocer algunos componentes adicionales de las interfaces de usuario de Swing.

Sobre la edición de imágenes

Las imágenes digitales consisten en un conjunto de valores numéricos ordenados en forma de arreglos o matrices, cada uno de estos valores representa el contenido del color de un pixel.


Zoom a una imagen digital con el que se aprecian sus pixeles

Los pixeles son los puntos de color que se despliegan en la pantalla al reproducir una imagen en nuestra PC. Al observar estos puntos en conjunto podemos apreciar la imagen en su totalidad, la cual no es otra cosa que una representación digital de una imagen del mundo real, pero que nuestro cerebro interpreta como si se tratara de la imagen original.

Podemos distinguir tres tipos de imágenes por las características de sus pixeles, estás son las las imágenes en escala de grises, las imágenes en blanco y negro y las imágenes a color.


Imagen a escala de grises

A nivel de programación encontramos que lo que las diferencia es la forma en la que varían los valores de los pixeles y cómo son interpretados por nuestra PC.

En las imágenes a color tenemos un conjunto de ternas de valores para representar el color de cada pixel que será visualizado. Así, en una imagen a color un pixel está formado en realidad por tres valores que representan la intensidad de los colores rojo, verde y azul. Al combinar estos tres valores se puede obtener un color diferente a los tres originales, y haciéndolos variar en su intensidad podemos obtener todo un gran conjunto de colores para dibujar. La forma en la que estos colores son combinados obedece al modelo de color RGB, que es el que se utiliza más frecuentemente con las imágenes digitales.


Mezcla activa de colores

En cambio, con las imágenes en escala de grises cada pixel representa un valor equivalente a una graduación de gris. Tal y como dice el artículo enlazado, "las imágenes representadas de este tipo están compuestas de sombras de grises, que van desde el negro más profundo variando gradualmente en intensidad de grises hasta llegar al blanco."

Una de las características interesantes del procesamiento de imagen es que el posible convertir una imagen a color a una imagen equivalente en escala de grises, aplicando un sencillo algoritmo de procesamiento a sus pixeles. Esto se logra calculando el promedio de las ternas de valores de cada uno de los pixeles de la imagen, y reemplazando los tres valores de cada pixel por el promedio obtenido, de forma que los tres colores tengan un mismo valor de intensidad.

Los pixeles de las imágenes en blanco y negro, son similares a las de escala de grises, con la diferencia de que éstos solo pueden contener uno de dos valores de color posibles, el 255 (blanco) y el 0 (negro).

Otra característica importante, es que si modificamos los valores de intensidad de los pixeles de una imagen, podemos hacer que varíen tanto su brillo como su color. Por ejemplo, para aumentar el brillo de una imagen, podemos sumarle un valor escalar a cada una de las ternas de colores de sus pixeles. Mientras que si deseáramos hacer que variara su color, podemos cambiar la intensidad de una de las tres ternas de color de cada uno de sus pixeles, por ejemplo para hacer más azul una imagen podemos sumarle un escalar a todos los valores de azul de sus pixeles.

Teniendo clara esta diferencia entre los tipos de imagen y lo que es un pixel, ahora podemos pasar a la programación de nuestra aplicación, la cual consistirá en una ventana con un menú de opciones, que permitirá cargar archivos de imágenes de nuestros sistema y gracias a una serie de componentes para interfaces gráficas, darles un pequeño tratamiento a estas imágenes. El programa además tendrá la capacidad para salvar las imágenes una vez que hayan sido modificadas por el usuario.


Manos a la obra

Nuevamente nos basaremos en el esquema MVC (Modelo, Vista, Controlador) para la construcción del software, comenzando con la parte de la vista para nuestra ventana y nuestros paneles de opciones.

Componentes de la interfaz de usuario:

JSlider. Utilizaremos este componente para crear una pequeña paleta de intensidad de colores y otra de intensidad del brillo, de forma que podamos modificar con los tonos de rojo, verde y azul de las imágenes, así como el brillo que estas proyectan.
JPanel. Utilizaremos este componente primero, para crear todos los paneles en los que se cargarán los componentes, y además crearemos una clase llamada PanelDeImagen que extenderá a la clase JPanel, para que funcione como un lienzo sobre el cual dibujaremos las imágenes que carguemos y editemos en la aplicación.
JScrollPane. Ya que el tamaño de las imágenes puede varias respecto del tamaño de la ventana de nuestra aplicación, utilizaremos un panel de tipo JScrollPane, el cual tiene la característica de hacer aparecer barras de desplazamiento en sus bordes cuando su contenido excede a su tamaño en pantalla, de forma que podemos cargar imágenes más grandes que nuestra ventana sin problemas.
JMenuBar, JMenu y JMenuItem. Estos componentes nos permitirán crear una barra de menú en el panel superior de nuestra aplicación, el cual contendrá las opciones relacionadas con los archivos, como son abrir un archivo de imagen, guardar la imagen editada, salir de la aplicación. También permitirán activar los paneles que contendrán las opciones para la edición de nuestras imágenes.
JFileChooser. Este componente adicional, nos auxiliará para hacer más sencilla la selección y el guardado de archivos de imágenes, ya que con los objetos de esta clase podemos llamar una ventana de selección de archivos, que nos permitirá elegir los archivos que editaremos en nuestra aplicación de forma visual.

La Vista

El código para implementar la vista de la aplicación aparecerá a continuación, separando cada una de las clases en un apartado.

Código para la implementación de la ventana de la aplicación:

EditorImg.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
/**
* Descripción: Esta clase implementa un editor básico de imágenes .jpg y .gif,
* utilizando componentes Swing y una serie de clases para el manejo y procesamiento de imagenes digitales.
* @author Beto González
* @version 1.0
* @category Multimedia
*/
public class EditorImg extends JFrame{
static final long serialVersionUID=10000;
PanelSwing panel;
public static void main(String[] args) {
// TODO Auto-generated method stub
EditorImg editor = new EditorImg();
editor.setBounds(120, 120, 800, 600);
editor.setVisible(true);
editor.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
editor.addWindowListener(new WindowAdapter()
{
public void WindowCloser(WindowEvent e)
{
System.exit(0);
}
});
}

/**
* @Desc Constructor de la clase
*/
EditorImg() {
super("Editor básico de imagenes");
Container contentPane = getContentPane();
panel = new PanelSwing(this);
contentPane.add(panel);
}
}


Código para implementar el lienzo sobre el que se dibujará la imagen:

PanelDeImagen.java
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
/**
* @Desc Clase que extiende a la clase JPanel que se utiliza para crear un panel que permita visualizar imágenes en su interior
* @author Beto González
*
*/
public class PanelDeImagen extends JPanel{
static final long serialVersionUID=10000;
Image img;
Dimension tamaño;
JScrollPane base;
/**
* @Desc Constructor de la clase
*/
PanelDeImagen()
{
setBackground(Color.white);
}
/**
* @Desc Método a través del cual la clase recibe el objeto de la imagen que será visualizada en su interior
* @param i
*/
public void estableceImagen(Image i)
{
img = i;
}
/**
* @Desc Método a través del cual la clase obtiene la referencia hacia el panel en el cual se encuentra contenido.
* @param i
*/
public void estableceBase(JScrollPane contenedor)
{
base = contenedor;
}
/**
* @Desc Método extendido que es llamado cada ves que un objeto de esta clase llama al método repaint(). A este le agregamos
* una funcionalidad adicional que le permite redimencionar el panel que contiene la imagen de acuerdo a las dimensiones de
* ésta
*/
public void paintComponent(Graphics g)
{
super.paintComponent(g);
if (img != null)
{
if(base != null)
{
setSize(new Dimension(base.getWidth()-10,base.getHeight()-10));
setPreferredSize(new Dimension(base.getWidth()-10,base.getHeight()-10));
}
tamaño = new Dimension(getWidth(),getHeight());
int x = tamaño.width - img.getWidth(this);
while (x < 0)
{
tamaño.setSize(tamaño.width+1, tamaño.height);
x = tamaño.width - img.getWidth(this);
}
if(x > 0)
  x = (int) x/2;
int y = tamaño.height - img.getHeight(this);
while (y < 0)
{
tamaño.setSize(tamaño.width, tamaño.height+1);
y = tamaño.height - img.getHeight(this);
}
if(y > 0)
  y = (int) y/2;

if(!getSize().equals(tamaño))
{
setSize(tamaño);
setPreferredSize(tamaño);
}
g.drawImage(img, x, y, this);
}
}
}


La funcionalidad de los objetos de la clase PanelDeImagen consistirá en que recibirán una imagen por medio del método estableceImagen() y la dibujarán en su panel al llamar al método repaint() que heredaron de la clase JPanel. Ya que en esta implementación el panel de las imagenes estará contenida dentro de un objeto JScrollPane, es que además contamos con el método estableceBase(), con el cual podemos guardar una referencia a este panel. Además, debido a que las dimensiones de las imagenes cargada por la aplicación pueden, es que extendimos al método paintComponent(), el cuál además de encargarse de dibujar la imagen dentro del panel, modificará las dimensiones de este y del panel JScrollPane que lo contenga, de forma que se adapten al tamaño de las imágenes y estas puedan ser visualizadas en su totalidad en pantalla.

La siguiente clase implementa la totalidad de los componentes de la interfaz gráfica de la aplicación:

PanelSwing.java
import java.awt.*;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* @Desc Clase utilizada para crear los componentes de la interfaz gráfica de la aplicación
* @author Beto González
*
*/
public class PanelSwing extends JPanel {
static final long serialVersionUID = 10000;
String nombreArchivo, ruta;
JMenuBar barraMenu;
JMenu menuArchivo, menuEdicion;
JMenuItem abrir, guardar, salir, brillo, color, escala;
JScrollPane panelDespl;
JPanel panelBajo, panelBrillo, panelColor, panelVacio;
int altura = 80;
Image imagen;
Image imgAux;
EditorImg editor;
PanelDeImagen lienzo;
JSlider jslBrillo, jslRojo, jslVerde, jslAzul;
JLabel lblRojo, lblVerde, lblAzul;
CardLayout esqueInf;
/**
* @Desc Constructor de la clase
* @param editor
*/
PanelSwing(EditorImg editor)
{
this.editor = editor;
this.setLayout(new BorderLayout());
barraMenu = new JMenuBar();
FlowLayout f = new FlowLayout();
f.setAlignment(FlowLayout.LEFT);
barraMenu.setLayout(f);
menuArchivo = new JMenu("Archivo");
menuEdicion = new JMenu("Edición");
abrir = menuArchivo.add("Abrir");
guardar = menuArchivo.add("Guardar");
guardar.setEnabled(false);
menuArchivo.addSeparator();
salir = menuArchivo.add("Salir");
brillo = menuEdicion.add("Ajustar Brillo");
color = menuEdicion.add("Ajustar Colores");
escala = menuEdicion.add("Escala de Grises");
brillo.setEnabled(false);
color.setEnabled(false);
escala.setEnabled(false);
barraMenu.add(menuArchivo);
barraMenu.add(menuEdicion);
this.add("North",barraMenu); //Agregamos la barra de menu
creapanelCentral(); //Creamos el panel en el que se mostrara la imagen seleccionada
creapanelBajo(); //Creamos el panel en el que se mostraran los controles para manipular la imagen
}
/**
* @Desc Método que crea el contenido del panel central de la ventana
*/
private void creapanelCentral()
{
lienzo = new PanelDeImagen();
panelDespl = new JScrollPane(lienzo);
lienzo.estableceBase(panelDespl);
add("Center",panelDespl);
}
/**
* @Desc Método que crea el contenido del panel inferior de la ventana
*/
private void creapanelBajo()
{
panelBajo = new JPanel();
esqueInf = new CardLayout();
panelBajo.setLayout(esqueInf);
panelBajo.setPreferredSize(new Dimension(this.getWidth(),altura));
jslBrillo = new JSlider(SwingConstants.HORIZONTAL,0,100,0);
jslBrillo.setPaintTicks(true);
jslBrillo.setPaintLabels(true);
jslBrillo.setMajorTickSpacing(10);
jslBrillo.setMinorTickSpacing(5);
panelColor = new JPanel();
panelVacio = new JPanel();
panelBrillo = new JPanel(new BorderLayout());
panelBrillo.add("Center", new JLabel("Puedes ajustar el brillo de la imagen",JLabel.CENTER));
panelBrillo.add("South",jslBrillo);
panelBajo.add("carta1", panelVacio);
panelBajo.add("carta2", panelBrillo);
creaPaletas();
esqueInf.show(panelBajo, "carta1");
this.add("South",panelBajo);
}
/**
* @Desc Método que crea el contenido del panel inferior de la ventana
*/
private void creaPaletas()
{
GridBagLayout gridbag = new GridBagLayout();
GridBagConstraints constrain = new GridBagConstraints();
panelColor.setLayout(gridbag);
lblRojo = new JLabel("Rojo");
lblVerde = new JLabel("Verde");
lblAzul = new JLabel("Azul");
constrain.gridx = 0; constrain.gridy = 0;
constrain.gridheight = 1; constrain.gridwidth = 2;
gridbag.setConstraints(lblRojo, constrain);
panelColor.add(lblRojo);
constrain.gridx = 2; constrain.gridy = 0;
gridbag.setConstraints(lblVerde, constrain);
panelColor.add(lblVerde);
constrain.gridx = 4; constrain.gridy = 0;
gridbag.setConstraints(lblAzul, constrain);
panelColor.add(lblAzul);
jslRojo = new JSlider(SwingConstants.HORIZONTAL,0,50,0);
jslVerde = new JSlider(SwingConstants.HORIZONTAL,0,50,0);
jslAzul = new JSlider(SwingConstants.HORIZONTAL,0,50,0);
constrain.gridx = 0; constrain.gridy = 1;
constrain.gridheight = 1; constrain.gridwidth = 2;
gridbag.setConstraints(jslRojo, constrain);
panelColor.add(jslRojo);
constrain.gridx = 2; constrain.gridy = 1;
gridbag.setConstraints(jslVerde, constrain);
panelColor.add(jslVerde);
constrain.gridx = 4; constrain.gridy = 1;
gridbag.setConstraints(jslAzul, constrain);
panelColor.add(jslAzul);
panelBajo.add("carta3", panelColor);
}
}


Hasta este punto hemos completado la parte de la Vista de la aplicación, y nos quedan pendientes el Controlador y el Modelo. Sin embargo, para poder apreciar realmente la funcionalidad de las clases anteriores es necesario probarlas, echando a correr el programa, de forma que podamos visualizarlas y obtener un resultado como el siguiente:



En esta captura, podemos apreciar los tres paneles principales de la aplicación, el menú de opciones en la parte superior, el panel central que contiene los paneles de los componentes de las clases JScrollPane y PanelDeImagen, y finalmente el panel inferior, en el cual se desplegarán los componentes visuales para la edición de las imagenes, una vez que el Controlador y el Modelo estén trabajando.

En el siguiente artículo concluiremos con esta implementación.

Hasta pronto.

viernes, 21 de agosto de 2009

Diseño de interfaces gráficas en Java, Parte 5: Completando el Modelo


Prepárense para construir el Modelo

De regreso, bien, en este punto ya debe estar corriendo el servidor de base de datos PostgreSQL y la base de datos con su respectiva tabla debe estar lista y ésta última debe contar con algunos registros para la prueba. También los programas en Java de nuestro proyecto deben contar con la referencia al archivo JAR que descargamos e instalamos en el artículo anterior. Ahora solo queda concluir con la construcción del Modelo programando las capas de negocio y de acceso a datos de la aplicación.

Una vez más, retomamos el mismo proyecto que hemos ido construyendo en esta serie de post sobre interfaces gráficas en Java.

Primero, programaremos una clase para crear objetos de datos durante la ejecución del programa, es decir objetos cuyo único propósito sea transportar información a través de las capas de la aplicación. En este caso la clase se llamará InfoUsuario, y la utilizaremos para que sus objetos almacenen los datos extraídos de la tabla usuario de la base de datos:

InfoUsuario.java
/**
* @Desc Clase utilizada como una estructura de datos para alamacenar la información de los registros que
* consultemos de la base de datos
* @author Beto González
*/
public class InfoUsuario {
    public String nombre;
    public String correoElectronico;
    public String domicilio;
    public String telefono;
    public int edad;
}


Después crearemos una clase para manejar el acceso a la base de datos desde nuestra aplicación, incluyendo las consultas a la tabla requerida. El código fuente es el siguiente:

AccesoADatos.java
import java.sql.*;
import java.util.*;
/**
* @Desc Clase utilizada para acceder a la base de datos y realizar consultas sobre sus tablas
* @author Beto González
*/
public class AccesoADatos {
    private String usuario;
    private String contraseña;
    private String url;
    private Connection conexion;
    private Vector<InfoUsuario> resultado; //Vector en el que se almacenarán la lista de registros que resulten en una cosulta, con el <InfoUsuario> se especifica que cada uno de sus elementos será un objeto de la clase InfoUsuario
    private String mensaje;
    AccesoADatos(String url, String usuario, String contraseña)     {
        this.usuario = usuario;
        this.contraseña = contraseña;
        this.url = url;
        this.resultado = new Vector<InfoUsuario>();
        this.mensaje = "";
    }
    /**
     * @Desc
     * @return Verdadero si se realizó correctamente, falso en caso contrario
     */
    public boolean conecta() {
        boolean estado = true;
        Properties props = new Properties();
        props.setProperty("user",usuario);
        props.setProperty("password",contraseña);
        try{
            conexion = DriverManager.getConnection(url, props);
        }catch(SQLException e){
            estado = false;
            mensaje = e.toString();
        }
        return estado;
    }
    /**
     * @Desc Método que ejecuta una consulta SQL sobre la tabla usuario
     * @param condicion Condición para la consulta SQL
     * @return Verdadero si se realizó correctamente, falso en caso contrario
     */
    public boolean consultaUsuario(String condicion) {
        boolean estado = true;
        if(!condicion.isEmpty()) {
            try{
                PreparedStatement st = conexion.prepareStatement("SELECT * FROM usuario WHERE " + condicion);
                ResultSet rs = st.executeQuery();
                InfoUsuario iUsuario = new InfoUsuario(); //Objeto para almacenar la información de cada registro de la tabla
                while (rs.next()) {
                    iUsuario.correoElectronico = rs.getString("correoElectronico");
                    iUsuario.nombre = rs.getString("nombre");
                    iUsuario.domicilio = rs.getString("domicilio");
                    iUsuario.telefono = rs.getString("telefono");
                    iUsuario.edad = rs.getInt("edad");
                    resultado.add(iUsuario);
                }
                rs.close();
                st.close();
            }catch(SQLException e){
                estado = false;
                mensaje = e.toString();
            }
        }
        else{
            estado = false;
            mensaje = "Consulta vacía";
        }
        return estado;
    }
    /**
     * @Desc Método que realiza la búsqueda de un usuario a partir de un correo electrónico
     * @param correo Correo electrónico buscado
     * @return Verdadero si se realizó correctamente, falso en caso contrario
     */
    public boolean buscaCorreo(String correo) {
        boolean estado = consultaUsuario("\"correoElectronico\" = '" + correo + "'");
        return estado;
    }
    /**
     * @Desc Método que realiza la búsqueda de un usuario a partir de un nombre
     * @param correo Correo electrónico buscado
     * @return Verdadero si se realizó correctamente, falso en caso contrario
     */
    public boolean buscaNombre(String nombre) {
        boolean estado = consultaUsuario("nombre LIKE '%" + nombre + "%'");
        return estado;
    }
    /**
     * @Desc Devuelve un elemento específicado que se encuentre dentro del Vector de resultados
     * @param indice Indice del elemento dentro del vector
     * @return
     */
    public InfoUsuario regresaInfoUsuario(int indice) {
        if(resultado.size() > 0)
            return resultado.get(indice);
        else
            return null;
    }
    /**
     * @Desc Devuelve el último mensaje de erro generado por el controlador de la base de datos
     * @return Mensaje devuelto por el controlador de la base de datos
     */
    public String regresaMensaje() {
        return mensaje;
    }
    /**
     * @Desc Devuelve el número de elementos del Vector de resultados
     * @return Número de elementos
     */
    public int regresaNumDeRegistros() {
        return resultado.size();
    }
    /**
     * @Desc Método que cierra la conexión con el sistema manejador de base de datos
     */
    public void cierraConexion() {
        try{
            conexion.close();
        }catch(SQLException e){}
    }
}


Esta clase tiene como propósito abstraer al resto de la aplicación de los detalles del acceso a la base de datos, en este caso a través del SMBD PostgreSQL. Como vemos, se incluyen métodos para abrir y cerrar la conexión con el servidor, un método llamado consultaUsuario(), para ejecutar consultas sobre la tabla usuario a partir de la condición especificada, almacenando la información de los registros resultantes en la propiedad resultado, así como el método regresaInfoUsuario() que precisamente extrae la información de los registros almacenados en esta propiedad. El objeto resultado pertenece a la clase Vector, y funciona como un arreglo en el que se almacenará una lista de objetos de la clase InfoUsuario que ya definimos previamente. De forma que a cada registro resultante le corresponderá un nodo en el vector.

La tercera clase que crearemos se encargará de implementar una pequeña lógica de negocio, se llamará Buscador y en ésta se recibirá la información capturada por el usuario, con la cual se decidirá qué método de la capa de acceso a datos deberá ejecutarse para llevar a cabo la búsqueda de un usuario y retornar su información a la capa de presentación.

Buscador.java
import java.awt.Component;
import javax.swing.JOptionPane;
/**
* @Desc Clase que implementa las busquedas de información en el sistema a través de un objeto de la clase AccesoADatos
* @author Beto González
*/
public class Buscador {
    /**
     * @Desc Método que busca la información de un usuario a través de un objeto de la clase AccesoADatos
     * @param correoElectronico Correo electrónico del usuario
     * @param nombre Nombre del usuario
     * @return Un objeto de la clase InfoUsuario, que contendrá la información del usuario solicitado
     */
    public InfoUsuario buscaUsuario(String correoElectronico, String nombre) {
        InfoUsuario info = new InfoUsuario();
        AccesoADatos a = new AccesoADatos("jdbc:postgresql://localhost/pruebas","postgres","myhouse6");
        boolean estado = true;
        if(a.conecta()) {
            if(!nombre.isEmpty()) {
                if((estado = a.buscaNombre(nombre)) && a.regresaNumDeRegistros() > 0)
                    info = a.regresaInfoUsuario(0);
            }
            else if(!correoElectronico.isEmpty()) {
                if((estado = a.buscaCorreo(correoElectronico)) && a.regresaNumDeRegistros() > 0)
                    info = a.regresaInfoUsuario(0);
            }
            a.cierraConexion();
        }
        if(!estado) {
            JOptionPane.showMessageDialog((Component)null,"Error de acceso a la base de datos " + a.regresaMensaje(),"Error",JOptionPane.OK_OPTION);
        }
        if(a.regresaNumDeRegistros() == 0) {
            JOptionPane.showMessageDialog((Component)null,"No se encontraron registros con los datos capturados","Alerta",JOptionPane.WARNING_MESSAGE);
        }
        return info;
    }
}


En el método buscaUsuario() vemos que se instancía la clase AccesoADatos y sus métodos son utilizándolos según el contenido de la información recibida como parámetro, así si se recibió un nombre de usuario válido para la búsqueda se utilizará el método buscaNombre(), en caso contrario si el correo electrónico es válido, se empleará el método buscaCorreo(). Ambos métodos devolverán el objeto de datos de la clase InfoUsuario, cuyas propiedades contienen la información del usuario consultado. Finalmente se incluye un pequeño control de los mensajes de error generados, para indicar los errores de acceso a datos y las consultas que no arrojaron resultados.

La información devuelta por el método buscaUsuario() deberá llegar hasta la capa de presentación de la aplicación, por lo que dentro de los programas Controlador.java y PanelDeDatos.java realizaremos los siguientes cambios:

PanelDeDatos.java
import javax.swing.*;
import java.awt.*;
/**
* @Desc Clase utilizada para desplegar ventanas emergentes dentro del sistema que contendrán la información establecida
* en un objeto JPanel pasado por el constructor
* @author Beto González
*
*/
public class PanelDeDatos extends JPanel {
    private JLabel lblNombre;
    private JLabel lblCorreoElectronico;
    private JLabel lblDomicilio;
    private JLabel lblEdad;
    private JLabel lblTelefono;
    //Constructor de la clase
    PanelDeDatos () {
        this.lblNombre = new JLabel("Nombre del usuario: ");
        this.lblCorreoElectronico = new JLabel("Correo electronico: ");
        this.lblDomicilio = new JLabel("Domicilio: ");
        this.lblEdad = new JLabel("Edad: ");
        this.lblTelefono = new JLabel("Telefono: ");
        this.setLayout(new GridBagLayout());
    }
    /**
     * @Desc Método que agrega al panel principal cada una de las etiquetas declaradas
     */
    public void estableceContenido(InfoUsuario datos) {
        lblNombre.setText(lblNombre.getText() + datos.nombre);
        lblCorreoElectronico.setText(lblCorreoElectronico.getText() + datos.correoElectronico);
        lblDomicilio.setText(lblDomicilio.getText() + datos.domicilio);
        lblEdad.setText(lblEdad.getText() + Integer.toString(datos.edad));
        lblTelefono.setText(lblTelefono.getText() + datos.telefono);

        GridBagConstraints delimitador = new GridBagConstraints(); //Creamos un delimitador que usaremos para especificar la ubicación de los componentes dentro del panel
        delimitador.gridx = 0;
        delimitador.gridy = 0;
        delimitador.anchor = GridBagConstraints.EAST;
        this.add(lblNombre, delimitador);
        delimitador.gridy = 1;
        this.add(lblCorreoElectronico, delimitador);
        delimitador.gridy = 2;
        this.add(lblDomicilio, delimitador);
        delimitador.gridy = 3;
        this.add(lblEdad, delimitador);
        delimitador.gridy = 4;
        this.add(lblTelefono, delimitador);
    }
}


En la clase PanelDeDatos adecuamos el método estableceContenido() para que reciba un objeto de la clase InfoUsuario y actualice el contenido de sus etiquetas con los datos de éste.

Controlador.java
import javax.swing.JFrame;
import java.awt.event.*;
/**
* @Desc Clase que maneja los eventos realizados en la Vista implementando una serie de interfaces nativas del lenguaje
* @author Beto González
*
*/
public class Controlador implements ActionListener, MouseMotionListener {
    public Ventana ventana;
    Controlador(Ventana ventana) {
        this.ventana = ventana;
    }
    /**
     * @Desc método perteneciente a la interface ActionListener que es implementado para controlar las acciones sobre los botones
     * ya que se ejecutará si se hace clic sobre el botón que este atendiendo
     */
    public void actionPerformed(ActionEvent ie) {
        if(ie.getActionCommand() == "Buscar usuario") { //Validación para asegurarse que la acción fue realizada sobre el botón Buscar
            ventana.panelPrincipal.lblMensaje.setText("Has hecho clic en el bóton de búsqueda");
            //Desplegamos una ventana emergente que contendrá la información de un panel de la clase PanelDeDatos
            PanelDeDatos panel = new PanelDeDatos();
            Buscador buscador = new Buscador();
            InfoUsuario datos = buscador.buscaUsuario(ventana.panelPrincipal.txtCampo2.getText().trim(), ventana.panelPrincipal.txtCampo1.getText().trim()); //Buscamos la información del usuario según los datos capturados
            panel.estableceContenido(datos);
//Pasamos la información consultada al panel de datos
            VentanaEmergente emergente = new VentanaEmergente("Información del usuario", panel);
            emergente.setBounds(180, 180, 300, 250);
            emergente.setVisible(true);
            emergente.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            emergente.addWindowListener(new WindowAdapter()
            {
                public void WindowCloser(WindowEvent e)                 {
                    System.exit(0);
                }
            });
        }
    }
     /**
     * @Desc Método ejecutado cuando el usuario arrastra el ratón presionando el botón de acción
     */
    public void mouseDragged(MouseEvent e) {
        ventana.panelPrincipal.lblMensaje.setText("Ratón arrastrado");
    }
    /**
     * @Desc Método ejecutado cuando el usuario solo arrastra el ratón
     */
    public void mouseMoved(MouseEvent e) {
        ventana.panelPrincipal.lblMensaje.setText("Posición del puntero X:"
         + Integer.toString(e.getX()) + " Y:"
         + Integer.toString(e.getY()));
    }
}


En la clase Controlador modificamos el método actionPerformed() para que se incluya un objeto de la clase Buscador que proporcione la información del usuario al objeto panel.

Ahora solo queda probar todos estos cambios, el resultado al ejecutar la aplicación deberá verse al capturar un nombre un un correo en el formulario y presionar el botón "Buscar", tal y como se muestra en la siguiente imagen:


En este caso, buscamos un usuario con nombre María. Debido a que en la base de datos existe una registro con nombre 'María Rodríguez' la aplicación nos arrojó un resultado satisfactorio, gracias a que en el nivel de acceso a datos la búsqueda se realiza utilizando un LIKE '%%', aunque claro que el usuario será ajeno a todos esos detalles.

En caso de buscar algún nombre inexistente, el sistema nos debe desplegar un mensaje como el siguiente:


También pueden probar la búsqueda por correo electrónico, en donde la coincidencia con la información almacenada sí deberá ser exacta.

Claro que a este programa se le pueden hacer múltiples mejoras, como por ejemplo en la búsqueda por nombre desplegar una lista con la información de todos los usuarios, en lugar del primer usuario consultado, sin embargo el ejemplo cumple con el propósito de mostrar una forma para separar nuestras aplicaciones en una Vista, un Controlador y un Modelo, así como trabajar con capas verticalmente, yendo desde la base de datos hasta la presentación. Un aspecto que me agrada mucho de trabajar con objetos y que aquí se ve reflejado, es el encapsulamiento que proporcionan, es decir que los detalles de las tareas realizadas por una capa de más bajo nivel, quedan fuera del conocimiento de las capas superiores, a estas solo les interesarán los datos o mensajes que las capas de abajo arrojen al realizar sus tareas.

Con este último post es que finalizamos con el pequeño proyecto que fuimos construyendo a partir de la parte 2 de la programación de interfaces gráficas en Java. Ya vimos algunos detalles interesantes sobre la presentación de los componentes y el manejo de eventos, complementándolos con el acceso a una base de datos. En los siguientes post veremos algunos otros componentes de la interfaz gráfica, y cómo utilizarlos para crear una nueva aplicación para la manipulación de imágenes, que será el proyecto con el que concluiremos con esta serie de artículos sobre Java.

Hasta la próxima.

lunes, 17 de agosto de 2009

Diseño de interfaces gráficas en Java, Parte 4: Creación del Modelo de la aplicación


Primero la capa de datos

¡Saludos nuevamente!

En esta ocasión dejaremos un poco la parte de la presentación y nos enfocaremos en la creación de una pequeña lógica de negocio junto con el acceso a un sistema manejador de base de datos. Aunque podría parecer que me salgo del tema original relacionado con interfaces gráficas, considero que es importante tocar el punto en el que uniríamos el Modelo de la aplicación con la Vista y el Controlador, ya que es el punto al que finalmente se tiene que llegar en todo desarrollo de aplicaciones.

Para lograr este objetivo primero tenemos que considerar la comunicación horizontal de las capas de la aplicación, es decir la forma en que la información llegará desde la capa de acceso a datos hasta la capa de presentación. En el nivel más bajo tendremos un sistema manejador de bases de datos en que estará almacenada nuestra información, en este caso opte por el PostgreSQL, ya que es un gestor en el que apenas me estoy adentrando y nos servirá a todos los novatos para comenzar a dominar un nuevo administrador de base de datos.

Además es un SMBD open source rubusto y por si fuera poco, su mascota es un elefante, y todos sabemos que los elefantes son geniales.

Pueden descargar este sistema desde la sección downloads de la página oficial de PostgreSQL, eligiendo el sistema operativo de su preferencia.

Si descargan la versión para Windows notarán que además hay una aplicación visual de escritorio para administrar las base de datos en PostgreSQL, llamado pgAdmin. Esta aplicación viene incluida dentro del instalador de postgreSQL para este sistema operativo.

Dentro de las características de postgreSQL encontramos que:
- Es un manejador de base de datos dual al ser relacional y orientado a objetos, esto último es una características muy interesante ya que permite manejar conceptos como la herencia entre las tablas.
- Es un sistema multiusuario y multiplataforma.
- Soporta replicación.
- Nos permite utilizar un conjunto de datos complejos, como vectores, xml, figuras geométricas o arreglos, entre otros.
- Permite utilizar varios conceptos avanzados como llaves foráneas, constraints, vistas, transacciones y la propia herencia.
- Implementa un gran conjunto de instrucciones del lenguaje SQL.
- También permite implementar triggers en diferentes lenguajes de programación ajenos al lenguaje nativo procedimental.

La documentación completa de este sistema la encontraremos en la sección Documentation de la página oficial de la plataforma.

Para este ejemplo utilizaremos las funcionalidades estándar, como son crear una base de datos, crear una tabla, añadir registros y consultar.

Iniciando el servidor

Primero asegúrense de que el servidor de PostgreSQL está corriendo, en el caso de un escritorio de Windows pueden echarlo a andar desde Inicio > Todos los programas > PostgreSQL 8.4 > Start Server. Para un S.O. GNU/Linux en la consola de comandos pueden utilizar la instrucción:

#su -
Ingresa tu contraseña de root
#service postgresql initdb
Iniciamos por primera vez el cluster de postgreSQL
#service postgresql start
Iniciamos el servicio de postgreSQL


Si hemos instalado el pgAmin en nuestro escritorio podemos realizar cada uno de los siguientes pasos a través de su interfaz gráfica. En caso contrario también podemos utilizar el Shell de PostgreSQL con los comando. Dentro de Windows pueden encontrar este Shell en Inicio > Todos los programas > PostgreSQL 8.4 > Shell, para Linux basta que desde la consola de comando tenemos que estar logeados como root (tal y como quedamos en el paso anterior) e ingresar los siguientes comandos:

#su - postgres
Nos cambiamos a la cuenta del administrador de postgreSQL
#psql
Entramos al Shell de postgreSQL


Creación de la base de datos

Crearemos una nueva base de datos llamada pruebas. Desde el Shell sería:

postgres=#CREATE DATABASE pruebas WITH OWNER = postgres;


Nota: El siguiente paso es solo necesario para los que esten utilizando el Shell:

Salimos del Shell

postgres=#\q


y volvemos a ingresar, con los pasos que vimos antes, solo que ahora debemos seleccionar la base de datos pruebas que recién creamos. Desde el Shell de Windows bastará con en los parámetros que nos pide capturar en un inicio:

Server [localhost]:
Database [postgres]: pruebas
Port [5432]:
Username[postgres]:


Dejando el resto de los campos sin cambio para que conserven los valores por defecto que se indican. Mientras que en Linux bastará con el comando :

#psql -d pruebas


desde la consola de comandos. Recuerden que tenemos que estar logeados como el usuario postgres.

Creación de la tabla

Ahora, creamos una nueva tabla para esta base de datos llamada usuario, con las características que se muestran en el script:

CREATE TABLE usuario
(
"correoElectronico" character varying(50) NOT NULL,
nombre character varying(60),
domicilio character varying(60),
edad integer,
telefono character varying(20),
CONSTRAINT correo PRIMARY KEY ("correoElectronico")
)
WITH (
OIDS=FALSE
);
ALTER TABLE usuario OWNER TO postgres;


Desde el pgAdmin la creación de tablas se hace accediendo a la base de datos y dentro de Schemas > public seleccionamos la opción New Object > New Table...



Inserción de los registros

Ahora, solo queda insertar algunos registros en la tabla para hacer las pruebas desde nuestra aplicación en Java:

INSERT INTO usuario(
"correoElectronico", nombre, domicilio, edad, telefono)
VALUES
('maria@gmail.com', 'María Rodríguez', 'Av. Libertadores #5464', 19, '(611)6766676'),
('antonio@gmail.com', 'Antonio Márquez', 'Misiones #786', 22, '(611)9898981'),
('estela@gmail.com', 'Estela Saldivar', 'Av. Ruta de la Seda #9004', 25, '1184564');


Controlador de PostgreSQL para Java

De esta forma la base de datos esta preparada para ser utilizada. Ahora solo tenemos que incluir en nuestro proyecto de Java el controlador para conectarse con bases de datos de PostgreSQL. Este lo pueden descargar desde la página oficial del proyecto PostgreSQL > JDBC Driver, el cual consiste en un archivo .jar que contiene una biblioteca de clases para implementar el acceso a las bases de datos de este servidor. Una vez descargado el archivo, para incluirlas en nuestro proyecto de Eclipse, bastará con que configuremos el Build Path del JRE System Library (biblioteca del sistema de JRE) del proyecto, haciendo clic derecho sobre el JRE System Library y seleccionando:



Y dentro de la ventana que aparece seleccionamos el botón "Add External JARs..." con lo cual podremos agregar el archivo .jar descargado a la lista de referencia del proyecto:



Si están utilizando el entorno de desarrollo diferente de Eclipse entonces pueden usar la herramienta que posea para agregar archivos JAR externos.

Si por el contrario, están compilando y probando todos los programas desde la consola de comandos, una solución será modificar el CLASSPATH del programa para incluir la referencia al archivo .jar. En la documentación de la biblioteca que encuentran en la misma página de PostgreSQL > JDBC Driver se especifican los pasos, pero para ahorrar un poco el trabajo sería más o menos así:

Como ejemplo, si tenemos un archivo fuente llamado myapp.jar que contiene la definición de la clase MyApp, este archivo se encuentra en /usr/local/lib/myapp.jar y la biblioteca .jar está en /usr/local/pgsql/share/java/postgresql.jar, la ejecución de la aplicación sería:

#export CLASSPATH=/usr/local/lib/myapp.jar:/usr/local/pgsql/share/java/postgresql.jar:.
#java MyApp


Todo desde la consola de comandos. Otra solución, -que me pareció más sencilla- la encontré en este blog, y consisten en copiar el archivo en el directorio de extensiones de JRE, es decir JRE\lib\ext. Por ejemplo, la ruta absoluta en mi S.O. Windows es C:\Archivos de programa\Java\jre6\lib\ext, y variará según el sistema operativo en el que estén trabajando.

Y con esta referencia ya estaremos listos para pasar a la programación de la aplicación en Java, pero debido a lo mucho que se extendió este post, esa parte quedará pendiente para el siguiente artículo.

Recuerden que para cualquier duda u observación pueden dejar un comentario en este y todos los otros artículos.

Hasta la próxima.

martes, 11 de agosto de 2009

Diseño de interfaces gráficas en Java, Parte 3: Manejo de eventos


El manejo de eventos siempre estará bajo nuestro control

¡Saludos nuevamente blogueros!

La gestión o manejo de eventos en Java, es un mecanismo que podemos incluir en nuestros programas para reconocer las acciones realizadas por los usuarios en las interfaces gráficas, y programar una respuesta en consecuencia.

Algunos de los eventos comunes que nos encontramos en la interfaces son el hacer clic con el ratón sobre un botón o un elemento del menú, utilizar el menú contextual del ratón, o utilizar una combinación de teclas para acciones rápidas, para los cuales podemos preparar una respuesta de nuestro sistema, de modo que gestionaremos estos eventos de la interfaz gráfica. Además, es posible incluir manejo de eventos más especializados, como validar el texto que el usuario está capturando a través del teclado, obtener la ubicación del ratón dentro de la pantalla, u obtener los valores capturados en algunos controles especiales como los componentes JSlider, JTree o JSpinner, entre otros.

Como ya había mencionado en el primer post de esta serie, la implementación de la gestión de eventos recae en el componente de la aplicación conocido como Controlador. Dentro del ejemplo que exploraremos a continuación, este aparece representado por una única clase que se encarga de atender diferentes eventos ocurridos en la ventana principal de nuestra aplicación, a través de la implementación de 2 interfaces nativas del lenguaje Java: ActionListener y MouseMotionListener. Las acciones que recibirán nuestra atención son que el usuario haga un un clic con el ratón sobre el botón de búsqueda titulado "Buscar", y el movimiento del ratón sobre la pantalla, de modo que al seleccionar este botón se deberá desplegar una ventana emergente que contendrá una serie de datos, y al mover el ratón deberán mostrarse en una de las etiquetas de la pantalla principal las coordenadas de la ubicación del ratón.

Notas: Además de las interfaces, Java cuenta con un grupo de clases nativas que también podemos usar para incluir la gestión de eventos en nuestro programas, llamadas clases adaptadoras. Sin embargo, para efectos prácticos, nos enfocaremos en las interfaces, ya que Java nos permiten implementar múltiples interfaces en una misma clase, con lo que podemos tener una sola clase controladora de todos los eventos.

Retomaremos el proyecto del post anterior, agregando nuevas clases y modificando parte del código de la clase Ventana para echar a andar la gestión de eventos.

El Controlador

Para incluir este componente dentro de nuestro programa crearemos el archivo de la clase que lo implementará, y después agregaremos una instancia a ésta en el código de la ventana principal. El código para el Controlador es el que aparece a continuación:

Controlador.java
import javax.swing.JFrame;
import java.awt.event.*;
/**
* @Desc Clase que maneja los eventos realizados en la Vista implementando una serie de interfaces nativas del lenguaje
* @author Beto González
*
*/
public class Controlador implements ActionListener, MouseMotionListener {
    public Ventana ventana;
    Controlador(Ventana ventana) {
        this.ventana = ventana;
    }
    /**
    * @Desc método perteneciente a la interface ActionListener que es implementado para controlar las acciones sobre los botones
    * ya que se ejecutará si se hace clic sobre el botón que este atendiendo
    */
    public void actionPerformed(ActionEvent ie)
    {
        if(ie.getActionCommand() == "Buscar usuario") { //Validación para asegurarse que la acción fue realizada sobre el botón Buscar
            ventana.panelPrincipal.lblMensaje.setText("Has hecho clic en el bóton de búsqueda");
            //Desplegamos una ventana emergente que contendrá la información de un panel de la clas PanelDeDatos
            PanelDeDatos panel = new PanelDeDatos();
            panel.estableceContenido();
            VentanaEmergente emergente = new VentanaEmergente("Información del usuario", panel);
            emergente.setBounds(180, 180, 300, 250);
            emergente.setVisible(true);
            emergente.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            emergente.addWindowListener(new WindowAdapter()
            {
                public void WindowCloser(WindowEvent e)
                {
                    System.exit(0);
                }
            });
        }
    }
    /**
    * @Desc Método ejecutado cuando el usuario arrastra el ratón presionando el botón de acción
    */
    public void mouseDragged(MouseEvent e) {
        ventana.panelPrincipal.lblMensaje.setText("Ratón arrastrado");
    }
    /**
    * @Desc Método ejecutado cuando el usuario solo arrastra el ratón
    */
    public void mouseMoved(MouseEvent e) {
        ventana.panelPrincipal.lblMensaje.setText("Posición del puntero X:" + Integer.toString(e.getX()) + " Y:" + Integer.toString(e.getY()));
    }
}


Como vemos, es el código de una clase que implementa las interfaces ActionListener y MouseMotionListener, gracias a las cuales hereda una serie de métodos que le permitirán controlar los eventos sobre algunos de los componentes de la pantalla, en este caso particular presionar el botón "Buscar" y el movimiento del ratón sobre la pantalla. El método actionPerformed() que viene de la interface ActionListener se ejecuta cada vez que se hace clic sobre un botón que haya sido habilitado para recibir eventos y este reciendolos a través de la clase Controlador. De una manera similar, los métodos mouseDragged() y mouseMoved() son heredados de la interface MouseMotionListener, y se ejecutarán con las acciones del ratón correspondientes, siempre y cuando la ventana donde deseamos que actue este configurada para recibir los eventos del movimiento del ratón con esta misma clase.

El método mouseMoved obtiene las coordenadas del ratón a través del parámetro de la clase MouseEvent que recibe, para después desplegarlos incluyendolos en el valor de una de las etiquetas de la pantalla principal. Tanto la ejecución de este método como la creación del objeto que recibe son responsabilidad de Java, por lo que no tenemos que preocuparnos por estos detalles. Algo similar ocurre con el método actionPerformed, ya que las especificaciones del evento ocurrido pueden ser obtenidas a traves de parámetro de la clase ActionEvent recibido, este objeto puede ser utilizada para obtener una referencia al componente desde el que se originó el evento, y de esa forma podemos validar que acción deberemos realizar en consecuencia. En este caso se valida que la propiedad actionCommand del objeto origen sea igual a "Buscar usuario". Después de que se realiza la validación aparece el código que permitirá desplegar una ventana emergente con datos incluidos, para esto se utilizarán 2 clases más definidas por nosotros mismos, cuyo código aparece a continuación.

VentanaEmergente.java
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JPanel;
/**
* @Desc Clase utilizada para desplegar ventanas emergentes dentro del sistema que contendrán la información establecida
* en un objeto JPanel pasado por el constructor
* @author Beto González
*/
public class VentanaEmergente extends JFrame {
    //Constructor de la clase
    VentanaEmergente(String titulo, JPanel panelDeDatos) {
        super(titulo);
        Container contentPane = getContentPane();
        contentPane.add(panelDeDatos);
    }
}


PanelDeDatos.java
import javax.swing.*;
import java.awt.*;
/**
* @Desc Clase utilizada para desplegar ventanas emergentes dentro del sistema que contendrán la información establecida
* en un objeto JPanel pasado por el constructor
* @author Beto González
*
*/
public class PanelDeDatos extends JPanel {
    private JLabel lblNombre;
    private JLabel lblCorreoElectronico;
    private JLabel lblDomicilio;
    private JLabel lblEdad;
    private JLabel lblTelefono;
    //Constructor de la clase
    PanelDeDatos () {
        this.lblNombre = new JLabel("Nombre del usuario: ");
        this.lblCorreoElectronico = new JLabel("Correo electronico: ");
        this.lblDomicilio = new JLabel("Domicilio: ");
        this.lblEdad = new JLabel("Edad: ");
        this.lblTelefono = new JLabel("Telefono: ");
        this.setLayout(new GridBagLayout());
    }
    /**
    * @Desc Método que agrega al panel principal cada una de las etiquetas declaradas
    */
    public void estableceContenido() {
        GridBagConstraints delimitador = new GridBagConstraints(); //Creamos un delimitador que usaremos para especificar la ubicación de los componentes dentro del panel
        delimitador.gridx = 0;
        delimitador.gridy = 0;
        delimitador.anchor = GridBagConstraints.EAST;
        this.add(lblNombre, delimitador);
        delimitador.gridy = 1;
        this.add(lblCorreoElectronico, delimitador);
        delimitador.gridy = 2;
        this.add(lblDomicilio, delimitador);
        delimitador.gridy = 3;
        this.add(lblEdad, delimitador);
        delimitador.gridy = 4;
        this.add(lblTelefono, delimitador);
    }
}


La clase VentanaEmergente -que extiende a JFrame- se utilizará para poder crear y lanzar ventanas personalizadas en tiempo de ejecución, mientras que la clase PanelDeDatos -que exiende a JPanel- se utilizará para incluir información en forma de etiquetas dentro estas ventanas.

Por ahora la información que contendrán los objetos de PanelDeDatos será constante, ya que no poseemos una fuente de datos externa con la cual alimentarla. Ese será precisamente el trabajo que realizaremos en el siguiente post, el acceder a una base de datos para extraer la información que se esté consultando, al buscar algunos de los datos caputados por el usuario en la pantalla principal. La razón por la que ese detalle queda fuera de ese post es porque la responsabilidad del acceso a datos recae sobre el Modelo de la aplicación, no sobre el Controlador.

Interacción entre el Controlador y la Vista


Una vez creados los archivos Cotrolador.java, VentanaEmergente.java y PanelDeDatos.java, solo queda modificar el código de la clase Ventana.java para poner a trabajar el manejo de eventos, agregando una referencia a un objeto de la clase Controlador en los objetos correspondientes de la Vista:

Ventana.java
import java.awt.Container;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import javax.swing.*;
/**
* @Desc Clase que utilizamos para crear una ventana que contendrá el panel principal de la aplicación
* @author Beto González
*/
public class Ventana extends JFrame {
    PanelSwing panelPrincipal;
    /**
    * @param args
    */
    public static void main(String[] args) {
        Ventana ventana = new Ventana();
        ventana.setBounds(120, 120, 800, 600);
        ventana.setVisible(true);
        ventana.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        ventana.addWindowListener(new WindowAdapter()
        {
            public void WindowCloser(WindowEvent e)
            {
                System.exit(0);
            }
        });
    }
    //Constructor de la clase
    Ventana(){
        super("Primer paso en la interfaces gráficas de Java"); //Llamada al constructor de la clase padre
        Container contentPane = getContentPane();
        panelPrincipal = new PanelSwing();
        panelPrincipal.estableceContenedores();
        contentPane.add(panelPrincipal);
        try{
            UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
        }catch(Exception ex){}
        SwingUtilities.updateComponentTreeUI(getContentPane());
        panelPrincipal.btnAccion1.setActionCommand("Buscar usuario"); //Establecemos un Action Command para el boton btnAccion1, con el objetivo de poder identificar sus eventos en el Controlador
        Controlador controlador = new Controlador(this); //Instanciamos a la clase Controlador
        panelPrincipal.btnAccion1.addActionListener(controlador); //Indicamos que el objeto controlador manejará los eventos sobre btnAccion1
        this.addMouseMotionListener(controlador); //Indicamos que el objeto controlador manejará los eventos de movimiento del ratón sobre la ventana

    }
}


Vemos que dentro del código establecemos el valor de la propiedad actionCommand del objeto btnAccion1 como "Buscar usuario", ya que es el valor que se valida del lado del Controlador para comprobar que la acción se realizó sobre este botón. Justo después de esa línea se instancia la clase Controlador y aparece finalmente la referencia del componente con el Controlador de eventos, al escribir la sentencia:

panelPrincipal.btnAccion1.addActionListener(controlador);


Así, establecemos que los todos los eventos recibidos por el botón btnAccion1 serán gestionados por el objeto controlador. En este punto quizás podrían pensar que la validación de la propiedad actionCommand en el método actionPerformed está de más, ya que solo contamos con un botón en la aplicación, pero recuerden que de esta forma, en caso de contar con multiples botones de acción en la aplicación, sería mucho más sencillo manejar los eventos de todos, al recibirlos con un solo objeto en un mismo método.

Para que los eventos del movimiento del ratón sean atendidos por el Controlador, haremos una referencia desde objeto ventana:

this.addMouseMotionListener(controlador);


De modo que todos estos eventos que ocurran en la venta principal serán manejados por este mismo objeto.

Y esto es todo lo que tenemos que hacer para que el manejo de evento funcione, al echar a andar el programa debemos obtener un resultado como el que se observa a continuación:



Y como podemos apreciar, en la etiqueta lblMensaje se despliega el valor de la posición del ratón en la pantalla, y también nos informa cuando lo estamos arrastrando.

Si probamos hacer clic sobre el botón "Buscar" debe aparecer la ventana emergente que mencionamos, tal y como pueden ver en la imagen:



Con la implementación de este ejemplo es que concluímos el post de hoy, el objetivo principal es que aprecieran que agregar un manejo de eventos a nuestros programas en Java es relativamente sencillo, tan solo debemos conocer que clase o interface nativa le corresponde al tipo de componente que deseamos controlar, agregar las definiciones de los métodos necesarios dentro de una clase que que herede o implemente las clases o interfaces, y realizar la vinculación entre el componente y el manejador de eventos. El otro objetivo -que me falto mencionar al principio- era diseñar un esqueleto funcional al que agregaremos el acceso a un manejador de base de datos y una pequeña lógica de negocios, los cuales integrarán el Modelo de nuestra aplicación, completando de esta forma el MVC de que habiamos hablado en un inicio, pero que quedará pendiente para el siguiente artículo.

Hasta la próxima.