Un programa de dibujo simple

prev.gif (997 bytes)chapter.gif (1105 bytes)home.gif (1232 bytes)next.gif (998 bytes)

Ejemplos completos

Diseño del applet

Manejo del ratón

Guardar las figuras en memoria

La clase que describe las figuras

La jerarquía de clases

Conclusiones

El código fuente


Vamos a estudiar un programa de dibujo muy sencillo que consta de una barra de herramientas constituída por tres botones de radio que corresponden a una línea, una elipse, un rectángulo, y un botón que borra el área de trabajo del canvas. El propósito del programa es el de simular un programa de dibujo con una caja de herramientas.

Otra de las particularidades del programa, es la creación de clases para realizar las distintas tareas. La clase que describe el applet, en la que se disponen los controles, la clase que describe el canvas que nos permite dibujar figuras sobre su área de trabajo, y la clase que describe las distintas figuras.

Podemos finalmente, optar por una clase que describe todas las figuras o bien crear una jerarquía de clases, que desciendan de una clase base abstracta, y definir en las clases derivadas la función que la dibuja en un contexto gráfico.

Diseño del applet

dibujo1.gif (2338 bytes)

El applet consta de paneles anidados en la parte superior en el que se sitúan los botones, y un control canvas en la parte inferior, y que ocupa la mayor parte del applet.

   public void init() {        
	//...
	this.add(panel1, BorderLayout.NORTH);
        this.add(canvas, BorderLayout.CENTER);
} 

Hacemos doble-clic sobre el botón titulado Borrar en modo diseño, JBuilder genera el código que asocia dicho botón con un objeto de una clase anónima que implementa el interface ActionListener. El programador solamente se tiene que preocupar de añadir el código a la función respuesta btnBorrar_actionPerformed cuyo nombre ha generado el Entorno Integrado. 

    void btnBorrar_actionPerformed(ActionEvent ev){
        canvas.borrar();
    }

 

Manejo del ratón

Para crear un control canvas definimos una clase derivada de Canvas. Dicha clase la llamaremos MiCanvas, que redefinirá la función paint, y definirá otras funciones miembro.

La clase que describe el canvas ha de responder a las acciones de pulsar el botón izquierdo del ratón, cuando se comienza a dibujar la figura en una determinada posición; de arrastrar el ratón, cuando se proporciona un tamaño a la figura; y de liberar el botón izquierdo del ratón, cuando terminamos de dibujar la figura.

Hemos estudiado las distintas aproximaciones, para responder a estas acciones. Una de ellas, es que la clase que describe el canvas implemente los interfaces MouseListener y MouseMotionListener. Otra opción, es la de usar el código generado por JBuider cuando en el modo diseño se selecciona el canvas, y en el correspondiente panel Events se hace doble-clic sobre los editores asociados a mousePressed, mouseReleased y mouseDragged.

Vamos a seguir una tercera opción, que es la de crear dos clases internas denominas RatonPoner y RatonMover que derivan de MouseAdapter y de MouseMotionAdapter, y que redefinen las funciones mousePressed, mouseReleased y mouseDragged, respectivamente.

 class RatonPoner extends MouseAdapter{
    public void mousePressed(MouseEvent ev){
        empiezaDibujar(ev.getPoint());
    }
    public void mouseReleased(MouseEvent ev){
        terminaDibujar();
        figuras.addElement(figActual);
    }
 }

 class RatonMover extends MouseMotionAdapter{
    public void mouseDragged(MouseEvent ev){
        dibuja(ev.getPoint());
    }
 }

En el constructor de MiCanvas se asocia el productor de los sucesos asociados al ratón, el canvas (this) con objetos de las clases RatonPoner y RatonMover.

    public MiCanvas(DrawApplet parent) {
        addMouseListener(new RatonPoner());
        addMouseMotionListener(new RatonMover());
    }

Para dibujar una figura se requieren tres acciones

Cuando se pulsa el botón izquierdo del ratón, se llama a la función mousePressed, y esta llama a la función empiezaDibujar miembro de MiCanvas. En esta función se obtiene el contexto gráfico del componente gc mediante getGraphics y se establece en dicho contexto el modo de dibujo XOR mediante setXORMode. Finalmente, se dibuja el punto inicial llamando a la función dibujar miembro de la clase Figura, para dibujar la figura actualmente seleccionada.

 private void getTipo(){
	String s=parent.chkGrupo.getSelectedCheckbox().getLabel();
	if(s.equals("Línea")){
		figActual=new Figura(Figura.LINEA);
	}else if(s.equals("Rectángulo")){
		figActual=new Figura(Figura.RECTANGULO);
	}else if(s.equals("Elipse")){
		figActual=new Figura(Figura.ELIPSE);
	}
}

private void empiezaDibujar(Point p){
	getTipo();
	ini=p;
	fin=p;
	gc=getGraphics();
	gc.setXORMode(getBackground());
	figActual.dibujar(gc, ini, fin);
}    

Cuando se arrastra el ratón permaneciendo el botón izquierdo pulsado se llama a mouseDragged, y esta llama a la función miembro dibuja de MiCanvas, para dibujar la figura con unas dimensiones determinadas. En el modo XOR, la primera sentencia borra la figura dibujada previamente y a continuación dibuja la nueva figura con las dimensiones proporcionadas por el parámetro p (punto final).

Los miembros dato ini y fin (objetos de la clase Point), guardan los puntos inicial (esquina superior izquierda) y final (esquina inferior derecha) de la figura que se ha dibujado previamente. El valor de ini no cambia y se establece al pulsar el botón izquierdo del ratón, mientras que el valor fin, se actualiza con el valor que nos suministra el objeto ev de la clase MouseEvent. Cuando se mueve el ratón, se llama a la función respuesta mouseDragged y ésta llama a dibuja.

    void dibuja(Point p){
        figActual.dibujar(gc, ini, fin);	//borra la figura previa
        fin=p;
        figActual.dibujar(gc, ini, fin);	//dibuja la figura actual
    }

Cuando se libera el botón izquierdo del ratón, se llama a la función mouseReleased, y ésta llama a la función miembro terminaDibujar de MiCanvas. Dicha función, borra la última figura dibujada , establece el modo de dibujo por defecto con setPaintMode, y dibuja la figura final en este modo. Finalmente, nos acordamos de liberar los recursos asociados al contexto gráfico gc, mediante la llamada a dispose.

    void terminaDibujar(){
        figActual.dibujar(gc, ini, fin);
        gc.setPaintMode();
        figActual.dibujar(gc, ini, fin);
        gc.dispose();
    }

 

Guardar las figuras en memoria

Para guardar un número indeterminado de figuras empleamos la clase Vector. Creamos un objeto figuras de dicha clase llamando a uno de sus tres constructores. Cuando la figura se termina de dibujar, al soltar el botón izquierdo del ratón, se añade a la colección mediante addElement.

    public void mouseReleased(MouseEvent ev){
        terminaDibujar();
        figuras.addElement(figActual);
    }
 }

Para dibujar todas las figuras guardadas en el vector figuras, se redefine paint.

    public void paint(Graphics g){
        for(int i=0; i<figuras.size(); i++){
            Figura fig=(Figura)figuras.elementAt(i);
            fig.dibujar(g);
        }
    }

Para acceder a todos los elementos de la colección se puede emplear un bucle while o bien, un bucle for, si obtenemos antes el número de elementos que se han guardado en figuras mediante la función miembro size de la clase Vector.

Una vez obtenida un objeto de la clase Figura mediante elementAt, se dibuja la figura en el contexto gráfico g, llamando a la función miembro dibujar de la clase Figura.

Cuando se pulsa al botón titulado Borrar se llama a la función borrar, se eliminan todos las figuras guardadas en el vector figuras mediante removeAllElements. Se vuelve a dibujar el canvas llamando a la función miembro paint, que no dibuja nada.

    public void borrar(){
        figuras.removeAllElements();
        repaint();
    }

El código completo de la clase MiCanvas que describe el canvas es, el siguiente

public class MiCanvas extends Canvas {
	private Draw_Applet parent;
	private Vector figuras=new Vector();
	private Figura figActual;
	private Graphics gc;
	private Point ini, fin;
	public MiCanvas(Draw_Applet parent) {
		setBackground(Color.white);
		addMouseListener(new RatonPoner());
		addMouseMotionListener(new RatonMover());
		figActual=new Figura(Figura.LINEA);
		this.parent=parent;
	}
	public void borrar(){
		figuras.removeAllElements();
		repaint();
	}
	private void getTipo(){
		String s=parent.chkGrupo.getSelectedCheckbox().getLabel();
		if(s.equals("Línea")){
			figActual=new Figura(Figura.LINEA);
		}else if(s.equals("Rectángulo")){
			figActual=new Figura(Figura.RECTANGULO);
		}else if(s.equals("Elipse")){
			figActual=new Figura(Figura.ELIPSE);
		}
	}
	private void empiezaDibujar(Point p){
		getTipo();
		ini=p;
		fin=p;
		gc=getGraphics();
		gc.setXORMode(getBackground());
		figActual.dibujar(gc, ini, fin);
	}
	private void dibuja(Point p){
		figActual.dibujar(gc, ini, fin);
		fin=p;
		figActual.dibujar(gc, ini, fin);
	}
	private void terminaDibujar(){
		figActual.dibujar(gc, ini, fin);
		gc.setPaintMode();
		figActual.dibujar(gc, ini, fin);
		gc.dispose();
	}
	public void paint(Graphics g){
		for(int i=0; i<figuras.size(); i++){
			Figura fig=(Figura)figuras.elementAt(i);
			fig.dibujar(g);
		}
	}
	class RatonPoner extends MouseAdapter{
		public void mousePressed(MouseEvent ev){
			empiezaDibujar(ev.getPoint());
		}
		public void mouseReleased(MouseEvent ev){
			terminaDibujar();
			figuras.addElement(figActual);
		}
	}
	class RatonMover extends MouseMotionAdapter{
		public void mouseDragged(MouseEvent ev){
			dibuja(ev.getPoint());
		}
	}
}

 

La clase que describe las figuras

La clase Figura tiene tres miembros datos constantes que indican el tipo de figura. Se accede a dichos miembros estáticos poniendo el nombre de la clase Figura, seguido del nombre del miembro estático, separados por un punto

	canvas.setFigura(Figura.LINEA);

La clase Figura tiene tres constructores, el constructor por defecto, y dos constructores explícitos. También tiene dos funciones miembro con el mismo nombre dibujar. La primera versión, se llama mientras se está dibujando la figura, y la segunda versión se llama  cuando se dibujan todas las figuras en la redefinición de paint.

La primera versión de dibujar, actualiza los valores de los miembros dato ini y fin, que guardan los puntos extremo superior izquierdo de la figura, y extremo inferior derecho de la misma,  después llama a la función correspondiente de la clase Graphics para dibujar el tipo de figura que se guarda en el miembro dato tipo. Para dibujar una línea se llama a drawLine, para dibujar un rectángulo a drawRect, y para dibujar una elipse a drawOval.

Se ha de tener cuidado al dibujar un rectángulo o una elipse, ya que los dos primeros parámetros indican las coordenadas de la esquina superior izquierda y los dos últimos la anchura y la altura, ambas cantidades positivas. Pero con el ratón podemos dibujar en la dirección que queramos, de modo que la anchura o altura podrían ser cantidades negativas, y la esquina superior izquierda podría ser la inferior derecha. Tenemos que escribir el código que trate esta situación empleando las funciones estáticas min y abs miembros de la clase Math.

La segunda versión de de dibujar, se limita a llamar a la primera versión, pasándole los valores que guardan los miembros dato ini y fin, cuando se ha terminado de dibujar la figura.

El código completo de la clase Figura es, el siguiente

public class Figura {
    public static final int LINEA=0;
    public static final int RECTANGULO=1;
    public static final int ELIPSE=2;
    private int tipo;
    private Point ini, fin;
//figura final
    public Figura(int tipo, Point ini, Point fin) {
        this.tipo=tipo;
        this.ini=ini;
        this.fin=fin;
    }
    public Figura(){
        this(LINEA, new Point(), new Point());
    }
    public Figura(int tipo){
        this(tipo, new Point(), new Point());
    }
//mientras se dibuja la figura
    public void dibujar(Graphics g, Point ini, Point fin){
        this.ini=ini;
        this.fin=fin;
        if(tipo==LINEA){
            g.drawLine(ini.x, ini.y, fin.x, fin.y);
        }else{
            int ancho=Math.abs(ini.x-fin.x);
            int alto=Math.abs(ini.x-fin.x);
            int iniX=Math.min(ini.x, fin.x);
            int iniY=Math.min(ini.y, fin.y);
            if(tipo==ELIPSE){
                g.drawOval(iniX, iniY, ancho, alto);
            }else{
                g.drawRect(iniX, iniY, ancho, alto);
            }
        }
    }
//figura final
    public void dibujar(Graphics g){
        dibujar(g, ini, fin);
    }
}

 

La jerarquía de clases

Ya hemos estudiado la jeraquía de clases de las figuras planas, y se ha definido la función area que calcula el área de cada uno de los tipos de figuras. Vamos a hacer algo parecido, pero definiendo en las clases derivadas la función dibujar que dibuja una figura concreta en un contexto gráfico.

La clase base Figura tiene como miembros dato los puntos ini (esquina superior izquierda) y fin (esquina inferior derecha) de la figura, y como funciones miembro, actualizar, que actualiza los miembros dato ini y fin, a medida que se va dibujando la figura con el ratón.

    protected void actualizar(Point ini, Point fin){
        this.ini=ini;
        this.fin=fin;
    }

La función miembro dibujar, dibuja la figura final que se ha guardado en el vector figuras, y que se llama en paint cuando se dibujan todas las figuras en el contexto gráfico g.

    public void dibujar(Graphics g){
        dibujar(g, ini, fin);
    }

Se declara abstracta la primera versión de dibujar, que se define en cada una de las clases derivadas, esta función se llama mientras se está dibujando la figura.

    public abstract void dibujar(Graphics g, Point ini, Point fin);

La definición de la clase base Figura y de las clases derivadas Linea, Elipse y Rectangulo, es el siguiente

public abstract class Figura {
    protected Point ini, fin;
    public Figura(Point ini, Point fin) {
        this.ini=ini;
        this.fin=fin;
    }
    public Figura(){
        this(new Point(), new Point());
    }
    protected void actualizar(Point ini, Point fin){
        this.ini=ini;
        this.fin=fin;
    }

//mientras se dibuja la figura
    public abstract void dibujar(Graphics g, Point ini, Point fin);

 //figura final
    public void dibujar(Graphics g){
        dibujar(g, ini, fin);
    }
}

class Linea extends Figura{
    public Linea(Point ini, Point fin) {
        super(ini, fin);
   }
    public Linea(){
        this(new Point(), new Point());
    }
    public void dibujar(Graphics g, Point ini, Point fin){
        actualizar(ini, fin);
        g.drawLine(ini.x, ini.y, fin.x, fin.y);
    }
}

class Rectangulo extends Figura{
    public Rectangulo(Point ini, Point fin) {
        super(ini, fin);
   }
    public Rectangulo(){
        this(new Point(), new Point());
    }
    public void dibujar(Graphics g, Point ini, Point fin){
        actualizar(ini, fin);
        int ancho=Math.abs(ini.x-fin.x);
        int alto=Math.abs(ini.x-fin.x);
        int iniX=Math.min(ini.x, fin.x);
        int iniY=Math.min(ini.y, fin.y);
        g.drawRect(iniX, iniY, ancho, alto);
    }
}

class Elipse extends Figura{
    public Elipse(Point ini, Point fin) {
        super(ini, fin);
   }
    public Elipse(){
        this(new Point(), new Point());
    }
    public void dibujar(Graphics g, Point ini, Point fin){
        actualizar(ini, fin);
        int ancho=Math.abs(ini.x-fin.x);
        int alto=Math.abs(ini.x-fin.x);
        int iniX=Math.min(ini.x, fin.x);
        int iniY=Math.min(ini.y, fin.y);
        g.drawOval(iniX, iniY, ancho, alto);
    }
}

La clase MiCanvas, no sufre apenas variación. Se crea un objeto de la clase derivada dependiendo del tipo de figura seleccionada al pulsar uno u otro botón. El objeto de la clase derivada se guarda en una variable figActual de la clase base Figura. Cuando se llama desde figActual a la función dibujar, ¿a cuál de las tres funciones dibujar se llama: la que dibuja una línea, la que dibuja un rectángulo, o la que dibuja una elipse?. Esto es lo que estudiamos en el capítulo dedicado a la herencia y el polimorfismo.

public class MiCanvas extends Canvas {
	private Figura figActual;
public MiCanvas(Draw_Applet1 parent) {
	figActual=new Linea();
}
private void getTipo(){
	String s=parent.chkGrupo.getSelectedCheckbox().getLabel();
	if(s.equals("Línea")){
		figActual=new Linea();
	}else if(s.equals("Rectángulo")){
		figActual=new Rectangulo();
	}else if(s.equals("Elipse")){
		figActual=new Elipse();
	}
}
void empiezaDibujar(Point p){
	getTipo();
	ini=p;
	fin=p;
	gc=getGraphics();
	gc.setXORMode(getBackground());
	figActual.dibujar(gc, ini, fin);
}
//otras funciones miembro
}

 

Conclusiones

En este ejemplo hemos visto los siguientes temas:

  1. Respuesta a las acciones del usuario sobre un grupo de botones de radio, y un botón.
  2. Respuesta a los sucesos asociados al ratón
  3. Creación de clases para distintos propósitos:
  1. En este último caso, podemos crear una jerarquía de clases y aplicar de forma elegante las características de la herencia y del polimorfismo del lenguaje Java.
  2. El modo de dibujo XOR
  3. Guardar objetos en un vector, u objeto de la clase Vector.

 

El código fuente

Las dos versiones del programa

disco.gif (1035 bytes)draw: Figura.java, MiCanvas.java, Draw_Applet.java

disco.gif (1035 bytes)draw1: Figura.java, MiCanvas.java, Draw_Applet1.java