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();
   }
  });
 }
}