lunes, 19 de diciembre de 2011

RenderingHints, renderizados y operaciones de manipulación de imágenes

0 comentarios
Hola otra vez, hoy vamos a profundizar un poco en las claves de la manipulación de imágenes y textos con Graphics2D y de como mejorar la resolución de los objetos u imágenes mostrados.
Para ellos nos serviremos de la clase RenderingHints y le estableceremos los valores apropiados para los resultados que pretendemos obtener.

La clase RenderingHints define y maneja una colección de claves y valores asociados que permite a una aplicación proporcionar entradas a las opciones de algoritmos usados por otras clases que realizan renderizados y manipulación de imágenes. La clase Graphics2D, y clases que implementan BufferedImageOp y RasterOp proporcionan métodos para obtener y establecer individualmente o grupos de claves RenderingHints y sus valores asociados. Cuando estas implementaciones realizan cualquier renderizado u operaciones de manipulación de imágenes deberían examinar estos valores que son requeridos por el llamante y adaptar los algoritmos usados en consecuencia y en la medida de su capacidad.

A partir de aquí paso a explicar cada uno de los indicadores y como modifican el renderizado de los componentes.

KEY_ALPHA_INTERPOLATION
La clave ALPHA_INTERPOLATION es una clave general que proporciona una recomendación de alto nivel para que haciendo uso de un algoritmo de mezcla de alpha elegir entre mayor velocidad o calidad al evaluar ventajas y desventajas en el renderizado.
Esta clave controlaría la elección del factor alpha en los cálculos que sacrificarían algo de precisión para usar tablas de consulta rápida o reducir la precisión de la instrucciones SIMD (Single Instruction, Multiple Data). Esta clave también controlaría si un color y su valor alpha se convierten a un color linear durante los cálculos para un efecto visual mas lineal con el coste de hacer cálculos por pixel adicionales.
Los posibles valores son:

  • VALUE_ALPHA_INTERPOLATION_SPEED
  • VALUE_ALPHA_INTERPOLATION_QUALITY
  • VALUE_ALPHA_INTERPOLATION_DEFAULT


KEY_ANTIALIASING
Clave de suavizado. La clave ANTIALIASING controla si la representación geométrica en los métodos de un objeto Graphics2D intentará reducir el aliasing en los bordes de las formas.
Un algoritmo de suavizado típico funciona fundiendo los colores existentes en la frontera de una forma con el color de relleno de acuerdo a la cobertura de pixeles parcial estimada de la forma.
Los posibles valores son:

  • VALUE_ANTIALIAS_ON
  • VALUE_ANTIALIAS_OFF
  • VALUE_ANTIALIAS_DEFAULT


KEY_COLOR_RENDERING
La clave COLOR_RENDERING controla la precisión de aproximaciones y conversiones al almacenamos colores en una imagen o superficie destino.
Cuando al renderizar o realizar una operación de manipulación de imágenes se produce un valor de color que debe ser guardado en un destino, primero de debe convertir ese color en un tipo capaz de ser almacenado en la imagen o superficie destino. Como mínimo, las componentes del color deben ser convertidas a una representación de bits y ordenarse correctamente o un índice en una tabla de decisión de colores debe elegirse antes de que los datos sean guardados en la memoria destino. Si esta conversión mínima, los datos en el destino podrían perfectamente parecer aleatorios, incorrectos o incluso valores no soportados. Los algoritmos que se usan para convertir los resultados del renderizado rápidamente en el formato de color adecuado son muy conocidos y están bastante optimizados.
Simplemente realizando las conversiones de formato de color más básicas para guardarlos en un destino puede ignorar una diferencia en el calibrado del ColorSpace del origen y destino u otros factores tales como la linealidad de la corrección gamma. A menos que el ColorSpace origen y destino sean idénticos, para realizar correctamente una operación de renderizado con teniendo el máximo cuidado en la precisión de los colores representados, los colores origen deben transformarse a un ColorSpace independiente del dispositivo donde se valla a mostrar y posteriormente convertirlos al ColorSpace destino. Aún más, si cálculos tales como la mezcla de múltiples colores origen se van a realizar durante la operación de renderizado, se puede obtener más claridad si el ColorSpace intermedio elegido tiene una relación lineal entre los valores que se están calculando y la percepción del ojo humano a las curvas de respuesta del dispositivo de salida.
Los posibles valores son:

  • VALUE_COLOR_RENDER_SPEED
  • VALUE_COLOR_RENDER_QUALITY
  • VALUE_COLOR_RENDER_DEFAULT


KEY_DITHERING
Clave de Dithering. La clave DITHERING controla como de cerca aproximar un color cuando lo almacenamos en un destino con una resolución de color limitada.
Algunos destinos de renderizado pueden soportar un número limitado de colores lo cual puede que no permita representar el espectro completo de colores que podrían resultar durante las operaciones de renderizado. Para estos destinos, la clave DITHERING controla si el renderizado se realiza con un color de relleno sólido en un pixel específico que es de los colores permitido el mas cercano al que se solicita, o si las formas se rellenaran con un patrón de colores que se combinan para aproximarse lo máximo posible a este color deseado.
Los posibles valores son:

  • VALUE_DITHER_DISABLE
  • VALUE_DITHER_ENABLE
  • VALUE_DITHER_DEFAULT


KEY_FRACTIONALMETRICS
Clave de métricas fraccionadas en las fuentes. La clave FRACTIONALMETRICS controla si al posicionar los glifos de un carácter en particular se tiene en cuenta la precisión sub-pixel de los avances de carácter escalado de la fuente o si estos vectores de avance se redondean a un entero. Esta clave solo recomienda cuanta precisión debe usarse para posicionar los glifos y no especifica ni recomienda si la rasterización o las fronteras de pixel de un glifo deben modificarse para coincidir.
Renderizar texto a un dispositivo de baja resolución como una pantalla necesariamente implicará una serie de operaciones de redondeo ya que la definición de alta calidad y muy precisa de la forma y métricas de los glifos de un carácter deben ajustarse a un número discreto de pixeles en el dispositivo. Idealmente el posicionado de los glifos durante la disposición del texto debería calcularse escalando las métricas diseñadas en la fuente de acuerdo al tamaño del punto, pero entonces el ancho del avance escalado no será necesariamente un número entero de pixeles. Si los glifos se posicionan con una precisión sub-pixel de acuerdo a estas métricas de diseño escaladas, entonces la rasterización idealmente necesitará ser ajustada para cada posible origen sub-pixel.
Desafortunadamente, escalar cada glifo de manera personalizada a su origen sub-pixel exacto durante la disposición del texto sería prohibitivamente caro por lo que un sistema simplificado basado en las posiciones enteras de un dispositivo se usa para disponer el texto. La rasterización de un glifo y el ancho de avance escalado son ambos ajustados a la vez para conseguir que el texto se vea bien en la resolución del dispositivo y tenga distancias enteras de pixeles entre los glifos que ayude a que los glifos parezcan uniformes y consistentemente espaciados y legibles.
Este proceso de redondeo del ancho del avance para los glifos rasterizados significa que la densidad de un carácter y la longitud promedia de una cadena de texto será diferente entre las medidas teóricas del diseño debido a la acumulación de una serie de pequeñas diferencias en los anchos ajustados de cada glifo. Cada diferencia específica será diferente en cada glifo, algunos siendo mas anchos y otros más estrechos que sus teóricos anchos de diseño. Así la diferencia global en la densidad de carácter y la longitud variará por un número de factores incluyendo la fuente, la resolución específica del dispositivo destino, y los glifos elegidos para representar la cadena que se está renderizando. Como resultado, renderizar la misma cadena en múltiples resoluciones puede arrojar métricas muy varadas sobre cadenas enteras.
Cuando FRACTIONALMETRICS están habilitadas, las métricas de diseño originales se escalan a tamaño de punto y se usan para disponer con precisión sub-pixel. La densidad media de los glifos y la longitud total de una cadena larga de caracteres será por ello más aproximada al diseño teórico de la fuente, pero la legibilidad puede verse afectada ya que cada par individual de caracteres puede que no tengan distancias consistentes además dependiendo de como la acumulación sub-pixel de los orígenes de los glifos se engrane con la malla de pixeles del dispositivo. Habilitar esta indicación puede ser deseado cuando la disposición del texto que se realiza deba ser consistente a lo largo de una variedad de resoluciones de salida. Específicamente, esta indicación puede ser deseable en situaciones cuando la disposición del texto se previsualiza en dispositivos con baja resolución como una pantalla para salidas que serán eventualmente renderizadas en una impresora de alta resolución o dispositivo de composición.
Cuando está deshabilitado, las métricas escaladas se redondean o ajustan a distancias enteras para disponerse. Las distancias entre cada par específico de glifos será mas uniforme en el dispositivo, pero la densidad y longitud total de cadenas largas puede que no coincida ya con las intenciones teóricas del diseñador de la fuente. Deshabilitar este indicador producirá normalmente resultados más legibles en dispositivos de baja resolución como monitores de ordenador.
Los posibles valores son:

  • VALUE_FRACTIONALMETRICS_OFF
  • VALUE_FRACTIONALMETRICS_ON
  • VALUE_FRACTIONALMETRICS_DEFAULT


KEY_INTERPOLATION
Clave de Interpolación. El indicador INTERPOLATION controla como los pixeles de las imágenes se filtran o remuestrean durante una operación de renderizado de imágenes.
Implícitamente las imágenes se definen para proporcionar muestras de color en unas coordenadas enteras. Cuando las imágenes se renderizan tal cual sin escalado al destino, la elección de que pixeles de la imagen se mapean a que pixeles del dispositivo es obvio y las muestras en las coordenadas de la imagen se transfieren a los pixeles correspondientes en la rejilla de pixeles del destino uno a uno. Cuando las imágenes se renderizan en un sistema de coordenadas escalado, rotado o transformado de cualquier manera, entonces el mapeado de los pixeles del dispositivo con la imagen puede originar la pregunta de que muestra de color usar para las continuas coordenadas que existen entre las localizaciones enteras y las proporcionadas en las muestras de la imagen. Los algoritmos de interpolación definen funciones que proporcionan una muestra de color para una coordenada continua en una imagen basada en las muestras de color de las coordenadas colindantes.
Los posibles valores son:

  • VALUE_INTERPOLATION_NEAREST_NEIGHBOR → la muestra de color de la coordenada vecinas mas cercana en la imagen es la usada.
  • VALUE_INTERPOLATION_BILINEAR → la muestra de color se obtiene mediante interpolación lineal de las 4 coordenadas más cercanas.
  • VALUE_INTERPOLATION_BICUBIC → las muestras de color de 9 coordenadas cercanas en la imagen se interpolan usando una función cúbica en X e Y para producir la muestra de color.
KEY_RENDERING

Clave de renderizado. El indicador RENDERING es un indicador general que proporciona recomendaciones de alto nivel sobre como sesgar las opciones de un algoritmo hacia velocidad o calidad cuando evaluá pros y contras. El indicador puede ser consultado por cualquier renderizado u operación de manipulación de imágenes, pero las decisiones normalmente harán honor a otros indicadores mas específicos en relación a este indicador.
Los posibles valores son:

  • VALUE_RENDER_SPEED
  • VALUE_RENDER_QUALITY
  • VALUE_RENDER_DEFAULT


KEY_STROKE_CONTROL
Clave para normalización de trazo. El indicador STROKE_CONTROL controla si una implementación de renderizado debería o tiene permitido modificar la geometría de las formas renderizadas para varios fines.
Algunas implementaciones podrían ser capaces de usar una librería optimizada de la plataforma que podría ser mas rápida que los algoritmos de renderizado software tradicionales en una plataforma dada, pero que podría no soportar coordenadas de punto flotante. Algunas implementaciones podrían también tener sofisticados algoritmos que perturban las coordenadas de un trazado de tal manera que las lineas anchas aparezcan mas uniformes en anchura y espaciado.
Si una implementación realiza cualquier tipo de modificación o "normalización" de un trazado, debería mover las coordenadas mas de medio pixel en cada dirección.
Los posibles valores son:

  • VALUE_STROKE_NORMALIZE
  • VALUE_STROKE_PURE
  • VALUE_STROKE_DEFAULT


KEY_TEXT_ANTIALIASING
Clave de antialiasing de texto. El indicador TEXT_ANTIALIASING puede controlar el uso de algoritmos antialiasing para el texto independientemente de la elección para renderizado de formas. Muchas veces una aplicación puede querer usar antialiasing solo para texto y no para otras formas, Además, los algoritmos para reducir el solapamiento para texto suelen ser mas sofisticados que aquellos que se han desarrollado para renderizado general por lo que este indicador proporciona valores adicionales que pueden controlar las opciones de estos algoritmos específicos para texto. Si se deja en el valor DEFAULT, este indicador diferirá al valor del indicador general KEY_ANTIALIASING.
Los posibles valores son:

  • VALUE_TEXT_ANTIALIAS_ON → Antialiasing habilitado
  • VALUE_TEXT_ANTIALIAS_OFF → Antialiasing deshabilitado
  • VALUE_TEXT_ANTIALIAS_DEFAULT → El renderizado se realiza de acuerdo al valor del indicador KEY_ANTIALIASING
  • VALUE_TEXT_ANTIALIAS_GASP → El renderizado requiere usar la información en el recurso de la fuente que especifica para cada tamaño de punto si usar o no los valores VALUE_TEXT_ANTIALIAS_ON o VALUE_TEXT_ANTIALIAS_OFF.
  • VALUE_TEXT_ANTIALIAS_LCD_HRGB → El texto mostrado será optimizado para un display LCD con subpixeles para mostrar de izquierda a derecha RGB, de tal manera que la resolución horizontal subpixel es tres veces la de la resolución de un pixel horizontal completo (HRGB).
  • VALUE_TEXT_ANTIALIAS_LCD_HBGR → El texto mostrado será optimizado para un display LCD con subpixeles para mostrar de izquierda a derecha BGR, de tal manera que la resolución vertical subpixel es tres veces la de la resolución de un pixel vertical completo (HBGR).
  • VALUE_TEXT_ANTIALIAS_LCD_VRGB → El texto mostrado será optimizado para un display LCD con subpixeles para mostrar de arriba a abajo RGB, de tal manera que la resolución horizontal subpixel es tres veces la de la resolución de un pixel horizontal completo (VRGB).
  • VALUE_TEXT_ANTIALIAS_LCD_VBGR → El texto mostrado será optimizado para un display LCD con subpixeles para mostrar de arriba a abajo BGR, de tal manera que la resolución vertical subpixel es tres veces la de la resolución de un pixel vertical completo (VBGR).


KEY_TEXT_LCD_CONTRAST
Clave de contraste de texto LCD. El valor es un valor Integer que se usa como un ajuste en el contraste del texto cuando se usa en conjunción con un indicador de antialiasing de texto LCD tal como VALUE_TEXT_ANTIALIAS_LCD_HRGB.
Los valores deben ser enteros positivos en el rango 100 a 250.

  • Un valor bajo (100) corresponde a texto con mayor contraste cuando se muestra texto oscuro en un fondo claro.
  • Un valor alto (200) corresponde a texto de bajo contraste cuando se muestra texto oscuro en un fondo claro.
  • Un valor típico esta en el estrecho rango 140-180.
  • Si no se especifica ningún valor, una implementación por defecto o del sistema será aplicada.
El valor por defecto debería ser apropiado para la mayoría de los fines, por lo que los clientes raramente necesitan especificar un valor para este indicador a menos que tengan información concreta sobre un valor apropiado. Un valor más alto no significa mayor contraste, en realidad es al contrario. La corrección se aplica de una manera similar a un ajuste gamma para una respuesta de luminosidad de percepción no lineal de los sistemas de visualización, pero no indica una corrección total para esto.

Y como siempre os dejo algunos ejemplos simples de la aplicación de estos indicadores.

Y por aquí el código del ejemplo:

package RenderingHintsSamples;

import java.awt.BasicStroke;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.RenderingHints;

import javax.swing.JLabel;
import javax.swing.JPanel;

public class RenderingHintsSamples extends JPanel {

 public RenderingHintsSamples() {
  this.setLayout(new GridLayout(4, 2));

  this.add(new JLabel("KEY_ANTIALIASING --> OFF"));
  this.add(new AntialiasingPanel(false));
  this.add(new JLabel("KEY_ANTIALIASING --> ON"));
  this.add(new AntialiasingPanel(true));

  this.add(new JLabel("KEY_TEXT_ANTIALIASING --> OFF"));
  this.add(new TextAntialiasingPanel(false));
  this.add(new JLabel("KEY_TEXT_ANTIALIASING --> ON"));
  this.add(new TextAntialiasingPanel(true));
 }

 private class AntialiasingPanel extends JPanel {

  boolean claveActiva = false;

  public AntialiasingPanel(boolean claveActiva) {
   this.claveActiva = claveActiva;
  }

  @Override
  public void paint(Graphics g) {

   Graphics2D g2d = (Graphics2D) g;
   if (claveActiva) {
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
   } else {
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
   }

   g2d.setStroke(new BasicStroke(5f));
   g2d.drawOval(20, 20, 60, 60);
   g2d.drawLine(80, 20, 150, 75);

  }
 }

 private class TextAntialiasingPanel extends JPanel {

  boolean claveActiva = false;

  public TextAntialiasingPanel(boolean claveActiva) {
   this.claveActiva = claveActiva;
  }

  @Override
  public void paint(Graphics g) {

   Graphics2D g2d = (Graphics2D) g;
   if (claveActiva) {
    g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
   } else {
    g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
   }

   g2d.setFont(new Font("Serif", Font.ITALIC, 30));
   g2d.drawString("TEXT_ANTIALIASING", 0, 40);

  }
 }

}

package RenderingHintsSamples;

import javax.swing.JFrame;


public class Main {

 private static void createAndShowGUI() {
  JFrame frame = new JFrame("RenderingHints: ejemplos");

  RenderingHintsSamples ejemplos = new RenderingHintsSamples();

     frame.getContentPane().add(ejemplos);
     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
     frame.setSize(565, 370);
     frame.setVisible(true);
 }

 public static void main(String[] args) {

  javax.swing.SwingUtilities.invokeLater(new Runnable() {
   public void run() {
    createAndShowGUI();
   }
  });
 }
}


Un saludo y hasta la próxima entrada.

martes, 22 de noviembre de 2011

Diálogos personalizados, bordes redondeados, quitar botones de los diálogos.

2 comentarios
Hola a todos.

Hace un tiempo tuve que pelearme mucho para encontrar la solución a 2 problemas bastante comunes. Los diseñadores que se encargan de la interfaz gráfica de la aplicación que estábamos desarrollando insistían en eliminar los botones clásicos que aparecen en el marco de los diálogos, si, si, los de minimizar, maximizar y cerrar, y querían utilizar unos personalizados.
Por si esto fuera poco, además pedían que los diálogos no tuviesen la forma cuadrada normal, si no que tuviesen bordes redondeados o formas específicas.

Pues bien, después de mucho indagar dí con la solución a ambos problemas de un plumazo. Paso a detallaros como hacerlo.

Un JDialog no es más que un Frame más de nuestra aplicación, como tal tiene los botones mencionados anteriormente, se le puede añadir un JMenu y demás parafernalia que muchas veces nos sobra en nuestros diseños.
Para conseguir quitar el marco exterior de los diálogos y mostrar solo el contenido, es decir, quitarle la decoración, la única manera de conseguirlo es llamar al método setUndecorated(true). Esto elimina estos botones y nos deja solo el contenido a la vista.

Esta condición anterior es necesaria para conseguir nuestro segundo objetivo, darles una forma personalizada a nuestras ventanas. Para ello nos valdremos de la clase de utilidades AWTUtilities, esta clase además de permitir cambiar la forma de las ventanas permite aplicar ciertas clases de transparencia a las mismas aunque los resultados no son siempre los esperados dependiendo de la plataforma de desarrollo (aunque esto es un tema que no vamos a tratar de momento). Esta clase se distribuye a partir de la JRE|JDK 6u10, por lo que los usuarios de versiones anteriores no podréis hacer uso de ella :(

Este sería el resultado que podríamos obtener haciendo uso de lo que os he comentado:


Bien, así pues aquí os dejo el código para conseguir estos resultados.

Primero la clase de nuestro diálogo personalizado, aquí simplemente heredamos de un JDialog, le quitaremos el marco y la botonera y en su método paint(Graphics g) haremos que se dibuje con la forma deseada.
package dialogos;

import java.awt.Graphics;
import java.awt.Shape;

import javax.swing.JDialog;

import com.sun.awt.AWTUtilities;

public class ShapedDialog extends JDialog {

 // La forma de nuestro dialogo
 Shape dialogShape;

 public ShapedDialog() {
  super();
  /* Con esta instruccion eliminamos la barra superior y los botones de
   * minimizar, maximizar y cerrar.
   */
  this.setUndecorated(true);
  // Esta orden centra el dialogo en la pantalla
  this.setLocationRelativeTo(null);
 }

 /**
  * Establece la forma de la ventana.
  * @param shape
  */
 public void setShape(Shape shape) {
  this.dialogShape = shape;
                /* Imprescindible para mostrar el diálogo, hasta que no hagamos esto,
   * la ventana permanece invisible
   */
  this.setVisible(true);
 }

 @Override
 public void paint(Graphics g) {

  // Pintamos todo el contenido
  super.paint(g);

  // Establecemos la forma de la ventana
  AWTUtilities.setWindowShape(this, dialogShape);

 }

}


Como veis ha sido bastante simple, a pesar de ello, a más de uno os habrá dolido la cabeza antes de llegar a este post, jeje.

Y ahora la clase desde donde lanzaremos nuestra aplicación y crearemos los diálogos.

package dialogos;

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.RoundRectangle2D;

import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;


public class Main {

 private static void createAndShowGUI() {
  JFrame frame = new JFrame("Ejemplos de dialogos");

  final ShapedDialog dialog = new ShapedDialog();
  // Este sera el contenido de nuestro dialogo
  JPanel dialogContent = new JPanel(new BorderLayout());
  dialogContent.add(new JLabel("Interior del dialogo", JLabel.CENTER), BorderLayout.CENTER);

  // Dado que eliminamos el boton de cerrar el dialogo, tendremos que añadirle un boton para ello
  JButton close = new JButton(new AbstractAction("Cerrar") {

   @Override
   public void actionPerformed(ActionEvent e) {
    dialog.dispose();
   }
  });
  dialogContent.add(close, BorderLayout.SOUTH);


  dialog.setContentPane(dialogContent);
  dialog.setSize(400, 200);

  JButton openDialog = new JButton();
  openDialog.setAction(new AbstractAction("Dialogo redondo") {

   @Override
   public void actionPerformed(ActionEvent e) {
    Shape oval = new Ellipse2D.Float(0, 0, 400, 200);
    dialog.setShape(oval);
   }
  });
  JButton openDialog2 = new JButton();
  openDialog2.setAction(new AbstractAction("Dialogo bordes redondeado") {

   @Override
   public void actionPerformed(ActionEvent e) {
    Shape roundRectangle = new RoundRectangle2D.Float(0, 0, 400, 200, 20, 20);
    dialog.setShape(roundRectangle);
   }
  });

  frame.getContentPane().setLayout(new FlowLayout());
     frame.getContentPane().add(openDialog);
     frame.getContentPane().add(openDialog2);
     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
     frame.setSize(200, 150);
     frame.setVisible(true);
 }

 public static void main(String[] args) {

  javax.swing.SwingUtilities.invokeLater(new Runnable() {
   public void run() {
    createAndShowGUI();
   }
  });
 }
}

Bien, pues esto es todo por hoy, espero que os estén siendo de ayuda estos mini-tutoriales.

Un saludo.

PD: ahora también podéis seguirme en Facebook y hacerme saber si os gusta lo que hago.

lunes, 14 de noviembre de 2011

Interfaz Stroke y lápices personalizados

0 comentarios
Hola de nuevo amigos desarrolladores, hoy os traigo cosas muy interesantes rescatadas de mi baúl de los recuerdos.
Seguramente muchos os encontréis ante la necesidad de pintar texto siguiendo un trazado, queriendo pintar letras con un contorno hecho con figuras de formas o simplemente queriendo dibujar usando un trazo personalizado.

Pues bien todo esto está permitido en Java gracias al uso de la interfaz Stroke y algunos truquillos que os paso a detallar.

La interfaz Stroke permite que un objeto Graphic2D obtenga una forma que es el contorno, o representación estética del contorno, de una Shape (Forma) especificada.
Dibujar una Shape es como trazar su contorno con un lápiz con la forma y tamaño apropiado.
Así pues todo nuestro trabajo consistirá en decirle a la interfaz Stroke que Shape tiene que devolvernos para dibujar cualquier objeto gráfico con Graphic2D. Esto se consigue al sobreescribir el método
public Shape createStrokedShape(Shape shape) para hacer que nos devuelva la forma que deseamos utilizar como lápiz.

Las posibilidades que ofrece esta interfaz son muchas, este sería el resultado de aplicar algunos de estos lápices personalizados a la hora de dibujar una línea, un círculo y los caracteres "A B C".

Por ejemplo, vamos a crear un contorno formado por figuras geométricas, en nuestro ejemplo serán círculos y cuadrados que se repetirán a lo largo del trazado. En los comentarios del código intento explicar el funcionamiento del mismo.

package strokes.samples;

import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.FlatteningPathIterator;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;

public class StrokeGeometrico implements Stroke {

 // Formas a incluir en el stroke
 private Shape shapes[];
 private float advance;

 // Valores necesarios para dibujar el Path (trazado)
 private AffineTransform t = new AffineTransform();

 /**
  * Crea un trazo a partir de un array de formas.
  * @param shapes Formas a dibujar
  * @param advance separación entre las formas
  */
 public StrokeGeometrico(Shape shapes[], float advance) {
  this.advance = advance;
  this.shapes = new Shape[shapes.length];

  for ( int i = 0; i < this.shapes.length; i++ ) {
   Rectangle2D bounds = shapes[i].getBounds2D();
   t.setToTranslation(-bounds.getCenterX(), -bounds.getCenterY());
   this.shapes[i] = t.createTransformedShape(shapes[i]);
  }
 }

 @Override
 public Shape createStrokedShape( Shape shape ) {
  /* Proporciona implementaciones del trazado geométrico en general que soporta
   * la funcionalidad de las clases Shape y PathIterator
   */
  GeneralPath result = new GeneralPath();
  // Usaremos este iterator para 'aplanar' el trazado
  PathIterator it = new FlatteningPathIterator(shape.getPathIterator( null ), 1);
  float points[] = new float[6];
  float moveX = 0, moveY = 0;
  float lastX = 0, lastY = 0;
  float thisX = 0, thisY = 0;
  int type = 0;
  float next = 0;
  int currentShape = 0;
  int length = shapes.length;

  /* Todas estas operaciones son necesarias para renderizar la forma del trazado.
   * La interfaz PathIterator proporciona el mecanismo para que objetos que implementan
   * la interfaz Shape devuelvan la geometria de sus formas, permitiendo que quien los llama
   * recupere el trazado de esa forma segmento por segmeto.
   */
  while (currentShape < length && !it.isDone()) {
   type = it.currentSegment(points);
   switch(type){
   /* Una constante que indica que en un segmento indica que un
    * punto debe comenzar un nuevo trazado.
    */
   case PathIterator.SEG_MOVETO:
    moveX = lastX = points[0];
    moveY = lastY = points[1];
    result.moveTo( moveX, moveY );
    next = 0;
    break;
   /* Una constante que indica que el anterior trazado debe ser cerrado
    * añadiendo una linea al segmento uniendo el punto correspondiente
    * al anterior SEG_MOVETO.
    */
   case PathIterator.SEG_CLOSE:
    points[0] = moveX;
    points[1] = moveY;
    // Fall into....
   /* La constante para un punto que indica el punto final de una linea que
    * se dibuja desde el punto especificado con anterioridad.
    */
   case PathIterator.SEG_LINETO:
    thisX = points[0];
    thisY = points[1];
    float dx = thisX-lastX;
    float dy = thisY-lastY;
    float distance = (float) Math.sqrt(dx*dx + dy*dy);
    if (distance >= next) {
     float r = 1.0f / distance;
     float angle = (float) Math.atan2(dy, dx);
     while (currentShape < length && distance >= next) {
      float x = lastX + next * dx * r;
      float y = lastY + next * dy * r;
      t.setToTranslation( x, y );
      t.rotate(angle);
      result.append(t.createTransformedShape(shapes[currentShape]), false);
      next += advance;
      currentShape++;
      currentShape %= length;
     }
    }
    next -= distance;
    lastX = thisX;
    lastY = thisY;
    break;
   }
   it.next();
  }

  return result;
 }

}


Además podemos, por ejemplo, combinar varios Strokes. A continuación os muestro como crear un lápiz compuesto por otros 2 creados previamente. Basta con hacer que el nuevo Stroke devuelva una forma creada a partir de los trazados de los 2 previamente creados. Así pues, podemos crear un trazado compuesto por un trazado simple de 10px de grosor y otro de 0.5px de la siguiente manera.


package strokes.samples;

import java.awt.Shape;
import java.awt.Stroke;

public class StrokeCompuesto implements Stroke {

 // Trazos básicos
 private Stroke stroke1;
 private Stroke stroke2;

 /**
  * Crea un trazo compuesto por 2 trazos.
  */
 public StrokeCompuesto(Stroke stroke1, Stroke stroke2) {
   this.stroke1 = stroke1;
   this.stroke2 = stroke2;
  }

 @Override
 public Shape createStrokedShape(Shape shape) {
  // Simplemente creamos el contorno del trazado usando otro trazado dado.
  return stroke2.createStrokedShape(stroke1.createStrokedShape(shape));
 }

}

Otra cosa que me parece muy interesante y que a muchos os habrá dado quebraderos de cabeza es lo que indica el título de la entrada, dibujar texto siguiendo un camino. Los caracteres al fin y al cabo son formas también, por lo que podemos decirle a nuestra interfaz Stroke que nos devuelva un lápiz que pinte nuestra cadena de texto de una manera similar a como hicimos con las formas geométricas.

package strokes.samples;

import java.awt.Font;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.FlatteningPathIterator;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;

public class StrokeTexto implements Stroke {

 // Texto y fuente a utilizar
 private String text;
 private Font font;

 // Valores necesarios para dibujar el Path (trazado)
 private boolean stretchToFit = false;
 private AffineTransform t = new AffineTransform();

 /**
  * Crearemos un trazado a partir de una cadena de texto.
  * @param text
  * @param font
  * @param stretchToFit Indica si queremos que el texto ocupe todo el trazado
  * @param repeat Indica si queremos que la cadena se repita
  */
 public StrokeTexto( String text, Font font, boolean stretchToFit) {
  this.text = text;
  this.font = font;
  this.stretchToFit = stretchToFit;
 }

 @Override
 public Shape createStrokedShape(Shape shape) {
  // Crearemos un vector con los caracteres dados, recordemos que cada uno de los caracteres (o glifos) son simples formas
  FontRenderContext frc = new FontRenderContext(null, true, true);
  GlyphVector glyphVector = font.createGlyphVector(frc, text);

  GeneralPath result = new GeneralPath();
  PathIterator it = new FlatteningPathIterator( shape.getPathIterator( null ), 1 );
  float points[] = new float[6];
  float moveX = 0, moveY = 0;
  float lastX = 0, lastY = 0;
  float thisX = 0, thisY = 0;
  int type = 0;
  float next = 0;
  int currentChar = 0;
  int length = glyphVector.getNumGlyphs();

  if ( length == 0 )
            return result;

  // Si queremos que el texto se adapte al trazado que vamos a realizar, necesitaremos un factor que adapte las longitudes
        float factor = stretchToFit ? measurePathLength(shape) / (float)glyphVector.getLogicalBounds().getWidth() : 1.0f;
        float nextAdvance = 0;

        /* Todas estas operaciones son necesarias para renderizar la forma del trazado.
   * La interfaz PathIterator proporciona el mecanismo para que objetos que implementan
   * la interfaz Shape devuelvan la geometria de sus formas, permitiendo que quien los llama
   * recupere el trazado de esa forma segmento por segmeto.
   */
  while (currentChar < length && !it.isDone()) {
   type = it.currentSegment(points);
   switch(type){
   /* Una constante que indica que en un segmento indica que un
    * punto debe comenzar un nuevo trazado.
    */
   case PathIterator.SEG_MOVETO:
    moveX = lastX = points[0];
    moveY = lastY = points[1];
    result.moveTo(moveX, moveY);
                nextAdvance = glyphVector.getGlyphMetrics(currentChar).getAdvance() * 0.5f;
                next = nextAdvance;
    break;
   /* Una constante que indica que el anterior trazado debe ser cerrado
    * añadiendo una linea al segmento uniendo el punto correspondiente
    * al anterior SEG_MOVETO.
    */
   case PathIterator.SEG_CLOSE:
    points[0] = moveX;
    points[1] = moveY;
    // Fall into....
   /* La constante para un punto que indica el punto final de una linea que
    * se dibuja desde el punto especificado con anterioridad.
    */
   case PathIterator.SEG_LINETO:
    thisX = points[0];
    thisY = points[1];
    float dx = thisX-lastX;
    float dy = thisY-lastY;
    float distance = (float)Math.sqrt( dx*dx + dy*dy );
    if (distance >= next) {
     float r = 1.0f/distance;
     float angle = (float)Math.atan2( dy, dx );
     while (currentChar < length && distance >= next) {
      Shape glyph = glyphVector.getGlyphOutline( currentChar );
      Point2D p = glyphVector.getGlyphPosition(currentChar);
      float px = (float)p.getX();
      float py = (float)p.getY();
      float x = lastX + next*dx*r;
      float y = lastY + next*dy*r;
                        float advance = nextAdvance;
                        nextAdvance = currentChar < length-1 ? glyphVector.getGlyphMetrics(currentChar+1).getAdvance() * 0.5f : 0;
      t.setToTranslation( x, y );
      t.rotate( angle );
      t.translate( -px-advance, -py );
      result.append( t.createTransformedShape(glyph), false );
      next += (advance+nextAdvance) * factor;
      currentChar++;
      currentChar %= length;
     }
    }
                next -= distance;
    lastX = thisX;
    lastY = thisY;
    break;
   }
   it.next();
  }

  return result;
 }

 /**
  * Mide la longitud del trazado.
  * @param shape
  * @return la longitud
  */
 public float measurePathLength(Shape shape) {
  PathIterator it = new FlatteningPathIterator(shape.getPathIterator( null ), 1);
  float points[] = new float[6];
  float moveX = 0, moveY = 0;
  float lastX = 0, lastY = 0;
  float thisX = 0, thisY = 0;
  int type = 0;
        float total = 0;

  while (!it.isDone()) {
   type = it.currentSegment( points );
   switch(type){
   case PathIterator.SEG_MOVETO:
    moveX = lastX = points[0];
    moveY = lastY = points[1];
    break;

   case PathIterator.SEG_CLOSE:
    points[0] = moveX;
    points[1] = moveY;
    // Fall into....

   case PathIterator.SEG_LINETO:
    thisX = points[0];
    thisY = points[1];
    float dx = thisX-lastX;
    float dy = thisY-lastY;
    total += (float)Math.sqrt(dx * dx + dy * dy);
    lastX = thisX;
    lastY = thisY;
    break;
   }
   it.next();
  }

  return total;
 }

}



package strokes;

import java.awt.BasicStroke;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.GlyphVector;
import java.awt.geom.Ellipse2D;

import javax.swing.JPanel;

import strokes.samples.StrokeCompuesto;
import strokes.samples.StrokeGeometrico;
import strokes.samples.StrokeTexto;

public class StrokesSamples extends JPanel {

 private StrokeCompuesto compStroke;
 private StrokeGeometrico geomStroke;
 private StrokeTexto textStroke;
 private Font font;

 /**
  * Constructor.
  */
 public StrokesSamples() {

  // Un trazo hecho compuesto por cuadrados y circulos
  geomStroke = new StrokeGeometrico(
    new Shape[] {
      new Rectangle(5, 5),
      new Ellipse2D.Float(0, 0, 4, 4)
      }, 10.0f);

  // Un trazo compuesto por 2 trazos.
  compStroke = new StrokeCompuesto(new BasicStroke(10f), new BasicStroke(0.5f));

  // Un trazo compuesto por una cadena de caracteres
  font = new Font("Arial", Font.PLAIN, 12);
  textStroke = new StrokeTexto("Esto es una cadena", font, false);


 }

 @Override
 public void paint(Graphics g) {

  Graphics2D g2d = (Graphics2D) g;
  // Activamos el anti aliasing para que nuestros trazados se rendericen en condiciones optimas
  g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

  // Esto lo hacemos para obtener el contorno de la cadena "A B C"
  Font font = new Font("Arial", Font.PLAIN, 100);
  g2d.setFont(font);
  GlyphVector gv = font.createGlyphVector(g2d.getFontRenderContext(), "A B C");
  // Así podremos dibujar esta forma con nuestros lápices
  Shape shape = gv.getOutline();

  // Dibujamos usando nuestro trazo con formas
  g2d.setStroke(geomStroke);
  g2d.drawLine(5, 10, 105, 25);
  g2d.drawOval(5, 30, 75, 75);
  // el método translate desplaza el origen de coordenadas al punto específicado
  g2d.translate(105, 75);
  g2d.draw(shape);
  g2d.translate(-105, -75);

  // Dibujamos usando el trazado compuesto
  g2d.setStroke(compStroke);
  g2d.drawLine(5, 120, 105, 135);
  g2d.drawOval(5, 140, 75, 75);
  g2d.translate(105, 185);
  g2d.draw(shape);
  g2d.translate(-105, -185);

  // Dibujamos usando el trazo de cadenas de texto
  g2d.setStroke(textStroke);
  g2d.drawLine(5, 230, 105, 245);
  g2d.drawOval(5, 250, 75, 75);
  g2d.translate(105, 295);
  g2d.draw(shape);
  g2d.translate(-105, -295);
 };

}


Bien, solo queda dejaros el método Main que lanza nuestra aplicación de ejemplo. Un saludo y gracias a todos por leerme.

package strokes;

import javax.swing.JFrame;


public class Main {

 private static void createAndShowGUI() {
  JFrame frame = new JFrame("Ejemplos de uso de strokes");

  StrokesSamples ejemplos = new StrokesSamples();

     frame.getContentPane().add(ejemplos);
     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
     frame.setSize(400, 400);
     frame.setVisible(true);
 }

 public static void main(String[] args) {

  javax.swing.SwingUtilities.invokeLater(new Runnable() {
   public void run() {
    createAndShowGUI();
   }
  });
 }
}


miércoles, 19 de octubre de 2011

Manipulacion y tratamiento de imagenes con Swing. Escala de grises, imagen espejo, imagen translucida

5 comentarios
Hola de nuevo amigos, hoy vamos a deleitarnos con algunas de las herramientas que nos ofrece Java2D para el tratamiento de imágenes.

En primer lugar os presentaré una imagen original y sobre esta iremos aplicando diversas transformaciones para hacer que parezca una imagen totalmente distinta.

En primer lugar os dejo el código para lanzar nuestra aplicación y a medida que vayamos avanzando, iremos explicando cada una de las transformaciones que hemos aplicado a nuestra imagen original.



package tratamientoImagenes;

import javax.swing.JFrame;


public class Main {

 private static void createAndShowGUI() {
  JFrame frame = new JFrame("Tratamiento de imágenes con Swing");

  Expositor panel = new Expositor();

     frame.getContentPane().add(panel);
     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
     frame.setSize(800, 534);
     frame.setVisible(true);
 }

 public static void main(String[] args) {

  javax.swing.SwingUtilities.invokeLater(new Runnable() {
   public void run() {
    createAndShowGUI();
   }
  });
 }
}

Este es nuestro panel expositor, mediante un GridLayout añadiremos 4 paneles, cada uno de ellos con una imagen de fondo y un aspecto distinto a pesar de ser la misma imagen de base.

package tratamientoImagenes;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.net.URL;

import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class Expositor extends JPanel {

 /**
  * Constructor.
  */
 public Expositor() {
  /*
   * Creamos un layout de 4 paneles, en cada uno mostraremos una imagen
   * "aparentemente" distinta
   */
  super(new GridLayout(2, 2));
  setPreferredSize(new Dimension(800, 534));

  // Los paneles con las imagenes
  Original original = new Original();
  EscalaDeGrises escalaDeGrises = new EscalaDeGrises();
  Espejo espejo = new Espejo();
  Translucida translucida = new Translucida();

  Dimension photoDimension = new Dimension(400, 267);
  original.setPreferredSize(photoDimension);
  escalaDeGrises.setPreferredSize(photoDimension);
  espejo.setPreferredSize(photoDimension);
  translucida.setPreferredSize(photoDimension);

  original.setOpaque(false);
  escalaDeGrises.setOpaque(false);
  espejo.setOpaque(false);
  translucida.setOpaque(false);

  // Etiquetas
  JLabel originalLabel = new JLabel("ORIGINAL");
  JLabel escalaDeGrisesLabel = new JLabel("ESCALA DE GRISES");
  JLabel espejoLabel = new JLabel("ESPEJO");
  JLabel translucidaLabel = new JLabel("TRANSLUCIDA");
  originalLabel.setForeground(Color.WHITE);
  escalaDeGrisesLabel.setForeground(Color.WHITE);
  espejoLabel.setForeground(Color.WHITE);
  translucidaLabel.setForeground(Color.WHITE);

  original.add(originalLabel);
  escalaDeGrises.add(escalaDeGrisesLabel);
  espejo.add(espejoLabel);
  translucida.add(translucidaLabel);

  this.add(original);
  this.add(escalaDeGrises);
  this.add(espejo);
  this.add(translucida);

 }

 /**
  * Para recuperar una imagen de un archivo...
  *
  * @param path Ruta de la imagen relativa al proyecto
  * @return una imagen
  */
 public static ImageIcon createImage(String path) {
  URL imgURL = Expositor.class.getResource(path);
  if (imgURL != null) {
   return new ImageIcon(imgURL);
  } else {
   System.err.println("Couldn't find file: " + path);
   return null;
  }
 }

}

Y ahora empezamos con los paneles, primero presentamos la imagen original para que podáis apreciar los cambios que iremos realizando, el como pintar un panel transparente con una imagen de fondo ya lo tratamos en una entrada anterior JPanel con imagen de fondo.

package tratamientoImagenes;

import java.awt.Graphics;

import javax.swing.ImageIcon;
import javax.swing.JPanel;

public class Original extends JPanel {

 // Cargamos la imagen original
 private ImageIcon photo = Expositor.createImage("imagenes/ER.jpg");

 @Override
 public void paint(Graphics g) {

  g.drawImage(photo.getImage(), 0, 0, null);

  // Pintamos el resto de componentes
  super.paint(g);
 }

}

Una transformación que personalmente me gusta bastante, pasaremos la imagen original por uno de los filtros de RGBImageFilter, en este caso el filtro de escala de grises. Este tipo de filtro hará que los pixeles se vean mas claros para posteriormente aplicarle tonos grises a la imagen. Así es como lo hemos usado:

package tratamientoImagenes;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.awt.image.ImageProducer;

import javax.swing.GrayFilter;
import javax.swing.ImageIcon;
import javax.swing.JPanel;

public class EscalaDeGrises extends JPanel {

 // Cargamos la imagen original
 private ImageIcon photo = Expositor.createImage("imagenes/ER.jpg");

 @Override
 public void paint(Graphics g) {

  // Imagen
  Image img = photo.getImage();

  // Filtro de escala de grises
  ImageFilter filter = new GrayFilter(true, 50);
  ImageProducer producer = new FilteredImageSource(img.getSource(), filter);

  ImageIcon newIcon = new ImageIcon(this.createImage(producer));
  newIcon.paintIcon(this, g, 0, 0);

  // Pintamos el resto de componentes
  super.paint(g);
 }

}

Cuando dibujamos una imagen, podemos aplicarle una transformada para girarla como nos apetezca, usamos para ello el método drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver observer); del objeto Graphics2D. Este método nos permite cosas como dibujar parte de una imagen en otra imagen, escalar una imagen y otras tantas. Tomamos la imagen y una serie de coordenadas del rectángulo destino (dx, dy) así como del rectángulo origen (sx, sy). Manipulando estas coordenadas podemos hacer que la imagen se voltee como queramos.

En Java las coordenadas empiezan en la esquina superior izquierda y terminan en la inferior derecha, así pues para girar una imagen como si estuviésemos frente a un espejo modificamos las coordenadas del rectángulo origen para que empiecen en el borde superior derecho y terminen en el inferior izquierdo.

En el ejemplo las modificamos para voltear la imagen 180 grados, es decir, como si viésemos un reflejo en un lago. Tomaremos entonces como origen la esquina inferior izquierda y terminamos en la superior derecha.

package tratamientoImagenes;

import java.awt.Graphics;
import java.awt.Image;

import javax.swing.ImageIcon;
import javax.swing.JPanel;

public class Espejo extends JPanel {

 // Cargamos la imagen original
 private ImageIcon photo = Expositor.createImage("imagenes/ER.jpg");

 @Override
 public void paint(Graphics g) {

  // Imagen
  Image img = photo.getImage();

  int width = photo.getIconWidth();
  int height = photo.getIconHeight();

  // Rotamos la imagen
  g.drawImage(img, 0, 0, width, height, 0, height, width, 0, null);

  // Pintamos el resto de componentes
  super.paint(g);
 }


Y por último haremos translucida la imagen original, para ello deberemos cargar todos los pixeles de la imagen en una BufferedImage, y sobre esta aplicar un filtro modificando el componente alpha de cada pixel.

package tratamientoImagenes;

import java.awt.AlphaComposite;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;

import javax.imageio.ImageIO;
import javax.swing.JPanel;

public class Translucida extends JPanel {

 @Override
 public void paint(Graphics g) {

  // En este caso tendremos que trabajar con una BufferedImage
  URL imgURL = Expositor.class.getResource("imagenes/ER.jpg");
  BufferedImage img = null;
  try {
   img = ImageIO.read(imgURL);
  } catch (IOException e) {
   e.printStackTrace();
  }
  int w = img.getWidth(null);
  int h = img.getHeight(null);

  // Necesitaremos una imagen auxiliar a la que aplicar un filtro
  BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
  Graphics gI = bi.getGraphics();

  // Sobre esta imagen dibujaremos la imagen original
  gI.drawImage(img, 0, 0, null);

  Graphics2D g2d = (Graphics2D) g;
  // Establecemos la componente alpha de nuestros graficos 50%
  g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
  /* Draw the image, applying the filter */
  g2d.drawImage(bi, null, 0, 0);

  // Pintamos el resto de componentes
  super.paint(g);
 }

}

Y así termina todo, espero que os haya resultado útil este mini-tutorial sobre tratamiento de imágenes.

Un saludo y hasta pronto.

martes, 27 de septiembre de 2011

Reloj análogico con Swing, ejemplo Swing Timer

1 comentarios
Hoy profundizaremos un poco en el Swing Timer, una instancia de Timer dispara una o varias acciones después de un retardo especificado.



Se recomienda el uso de Swing timers para la realización de tareas relacionadas con la interfaz de usuario (GUI-task) ya que todos los timers comparten un mismo hilo y todas las tareas se ejecutan automáticamente en el hilo de despacho de eventos (event-dispatch thread).



Su uso es muy fácil, cuando creamos el timer, especificamos un action listener que se notificará cuando el temporizador acaba. Es válido tanto para tareas repetitivas como para realizar una sola tarea transcurrido un cierto retraso, para este último fin podremos invocar setRepeats(false) en el timer.

Para inciarlo se llama a su método start(), para suspenderlo llamamos a stop().



//Timer
	Timer timer = new Timer(TIMER_UPDATE, new ActionListener() {
		public void actionPerformed(ActionEvent e) {
			// Tareas repetitivas...
			}
		});

timer.start();


Como ejemplo de uso de un Swing timer, les presento la implementación de un reloj analógico hecho con Swing.



Este sería el resultado y aquí a continuación os dejo el código.




En primer lugar nuestra clase reloj que definirá y arrancará el Timer.

package reloj;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Calendar;

import javax.swing.JPanel;
import javax.swing.Timer;


public class Reloj extends JPanel {

	/** Tiempo de actualizacion. */
	private static final int TIMER_UPDATE = 1000;

	/** Calendar. */
	private Calendar calendar = null;

	/**
	 * Constructor.
	 */
	public Reloj() {

		setUI(new RelojUI());

		//Inicializamos el calendario
		calendar = Calendar.getInstance();

		//Establecemos el tamaño a nuestro panel
		setPreferredSize(new Dimension(295, 270));

		//Timer para actualizar el calendario
		Timer timer = new Timer(TIMER_UPDATE, new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				//Añadimos 1 segundo al calendario
				calendar.add(Calendar.SECOND, 1);
				// repintamos
				repaint();
			}
		});
		timer.start();
	}

	/**
	 * Recupera la instancia de Calendar
	 * @return calendar
	 */
	public Calendar getCalendar() {
		return this.calendar;
	}

}
Con cada iteración del Timer llamamos al método repaint() de nuestro reloj, repintando así las manecillas de acuerdo a la hora actual que nos devolvera la instancia de Calendar que hemos definido y actualizaremos también con cada iteración.

A continuación el UI del reloj, que se encargará de redibujarlo cada segundo según establecimos en el retardo del Timer.



package reloj;

import java.awt.BasicStroke;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.net.URL;
import java.util.Calendar;

import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.PanelUI;

public class RelojUI extends PanelUI {

	private ImageIcon backgroundImage = null;

	private BasicStroke handsStroke;

	public static ComponentUI createUI(JComponent c) {
		return new RelojUI();
	}

	@Override
	public void installUI(JComponent c) {
		super.installUI(c);
		//Hacemos el panel transparente
		c.setOpaque(false);
		backgroundImage = createImage("imagenes/reloj.jpg");
		// pincel que utilizaremos para las manecillas de los minutos y horas
		handsStroke = new BasicStroke(2f);
	}

	@Override
	public void paint(Graphics g, JComponent c) {

		Graphics2D g2d = (Graphics2D) g;
		// imagen de fondo
		if (backgroundImage != null) {
			g2d.drawImage(backgroundImage.getImage(), 0, 0, null);
		}
		Reloj reloj = (Reloj) c;
		int hour = reloj.getCalendar().get(Calendar.HOUR);
		int minute = reloj.getCalendar().get(Calendar.MINUTE);
		int second = reloj.getCalendar().get(Calendar.SECOND);

		// acotamos la parte donde dibujaremos las manecillas del reloj
		Shape oval = new Ellipse2D.Double(0, 0, 167, 164);
		int centerX = (int) (oval.getBounds().getWidth() / 2);
		int centerY = (int) (oval.getBounds().getHeight() / 2);

		// puntos donde acaban las manecillas del reloj
		int xh, yh, xm, ym, xs, ys;

        // calculamos la posicion de las manecillas del reloj
		xs = (int) (Math.cos(second * Math.PI / 30 - Math.PI / 2) * 75 + centerX);
        ys = (int) (Math.sin(second * Math.PI / 30 - Math.PI / 2) * 75 + centerY);
        xm = (int) (Math.cos(minute * Math.PI / 30 - Math.PI / 2) * 60 + centerX);
        ym = (int) (Math.sin(minute * Math.PI / 30 - Math.PI / 2) * 60 + centerY);
        xh = (int) (Math.cos((hour * 30 + minute / 2) * Math.PI / 180 - Math.PI / 2) * 45 + centerX);
        yh = (int) (Math.sin((hour * 30 + minute / 2) * Math.PI / 180 - Math.PI / 2) * 45 + centerY);

		//offset, movemos el origen de las coordenadas usadas para dibujar
        //al vertice superior izquierdo del rectángulo que contiene la esfera del reloj.
		g.translate(48, 78);

		//dibujamos los numeros y las manecillas
		g.drawString("9", 10, centerY+5);
        g.drawString("3", (int) (oval.getBounds().getWidth() - 10), centerY+5);
        g.drawString("12", centerX-6, 20);
        g.drawString("6", centerX-3, (int) (oval.getBounds().getHeight() - 10));

		// segundos
        g.drawLine(centerX, centerY, xs, ys);
        // minutos
        g2d.setStroke(handsStroke);
        g.drawLine(centerX, centerY, xm, ym);
        //horas
        g.drawLine(centerX, centerY, xh, yh);

        // - offset
		g.translate(-47, -78);

	}

	/**
	 * Para recuperar una imagen de un archivo...
	 * @param path Ruta de la imagen relativa al proyecto
	 * @return una imagen
	 */
	public ImageIcon createImage(String path) {
		URL imgURL = getClass().getResource(path);
	    if (imgURL != null) {
	        return new ImageIcon(imgURL);
	    } else {
	        System.err.println("Couldn't find file: " + path);
	        return null;
	    }
	}

}
Y por último la clase main que lanzará la aplicación.



package reloj;

import javax.swing.JFrame;


public class Main {

	private static void createAndShowGUI() {
		JFrame frame = new JFrame("Reloj con Swing");

		Reloj reloj = new Reloj();

	    frame.getContentPane().add(reloj);
	    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	    frame.setSize(302, 302);
	    frame.setVisible(true);
	}

	public static void main(String[] args) {

		javax.swing.SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				createAndShowGUI();
			}
		});
	}
}

Un saludo y hasta la próxima!

jueves, 11 de agosto de 2011

Colspan JTable, expandir columnas en tablas de Swing

8 comentarios
Hola de nuevo,

Hoy vamos a intentar explicar como expandir las columnas de un JTable, esto es muy común y fácil cuando hablamos de código HTML con las propiedades colspan y rowspan de las tablas. Pero en Swing esta característica no está contemplada, por lo que tendremos que darle una vuelta a la gestión de las celdas de un JTable.

En primer lugar necesitaremos un mapeo de las celdas que queremos expandir, para ello crearemos un mapa con las claves de fila y columna que indican la celda. Nos valdremos de la siguiente interfaz para gestionar nuestro mapa y la información que necesitamos.


package colspanTable;

import java.util.HashMap;

public interface ColSpanMap {

	/**
	 * Indica si la celda [row,column] esta expandida
	 * @param row celda logica de fila
	 * @param column celda logica de columna
	 * @return numero de columnas expandiodas por celda
	 */
	int span(int row, int column);

	/**
	 * Devuelve el indice de las celda visibles.
	 * @param row celda logica de fila
	 * @param column celda logica de columna
	 * @return indice de la celda visible que cubre la celda logica
	 */
	int visibleCell(int row, int column);

	/**
	 * Establece el mapeo de filas/columnas con celdas expandidas y su posicion.
	 * @param spanMap Mapa
	 */
	void setSpanMap(HashMap< Integer, Integer > spanMap);

}

La implementación debería seguir un modelo similar a este que os presento a continuación.


package colspanTable;

import java.util.HashMap;

class TableColSpanMap implements ColSpanMap {

	/** Columnas a expandir por celda. */
	private static final int CELLS_TO_SPAN = 5;

	/** Mapeo de las celdas a expandir. */
	private HashMap< Integer, Integer > spanMap;

	@Override
	public int span(int row, int column) {
		if (spanMap != null && spanMap.containsKey(row)) {
			if (spanMap.get(row) == column) {
				return CELLS_TO_SPAN;
			}
		}
		return 1;
	}

	@Override
	public int visibleCell(int row, int column) {
		if (spanMap != null && spanMap.containsKey(row)) {
			if (column >= spanMap.get(row) && column < spanMap.get(row) + CELLS_TO_SPAN) {
				return spanMap.get(row);
			}
		}
		return column;
	}

	@Override
	public void setSpanMap(HashMap< Integer, Integer > spanMap) {
		this.spanMap = spanMap;
	}
}

Ahora en nuestra tabla, a la hora de consultar la posición, índice y anchura de las celdas de la tabla deberemos de tener en cuenta nuestro mapeo especial, para ello sobrescribimos los métodos getCellRect para el tamaño de celda y getColumnAtPoint para buscar los índices correctos a la hora de editarla, además al contruir nuestra tabla le pasaremos en el constructor el mapeo de las celdas expandidas. Los cambios los muestro a continuación:

package colspanTable;

import java.awt.Point;
import java.awt.Rectangle;

import javax.swing.JTable;
import javax.swing.table.TableModel;

public class SpanTable extends JTable {

	/** Map with expanded columns mapped. */
	private ColSpanMap map;

	/**
	 * Constructor.
	 * @param csm Mapa de celdas
	 * @param tbl Modelo de la tabla
	 */
	public SpanTable(ColSpanMap csm, TableModel tbl) {
		super(tbl);
		map = csm;
		setUI(new ColSpanTableUI());
	}

	/**
	 * Nos devuelve el mapa de las celdas a expandir.
	 * @return map
	 */
	public ColSpanMap getSpanMap() {
		return map;
	}

	@Override
	public Rectangle getCellRect(int row, int column, boolean includeSpacing) {
		// required because getCellRect is used in JTable constructor
		if (map == null)
			return super.getCellRect(row, column, includeSpacing);
		// add widths of all spanned logical cells
		int sk = map.visibleCell(row, column);
		Rectangle r1 = super.getCellRect(row, sk, includeSpacing);
		if (map.span(row, sk) != 1)
			for (int i = 1; i < map.span(row, sk); i++) {
				r1.width += getColumnModel().getColumn(sk + i).getWidth();
			}
		return r1;
	}

	@Override
	public int columnAtPoint(Point p) {
		int x = super.columnAtPoint(p);
		// -1 is returned by columnAtPoint if the point is not in the table
		if (x < 0)
			return x;
		int y = super.rowAtPoint(p);
		return map.visibleCell(y, x);
	}
}
Por último solo nos quedaría cuidarnos de la parte gráfica, tendremos que establecerle un UI peculiar a la tabla que nos pinte la rejilla de las celdas especial que necesitamos donde tendremos en cuenta las partes visibles de cada celda.
package colspanTable;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;

import javax.swing.JComponent;
import javax.swing.plaf.basic.BasicTableUI;
import javax.swing.table.TableCellRenderer;

public class ColSpanTableUI extends BasicTableUI {

	@Override
	public void paint(Graphics g, JComponent c) {
		Rectangle r = g.getClipBounds();
		int firstRow = table.rowAtPoint(new Point(0, r.y));
		int lastRow = table.rowAtPoint(new Point(0, r.y + r.height));
		// -1 is a flag that the ending point is outside the table
		if (lastRow < 0)
			lastRow = table.getRowCount() - 1;
		for (int i = firstRow; i <= lastRow; i++)
			paintRow(i, g);
	}

	private void paintRow(int row, Graphics g) {
		Rectangle r = g.getClipBounds();
		for (int i = 0; i < table.getColumnCount(); i++) {
			Rectangle r1 = table.getCellRect(row, i, true);
			if (r1.intersects(r)) // at least a part is visible
			{
				int sk = ((SpanTable) table).getSpanMap().visibleCell(row, i);
				paintCell(row, sk, g, r1);
				// increment the column counter
				i += ((SpanTable) table).getSpanMap().span(row, sk) - 1;
			}
		}
	}

	private void paintCell(int row, int column, Graphics g, Rectangle area) {
		int verticalMargin = table.getRowMargin();
		int horizontalMargin = table.getColumnModel().getColumnMargin();

		Color c = g.getColor();
		g.setColor(table.getGridColor());
		g.drawRect(area.x, area.y, area.width - 1, area.height - 1);
		g.setColor(c);

		area.setBounds(area.x + horizontalMargin / 2, area.y + verticalMargin / 2, area.width - horizontalMargin, area.height - verticalMargin);

		if (table.isEditing() && table.getEditingRow() == row && table.getEditingColumn() == column) {
			Component component = table.getEditorComponent();
			component.setBounds(area);
			component.validate();
		} else {
			TableCellRenderer renderer = table.getCellRenderer(row, column);
			Component component = table.prepareRenderer(renderer, row, column);
			if (component.getParent() == null)
				rendererPane.add(component);
			rendererPane.paintComponent(g, component, table, area.x, area.y, area.width, area.height, true);
		}
	}
}
Bien, espero que todo este lo más claro posible. Como siempre os dejo un ejemplo funcionando :)
package colspanTable;

import java.util.HashMap;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;

public class Test {

	private static void createAndShowGUI() {
		JFrame frame = new JFrame("Tabla con celdas expandidas");

	    ColSpanMap m = new TableColSpanMap();
	    HashMap< Integer, Integer > cellSpanMap = new HashMap();
	    // Aqui los indices [fila,columna] de las celdas que queremos expandir
	    cellSpanMap.put(3, 3);
	    cellSpanMap.put(4, 7);
	    cellSpanMap.put(9, 5);
	    m.setSpanMap(cellSpanMap);
	    TableModel tm=new DefaultTableModel(15,20);
	    SpanTable table = new SpanTable(m,tm);

	    frame.getContentPane().add(new JScrollPane(table));
	    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	    frame.setSize(500, 295);
	    frame.setVisible(true);
	}

	public static void main(String[] args) {

		javax.swing.SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				createAndShowGUI();
			}
		});
	}
}

Espero que os siga siendo útil todo lo que aquí se dice, vuestros comentarios y dudas siempre son bienvenidos.

Un saludo y hasta la próxima


lunes, 13 de junio de 2011

JPanel transparente, JPanel con imagen de fondo

11 comentarios
Hola de nuevo, después de casi un mes sin Internet y sin poder pasaros contenido nuevo, volvemos con un ejemplo muy sencillo de algo que os puede resultar muy útil a muchos.

Se trata de hacer que nuestro JPanel sea transparente y ponerle detrás una imagen de fondo. El resultado es muy bueno.
En primer lugar haremos que nuestro panel sea transparente, esto es, básicamente haciendo que no sea opaco estableciéndole esta propiedad al crear el panel. Y en segundo lugar sobrescribiremos el método paint(Graphics g) de este panel para que antes de pintar todos los elementos nos dibuja la imagen de fondo que le hayamos establecido. ¿Fácil verdad?

Primero el código del panel:

package ejemplo7;

import java.awt.Graphics;
import java.awt.Image;
import java.net.URL;

import javax.swing.ImageIcon;
import javax.swing.JPanel;

/**
 * TransparentPanel.
 */
public class TransparentPanel extends JPanel {

 private Image bgImage;

 public TransparentPanel() {
  super();

  // Hacemos que el panel sea transparente
  this.setOpaque(false);
 }

 /**
  * Lo utilizaremos para establecerle su imagen de fondo.
  * @param bgImage La imagen en cuestion
  */
 public void setBackgroundImage(Image bgImage) {
  this.bgImage = bgImage;
 }

 /**
  * Para recuperar una imagen de un archivo...
  * @param path Ruta de la imagen relativa al proyecto
  * @return una imagen
  */
 public ImageIcon createImage(String path) {
  URL imgURL = getClass().getResource(path);
     if (imgURL != null) {
         return new ImageIcon(imgURL);
     } else {
         System.err.println("Couldn't find file: " + path);
         return null;
     }
 }

 @Override
 public void paint(Graphics g) {

  // Pintamos la imagen de fondo...
  if(bgImage != null) {
   g.drawImage(bgImage, 0, 0, null);
  }

  // Y pintamos el resto de cosas que pueda tener el panel
  super.paint(g);

 }

Y ahora como siempre el código de ejemplo:

package ejemplo7;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

/**
 * Ejemplo7.
 */
public class Ejemplo7 {

 private static JFrame frame;

 private static JPanel createExamplePanel() {
  TransparentPanel panel = new TransparentPanel();

  panel.setBackgroundImage(panel.createImage("images/bgImage.jpg").getImage());

  JLabel label = new JLabel("Esto y todo lo que queramos se pinta encima de la imagen de fondo");
  panel.add(label);

  return panel;
 }

 /**
  * Create the GUI and show it. For thread safety,
  * this method should be invoked from the
  * event dispatch thread.
  */
 private static void createAndShowGUI() {

  // Create and set up the window.
  frame = new JFrame("Panel with background image");
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

  Component contents = createExamplePanel();
  frame.getContentPane().add(contents, BorderLayout.CENTER);

  //Set window size
  frame.setPreferredSize(new Dimension(700,525));

  // Display the window.
  frame.pack();
  frame.setLocationByPlatform(true);
  frame.setVisible(true);
 }

 public static void main(String[] args) {
  // Schedule a job for the event dispatch thread:
  // creating and showing this application's GUI.
  javax.swing.SwingUtilities.invokeLater(new Runnable() {
   public void run() {
    createAndShowGUI();
   }
  });
 }

Así es como quedaría la cosa con la imagen que he elegido, obviamente el resultado dependerá de la imagen que carguéis vosotros.


Un saludo y hasta pronto, espero  :)

lunes, 9 de mayo de 2011

Bordes redondeados, creando nuestro propio borde para los componentes

1 comentarios
Hola de nuevo,

Hoy vamos a profundizar un poco en el tema de los elementos Border de los componentes Swing.

Todo componente al dibujarse lo hace en 2 partes, el contenido y su borde, así que hoy vamos a ver como personalizar un poco mas cualquiera de nuestros componentes añadiéndole un borde definido por nosotros mismos.

Aquí os dejo la clase de nuestro borde, con los métodos mínimos que toda clase que herede de AbstractBorder debería implementar. Esta es la clase de la que parten todos los bordes utilizados en Swing.
Ya sabéis que siempre pretendo dejaros un código auto-explicativo, pero para cualquier duda ya sabéis, dejadme un comentario y os contestaré en cuanto pueda.

package ejemplo5;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.geom.RoundRectangle2D;
import java.util.ArrayList;
import java.util.List;

import javax.swing.border.AbstractBorder;

/**
 * RoundedBorder.
 */
public class RoundedBorder extends AbstractBorder {

 private List<Color> colors;
 private int thickness;
 private int cornersArc;
 private int borders;

 /** Colores de los bordes. */
 private List<GradientPaint> gradients;
 /** Formas del bordes. */
 private List<RoundRectangle2D> rectangles;

 /**
  * @param color
  * @param thickness
  * @param roundedCorners
  */
 public RoundedBorder(List<Color> colors, int thickness, int cornersArc, int borders) {
  this.colors = colors;
  this.thickness = thickness;
  this.cornersArc = cornersArc;
  // Mínimo un borde
  this.borders = borders > 0 ? borders : 1;
 }

 @Override
 public void paintBorder(Component c, Graphics g, int x, int y, int w, int h) {

  createShapes(w, h, x, y);

  createGradients(c);

  Graphics2D g2d = ((Graphics2D) g);

  // Set a higher-quality rendering
  g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);

  for (int i = 0; i < borders; i++) {
   g2d.setPaint(gradients.get(i));

   g2d.setStroke(new BasicStroke(thickness));

   RoundRectangle2D auxRect = rectangles.get(i);
   g2d.drawRoundRect((int) auxRect.getX(), (int) auxRect.getY(), (int) auxRect.getWidth(), (int) auxRect.getHeight(), (int) auxRect.getArcWidth(), (int) auxRect.getArcHeight());
  }

 }

 /**
  * Creamos los gradientes.
  *
  * @param c Component
  */
 private void createGradients(Component c) {

  gradients = new ArrayList<GradientPaint>();
  for (int i = 0; i < borders; i++) {
   Color color = colors.get(i);
   GradientPaint gra = new GradientPaint(0, 0, color.darker(),
     0, c.getHeight(), color.brighter(), false);
   gradients.add(gra);
  }
 }

 /**
  * Creamos las formas del borde
  *
  * @param w ancho
  * @param h alto
  * @param x coordenada X
  * @param y coordenada Y
  */
 public void createShapes(int w, int h, int x, int y) {

  rectangles = new ArrayList<RoundRectangle2D>();
  int rectangleWidth = w, rectangleHeight = h;
  int xCoord = x;
  int yCoord = y;

  for (int i = 0; i < borders; i++) {
   if (i == 0) {
    xCoord += thickness / 2;
    yCoord += thickness / 2;
    rectangleWidth -= thickness;
    rectangleHeight -= thickness;
   } else {
    xCoord += thickness;
    yCoord += thickness;
    rectangleWidth -= 2 * thickness;
    rectangleHeight -= 2 * thickness;
   }

   RoundRectangle2D rectangle = new RoundRectangle2D.Float(xCoord, yCoord,
     rectangleWidth, rectangleHeight, cornersArc, cornersArc);

   rectangles.add(rectangle);

  }

 }

 /**
  * Este método nos dice cuanto espacio necesita el borde para dibujarse.
  */
 @Override
 public Insets getBorderInsets(Component c) {
  Insets insets = super.getBorderInsets(c);
  insets.left = insets.top = insets.right = insets.bottom += thickness * borders;
  return insets;
 }

}
Y aquí podéis ver un código de ejemplo que ilustrará el resultado obtenido:




package ejemplo5;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;

/**
 * Ejemplo5
 */
public class Ejemplo5 {

 private static JFrame frame;

 private static JPanel createExamplePanel() {
  JPanel panel = new JPanel();

  // Componentes de prueba
  JButton button = new JButton("Click");
  button.setPreferredSize(new Dimension(100, 100));
  button.setOpaque(false);
  JTextField textField = new JTextField(25);
  textField.setText("Escribe aqui...");
  textField.setOpaque(false);
  JTextArea textArea = new JTextArea(10, 25);
  textArea.setText("Escribe aqui...");
  textArea.setOpaque(false);

  List<Color> colors = new ArrayList<Color>();
  colors.add(Color.PINK);
  colors.add(Color.RED);

  button.setBorder(new RoundedBorder(colors, 5, 2, 2));

  colors = new ArrayList<Color>();
  colors.add(Color.PINK);
  colors.add(Color.RED);
  colors.add(Color.BLUE);
  colors.add(Color.YELLOW);

  textField.setBorder(new RoundedBorder(colors, 2,  5, 4));

  colors = new ArrayList<Color>();
  colors.add(Color.PINK);
  colors.add(Color.RED);
  colors.add(Color.BLUE);
  textArea.setBorder(new RoundedBorder(colors, 4, 5, 3));

  panel.add(button);
  panel.add(textField);
  panel.add(textArea);

  return panel;
 }

 /**
  * Create the GUI and show it. For thread safety,
  * this method should be invoked from the
  * event dispatch thread.
  */
 private static void createAndShowGUI() {

  // Create and set up the window.
  frame = new JFrame("RoundedBorder");
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

  Component contents = createExamplePanel();
  frame.getContentPane().add(contents, BorderLayout.CENTER);

  // Display the window.
  frame.pack();
  frame.setLocationByPlatform(true);
  frame.setVisible(true);
 }

 public static void main(String[] args) {
  // Schedule a job for the event dispatch thread:
  // creating and showing this application's GUI.
  javax.swing.SwingUtilities.invokeLater(new Runnable() {
   public void run() {
    createAndShowGUI();
   }
  });
 }

}

Un saludo y hasta pronto...

lunes, 25 de abril de 2011

Programa simple de dibujo con swing. Pintar en un lienzo usando MouseListener y MouseMotionListener

1 comentarios
Hola de nuevo.

Hoy vamos a empezar a captar los eventos de ratón y a utilizarlos para crear un programa simple de dibujo. Básicamente añadiremos unos listeners a un JFrame para lograr captar los eventos de ratón y así identificar los puntos por los que desplazamos el puntero.

Estos MouseAdapter y MouseMotionAdapter implementan las interfaces MouseListener y MouseMotionListener respectivamente, que se encargan de detectar eventos de ratón, como clicks, entradas y salidas del puntero en determinadas zonas, desplazamientos y arrastres de ratón.

En el ejemplo de hoy utilizaremos estos eventos para dibujar puntos sobre un lienzo, os dejo el código a continuación. Y ya sabéis, cualquier duda dejar un comentario y os responderé con la mayor brevedad posible.


import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;

import javax.swing.JFrame;
import javax.swing.JLabel;

public class Painter extends JFrame {

 private int cuentaPuntos = 0;

 private JLabel j = new JLabel();

 // arreglo de referencias a java.awt.Point
 private int maxPoints = 1500;
 private Point puntos[] = new Point[maxPoints];

 // configurar GUI y registrar manejador de eventos de ratón
 public Painter() {
  super("Un programa simple de dibujo");

  // crear una etiqueta y colocarla en la parte SOUTH del esquema
  // BorderLayout

  getContentPane().add(new JLabel("Arrastre el ratón para dibujar"), BorderLayout.SOUTH);
  getContentPane().add(j, BorderLayout.NORTH);

  // Ahora añadimos listeners propios para los eventos de ratón
  addMouseMotionListener(

  new MouseMotionAdapter() { // clase interna anónima

   // almacenar coordenadas de arrastre de ratón y llamar a repaint
   @Override
   public void mouseDragged(MouseEvent evento) {
    if (cuentaPuntos < puntos.length) {
     puntos[cuentaPuntos] = evento.getPoint();
     ++cuentaPuntos;
     repaint();
     j.setText("Van: " + cuentaPuntos + " puntos, le quedan: " + (maxPoints - cuentaPuntos) + " puntos");
    }
   }

  } // fin de la clase interna anónima

  ); // fin de la llamada a addMouseMotionListener

  addMouseListener(

  new MouseAdapter() { // clase interna anónima

   // almacenar coordenadas de click de ratón
   @Override
   public void mouseClicked(MouseEvent evento) {
    if (cuentaPuntos < puntos.length) {
     puntos[cuentaPuntos] = evento.getPoint();
     ++cuentaPuntos;
     repaint();
     j.setText("Van: " + cuentaPuntos + " puntos, le quedan: " + (maxPoints - cuentaPuntos) + " puntos");
    }
   }

  } // fin de la clase interna anónima

  ); // fin de la llamada a addMouseListener

  setSize(400, 200);
  setVisible(true);

 } // fin del constructor de Pintor

 // dibujar óvalo en un cuadro delimitador de 2 por 2 en ubicación
 // especificada en ventana
 @Override
 public void paint(Graphics g) {
  super.paint(g); // borra el área de dibujo

  for (int i = 0; i < puntos.length && puntos[i] != null; i++)
   g.fillOval(puntos[i].x, puntos[i].y, 2, 2);
 }

 public static void main(String args[]) {
  Painter aplicacion = new Painter();
  aplicacion.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 }

}


Espero que os sea útil :)

lunes, 11 de abril de 2011

Botones bordes redondos y con forma, personalizar los JButton

2 comentarios
Decíamos que una de las características de Swing es su facilidad para personalizar nuestros componentes mediante la modificación de su ComponentUI. Hoy vamos a ver como darle un aspecto personalizado a nuestros botones.

Aquí os dejaré unas líneas generales de como hacerlo, pero el límite lo ponéis vosotros y vuestra imaginación. Crearemos una subclase de BasicButtonUI y sobrescribiremos su método paint() para darle un aspecto determinado a nuestros botones y así conseguir este aspecto:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.RoundRectangle2D;

import javax.swing.AbstractButton;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.plaf.basic.BasicButtonUI;

public class ShapedButton {

 private static JFrame frame;

 public Component createComponents() {

  // Tres botones para probar diferentes efectos
  JButton roundButton = new JButton("Redondo");
  JButton squareButton = new JButton("Cuadrado");
  JButton polygonButton = new JButton("Poligonal");

  // Establecemos un UI específico
  ShapedButtonUI roundUI = new ShapedButtonUI();
  roundUI.setShape(ButtonShape.ROUND, roundButton);
  roundButton.setUI(roundUI);
  roundButton.setPreferredSize(new Dimension(100, 100));
  ShapedButtonUI squareUI = new ShapedButtonUI();
  squareUI.setShape(ButtonShape.SQUARE, squareButton);
  squareButton.setUI(squareUI);
  squareButton.setPreferredSize(new Dimension(100, 100));
  ShapedButtonUI polygonUI = new ShapedButtonUI();
  polygonUI.setShape(ButtonShape.POLYGON, polygonButton);
  polygonButton.setUI(polygonUI);
  polygonButton.setPreferredSize(new Dimension(100, 100));

  // Añadimos nuestros componentes
  JPanel panel = new JPanel();
  panel.add(roundButton);
  panel.add(squareButton);
  panel.add(polygonButton);

  return panel;
 }

 /**
  * Definimos tres posibles formas para los botones
  */
 public enum ButtonShape {
  ROUND,
  SQUARE,
  POLYGON
 }

 private class ShapedButtonUI extends BasicButtonUI {

  /** Button shape. */
  private ButtonShape shape;

  public ShapedButtonUI() {
   super();
  }

  public void setShape(ButtonShape shape, JButton button){
   // no pintamos el borde
   button.setBorderPainted(false);
   this.shape = shape;
  }

  @Override
  public void paint(Graphics g, JComponent c) {

   Graphics2D g2d = (Graphics2D) g;

   // definamos las formas de nuestros botones
   Shape buttonShape = null;
   switch (shape) {
    case ROUND:
     buttonShape = new RoundRectangle2D.Double(0, 0, c.getWidth() - 1, c.getHeight() - 1, 50, 50);
     break;
    case SQUARE:
     buttonShape = new Rectangle(0, 0, c.getWidth(), c.getHeight());
     break;
    case POLYGON:
     int[] xPoints = {0, 0 + c.getWidth() / 3, 0 + 2 * (c.getWidth() / 3), c.getWidth(), 0 + 2 * (c.getWidth() / 3), 0 + c.getWidth() / 3};
     int[] yPoints = {c.getHeight() / 2, 0, 0, c.getHeight() / 2, c.getHeight(), c.getHeight()};
     buttonShape = new Polygon(xPoints, yPoints, 6);
     break;
   }

   // establecemos un gradiente para el fondo del boton
   GradientPaint gp = new GradientPaint(0, 0, Color.GREEN, c.getWidth(), c.getHeight(), Color.BLUE);
   g2d.setPaint(gp);
   g2d.fill(buttonShape);

   super.paint(g2d, c);

  }

  @Override
  protected void paintButtonPressed(Graphics g, AbstractButton b) {
   Graphics2D g2d = (Graphics2D) g;

   // definamos las formas de nuestros botones
   Shape buttonShape = null;
   switch (shape) {
    case ROUND:
     buttonShape = new RoundRectangle2D.Double(0, 0, b.getWidth() - 1, b.getHeight() - 1, 50, 50);
     break;
    case SQUARE:
     buttonShape = new Rectangle(0, 0, b.getWidth(), b.getHeight());
     break;
    case POLYGON:
     int[] xPoints = {0, 0 + b.getWidth() / 3, 0 + 2 * (b.getWidth() / 3), b.getWidth(), 0 + 2 * (b.getWidth() / 3), 0 + b.getWidth() / 3};
     int[] yPoints = {b.getHeight() / 2, 0, 0, b.getHeight() / 2, b.getHeight(), b.getHeight()};
     buttonShape = new Polygon(xPoints, yPoints, 6);
     break;
   }

   // establecemos un gradiente para el fondo del boton
   GradientPaint gp = new GradientPaint(0, 0, Color.BLUE, 0, b.getHeight(), Color.GREEN);
   g2d.setPaint(gp);
   g2d.fill(buttonShape);
  }
 }

 /**
  * Create the GUI and show it. For thread safety,
  * this method should be invoked from the
  * event dispatch thread.
  */
 private static void createAndShowGUI() {

  // Create and set up the window.
  frame = new JFrame("Shaped button");
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

  ShapedButton app = new ShapedButton();
  Component contents = app.createComponents();
  frame.getContentPane().add(contents, BorderLayout.CENTER);

  // Display the window.
  frame.pack();
  frame.setLocationByPlatform(true);
  frame.setVisible(true);
 }

 public static void main(String[] args) {
  // Schedule a job for the event dispatch thread:
  // creating and showing this application's GUI.
  javax.swing.SwingUtilities.invokeLater(new Runnable() {
   public void run() {
    createAndShowGUI();
   }
  });
 }

viernes, 8 de abril de 2011

El Look and Feel (L&F) de Swing, cambiar el look and feel de la aplicación

3 comentarios
Tal y como está diseñado Swing, puedes cambiar el "look and feel" (L&F) de nuestras interfaces. "Look" se refiere a la apariencia de los componentes, "feel" se refiere a la manera en que se comportan estos.


Cada componente Swing se divide en dos clases distintas, una subclase de JComponent y otra subclase de ComponentUI. Por ejemplo un JButton tiene una implementación concreta de ButtonUI.


Veamos ahora varios ejemplos de como cambiar el look and feel de nuestra aplicación:


Programáticamente

 Para especificarlo haremos uso del método UIManager.setLookAndFeel() con el nombre completo de una subclase de LookAndFeel y los argumentos apropiados. P ej.
public static void main(String[] args) {
try {
// Establecemos el look and feel "Metal"
UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
} 
catch (UnsupportedLookAndFeelException e) {
// manejar excepción
}
catch (ClassNotFoundException e) {
// manejar excepción
}
catch (InstantiationException e) {
// manejar excepción
}
catch (IllegalAccessException e) {
// manejar excepción
}

new SwingApplication(); //Creamos y mostramos la interfaz de usuario
}

Por línea de comandos

Podemos especificar el L&F por línea de comandos usando el flag -D para establecer la propiedad swing.defaultlaf: P ej.

java -Dswing.defaultlaf=com.sun.java.swing.plaf.windows.WindowsLookAndFeel MyApp

Archivo swing.properties

Otra manera es cambiar el swing.properties, es posible que tengamos que crear este archivo. Suele estar ubicado en la carpeta lib de nuestro intérprete de Java.

# Swing properties
swing.defaultlaf=com.sun.java.swing.plaf.windows.WindowsLookAndFeel

Despues del inicio de la aplicación

Se puede cambiar el aspecto de la aplicación incluso despues de haber hecho visible la ventana. Para ahcer efectivo el cambio, deberemos llamar al método updateComponentTreeUI de SwingUtilities por cada contenedor de alto nivel. P ej.

UIManager.setLookAndFeel(lnfName);
SwingUtilities.updateComponentTreeUI(frame);
frame.pack();

Para hacer esto algo más gráfico aquí os dejo el código de una aplicación de ejemplo para cambiar el Look and Feel tras iniciar la aplicación.

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.BorderFactory;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class LookAndFeelDemo {

 static JFrame frame;
 JLabel label;
 JComboBox combo;

 public Component createComponents() {

  label = new JLabel("Selecciona un L&F");
  combo = new JComboBox();

  // Añado elementos al combo
  combo.addItem("Metal");
  combo.addItem("Nimbus");
  combo.addItem("Windows");
  combo.addItem("Motif");

  combo.addActionListener(new ActionListener() {

   @Override
   public void actionPerformed(ActionEvent e) {
    initLookAndFeel(combo.getSelectedItem().toString());
   }
  });

  label.setLabelFor(combo);

  JPanel pane = new JPanel(new GridLayout(0, 1));
  pane.add(label);
  pane.add(combo);
  pane.setBorder(BorderFactory.createEmptyBorder(30, // top
  30, // left
  10, // bottom
  30) // right
  );

  return pane;
 }

 private static void initLookAndFeel(String LaF) {
  String lookAndFeel = null;

  if (LaF != null) {
   if (LaF.equals("Metal")) {
    lookAndFeel = "javax.swing.plaf.metal.MetalLookAndFeel";
   } else if (LaF.equals("Nimbus")) {
    lookAndFeel = "com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel";
   } else if (LaF.equals("Windows")) {
    lookAndFeel = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel";
   } else if (LaF.equals("Motif")) {
    lookAndFeel = "com.sun.java.swing.plaf.motif.MotifLookAndFeel";
   } else {
    lookAndFeel = UIManager.getCrossPlatformLookAndFeelClassName();
   }

   try {
    UIManager.setLookAndFeel(lookAndFeel);
    SwingUtilities.updateComponentTreeUI(frame);
    frame.pack();
   } catch (ClassNotFoundException e) {
    e.printStackTrace();
   } catch (UnsupportedLookAndFeelException e) {
    e.printStackTrace();
   } catch (Exception e) {
    e.printStackTrace();
   }
  }
 }

 /**
  * Create the GUI and show it. For thread safety,
  * this method should be invoked from the
  * event dispatch thread.
  */
 private static void createAndShowGUI() {
  // Make sure we have nice window decorations.
  JFrame.setDefaultLookAndFeelDecorated(true);

  // Create and set up the window.
  frame = new JFrame("Look And Feel Demo");
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

  LookAndFeelDemo app = new LookAndFeelDemo();
  Component contents = app.createComponents();
  frame.getContentPane().add(contents, BorderLayout.CENTER);

  // Display the window.
  frame.pack();
  frame.setLocationByPlatform(true);
  frame.setVisible(true);
 }

 public static void main(String[] args) {
  // Schedule a job for the event dispatch thread:
  // creating and showing this application's GUI.
  javax.swing.SwingUtilities.invokeLater(new Runnable() {
   public void run() {
    createAndShowGUI();
   }
  });
 }
}

lunes, 4 de abril de 2011

¿Qué es Java Swing?

4 comentarios
Empecemos por el principio, ¿que es java swing?
Pues bien, el paquete javax.swing nos proporciona una serie de componentes "ligeros" (todo en lenguaje Java) que, al máximo grado posible funcionan igual en todas las plataformas.
Swing nos permite dotar de una interfaz gráfica de usuario a nuestras aplicaciones, dotándolas de interactividad y riqueza visual.


Veamos nuestro primer ejemplo de esta tecnología al alcance de todos. Para empezar os dejo con el típico ejemplo del "Hola Mundo" hecho con Swing. Copiar, pegar y ejecutar para ver el resultado...





import javax.swing.JFrame;
import javax.swing.JLabel;

public class HolaMundoSwing extends JFrame {

 private static void createAndShowGUI() {
  // Crea y prepara la ventana.
  JFrame frame = new JFrame(&quot;HolaMundoSwing&quot;);
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

  // A&Atilde;&plusmn;ade la etiqueta &quot;Hola Mundo&quot;.
  JLabel label = new JLabel(&quot;Hola Mundo en Swing&quot;);
  frame.getContentPane().add(label);

  // Mostramos la ventana.
  frame.setSize(300, 100);
  frame.setVisible(true);
 }

 public static void main(String[] args) {
  // Programa un trabajo para el hilo de despacho de eventos:
  // creando y mostrando la ventana de la aplicaci&Atilde;&sup3;n.
  javax.swing.SwingUtilities.invokeLater(new Runnable() {
   public void run() {
    createAndShowGUI();
   }
  });

 }
}