En esta última sesión vamos a hacer una aplicación que reúna varios de los conceptos importantes vistos hasta ahora. Debéis elegir una de las dos propuestas siguientes:
Ejercicio 1. Vamos a construir un Paint en versión simplificada. En dicho Paint deberemos poder dibujar tres tipos de figuras: círculos, rectángulos y líneas rectas. También deberemos poder guardar figuras en ficheros, y recuperarlas después desde la aplicación.
Una apariencia general de la aplicación podría ser esta:
Figura 1. Apariencia de la aplicación Paint
Para hacer este ejercicio deberéis tomar el ejercicio que hicisteis en la sesión 10, donde se os pedía que, en modo texto, indicarais qué figuras geométricas añadir o quitar de una lista.
La estructura de clases y paquetes que vamos a necesitar es la misma que la de aquella sesión:
La clase principal de aquella aplicación (la de la sesión 10) era AplicGeom. La borraremos, y sustituiremos por la versión en modo gráfico, la clase JAplicGeom que se os da en la plantilla, para que la completéis.
PREPARATIVOS PREVIOS
Antes de empezar, debéis realizar un cambio en la clase geom.Figura, y es añadirle un método abstracto más, llamado dibuja:
package geom; import java.awt.Graphics; public abstract class Figura implements java.io.Serializable { public abstract String imprime(); public abstract void dibuja(Graphics g); }
Este método indicará cómo se debe dibujar cada subtipo de figura. Por tanto, deberéis añadir este método también en las tres subclases (Circulo, Rectangulo y Linea), indicando cómo se dibujaría cada una:
package geom; import java.awt.Graphics; public class Circulo extends Figura implements java.io.Serializable { ... public void dibuja(Graphics g) { g.drawOval(x - x/2, y - y/2, radio, radio); } }
package geom; import java.awt.Graphics; public class Rectangulo extends Figura implements java.io.Serializable { ... public void dibuja(Graphics g) { g.drawRect(x1, y1, x2 - x1, y2 - y1); } }
package geom; import java.awt.Graphics; public class Rectangulo extends Figura implements java.io.Serializable { ... public void dibuja(Graphics g) { g.drawLine(x1, y1, x2, y2); } }
Con esto ya tenemos preparadas nuestras figuras para dibujarse. Nos faltará únicamente completar el programa principal (JAplicGeom) para indicar cómo dibujarlas, guardarlas y recuperarlas.
OBJETIVOS QUE DEBE CUMPLIR EL PROGRAMA
Se os da libertad para que cada uno implemente el programa como quiera, siempre que se cumplan los siguientes requisitos:
NOTAS ACERCA DE LA IMPLEMENTACIÓN
Vamos a ver ahora cómo hacer los apartados que más os puedan costar.
Cómo construir la ventana
Cómo gestionar las figuras
Cómo abrir y guardar archivos de figuras
Cómo responder a peticiones sobre menús y botones
Cómo responder a eventos sobre el área de dibujo
Cómo dibujar las figuras
Como veis en la figura 1, la ventana de la aplicación tiene un área de dibujo en la parte superior, y tres botones de radio en la inferior, que indican qué figura dibujar, además de los menús.
Para la parte superior, podemos crearnos una clase interna MiAreaDibujo, que sea un subtipo de JPanel, y que podamos colocar en la parte superior para dibujar. Dicha clase tendrá un método update y otro paint, donde pondremos el código necesario para dibujar las figuras:
import java.awt.*; import java.awt.event.*; import javax.swing.*; public class JAplicGeom extends JFrame { class MiAreaDibujo extends JPanel { public MiAreaDibujo() { } public void update(Graphics g) { paint(g); } public void paint(Graphics g) { // Borrar el área de dibujo g.clearRect(0, 0, getWidth(), getHeight()); // Dibujar las figuras (más adelante)... } } }
Luego añadiríamos un objeto de este tipo en la clase principal:
... public class JAplicGeom extends JFrame { MiAreaDibujo mad; public JAplicGeom() { ... mad = new MiAreaDibujo(); } ... }
Para la parte inferior, podemos crear tres JRadioButtons, y añadirlos a un JPanel.
... public class JAplicGeom extends JFrame { MiAreaDibujo mad; JRadioButton btnCirculo; JRadioButton btnRectangulo; JRadioButton btnLinea; public JAplicGeom() { ... mad = new MiAreaDibujo(); btnCirculo = new JRadioButton("Circulo", true); btnRectangulo = new JRadioButton("Rectangulo"); btnLinea = new JRadioButton("Linea"); JPanel panelBotones = new JPanel(); panelBotones.add(btnCirculo); panelBotones.add(btnRectangulo); panelBotones.add(btnLinea); } ... }
Finalmente, basta con colocar el área de dibujo arriba, y el panel de botones abajo:
... public class JAplicGeom extends JFrame { MiAreaDibujo mad; JRadioButton btnCirculo; JRadioButton btnRectangulo; JRadioButton btnLinea; public JAplicGeom() { ... mad = new MiAreaDibujo(); btnCirculo = new JRadioButton("Circulo", true); btnRectangulo = new JRadioButton("Rectangulo"); btnLinea = new JRadioButton("Linea"); JPanel panelBotones = new JPanel(); panelBotones.add(btnCirculo); panelBotones.add(btnRectangulo); panelBotones.add(btnLinea); getContentPane().add(mad, BorderLayout.CENTER); getContentPane().add(panelBotones, BorderLayout.SOUTH); } ... }
Para los menús, definimos un JMenuBar, un JMenu llamado "Archivo", y dentro tres items: "Abrir" (para abrir un fichero de figuras), "Guardar" (para guardar las figuras actuales en fichero) y "Salir" (para cerrar la aplicación):
... public class JAplicGeom extends JFrame { ... public JAplicGeom() { ... JMenuBar mb = new JMenuBar(); JMenu m = new JMenu("Archivo"); JMenuItem mAbrir = new JMenuItem("Abrir"); m.add(mAbrir); JMenuItem mGuardar = new JMenuItem("Guardar"); m.add(mGuardar); JMenuItem mSalir = new JMenuItem("Salir"); m.add(mSalir); mb.add(m); this.setJMenuBar(mb); } ... }
Definimos un "main" donde creamos la ventana, le damos tamaño, le ponemos el evento de cerrarse, y la mostramos:
... public class JAplicGeom extends JFrame { ... public static void main(String[] args) { JAplicGeom jag = new JAplicGeom(); jag.setSize(400, 300); jag.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); jag.show(); } }
Probad a compilar y ejecutar el programa ahora, para depurar erratas y ver que se os muestra la ventana como la de la figura 1 (deberéis importar los paquetes necesarios en el fichero JAplicGeom).
Para llevar un seguimiento de la lista de figuras, podemos definir una variable global que sea un ArrayList, donde almacenemos las figuras que vamos dibujando. Sería buena idea también definir una variable Figura que almacene la figura que se está dibujando actualmente (antes de añadirla a la lista):
... public class JAplicGeom extends JFrame { ArrayList figuras = new ArrayList(); Figura figActual = null; ... }
Cuando empecemos a dibujar una figura, ésta se almacenará temporalmente en figActual, y cuando terminemos de dibujarla, la añadiremos a la lista figuras, y volveremos a dejar vacía figActual. En otros apartados se explica cómo cargar una lista de fichero, o cómo ir actualizando la lista con nuevas figuras dibujadas.
c) Cómo abrir y guardar archivos de figuras
En primer lugar vamos a ver qué hacer cuando elijamos Archivo - Abrir o Archivo - Guardar. Para leer o guardar una lista de figuras, tenemos los métodos leeFiguras y guardaFiguras, que implementamos en la clase io.IOFiguras, en la sesión 10.
Para elegir el fichero del que leer, o donde guardar, utilizaremos la clase JFileChooser de Swing.
De esta forma, si quiero leer figuras de un fichero, y guardarlas en el campo figuras definido antes, haré algo como:
JFileChooser jfc = new JFileChooser("."); int result = jfc.showOpenDialog(this); if (result == JFileChooser.APPROVE_OPTION) { String fichero = jfc.getSelectedFile().getAbsolutePath(); Figura[] figs = IOFiguras.leeFiguras(fichero); figuras = new ArrayList(); if (figs != null) for (int i = 0; i < figs.length; i++) figuras.add(figs[i]); repaint(); }
donde primero muestro el diálogo con showOpenDialog (this sería la ventana actual, que actúa como ventana padre del diálogo), recojo la respuesta, y si es APPROVE_OPTION quiere decir que he elegido un fichero. En ese caso cojo la ruta y el nombre del fichero (getAbsolutePath), y llamo a IOFiguras.leeFiguras para obtener las figuras. Luego las cargo en el campo figuras, y redibujo la ventana.
Por otro lado, si quiero guardar las figuras en un fichero, haré algo como:
JFileChooser jfc = new JFileChooser("."); int result = jfc.showSaveDialog(this); if (result == JFileChooser.APPROVE_OPTION) { String fichero = jfc.getSelectedFile().getAbsolutePath(); Figura[] figs = (Figura[])(figuras.toArray(new Figura[0])); IOFiguras.guardaFiguras(figs, fichero); }
Es muy parecido a lo anterior: muestro el diálogo (ahora con showSaveDialog), recojo el fichero elegido, y llamo a IOFiguras.guardaFiguras para guardar la lista de figuras en el fichero.
NOTA: estos dos bloques tendréis que colocarlos donde toque, es decir, el primero lo pondréis donde se dé la orden de abrir un fichero, y el segundo donde se dé la orden de guardar.
d) Cómo responder a peticiones sobre menús y botones
Distinguimos dos tipos de eventos en el programa: los que se producen al elegir opciones de menú, o pulsar botones de radio, y los que se producirán cuando dibujemos figuras en el área de dibujo. Aquí trataremos los primeros.
Notemos que necesitamos distinguir 6 tipos de opciones diferentes, entre menús y botones de radio:
Podríamos definir un ActionListener para cada uno de estos elementos. Sin embargo, nos es más fácil hacer que la clase principal implemente ActionListener, y juntar todos los eventos en un solo método:
... public class JAplicGeom extends JFrame implements ActionListener { ... public JAplicGeom() { ... btnCirculo.addActionListener(this); btnRectangulo.addActionListener(this); btnLinea.addActionListener(this); mAbrir.addActionListener(this); mGuardar.addActionListener(this); mSalir.addActionListener(this); } public void actionPerformed(ActionEvent e) { } }
En el actionPerformed que hemos añadido, pondremos el código que controle TODAS esas acciones. Para distinguir qué acción se ha pedido, el parámetro ActionEvent tiene un método getActionCommand, que permite comparar y ver de qué opción se trata:
... public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("Abrir")) { //... Qué hacer al elegir "Archivo - Abrir" } else if (e.getActionCommand().equals("Guardar")) { //... Qué hacer al elegir "Archivo - Guardar" } else if (e.getActionCommand().equals("Salir")) { //... Qué hacer al elegir "Archivo - Salir" } else if (e.getActionCommand().equals("Circulo")) { // Marcar como seleccionado el Círculo, y quitar los otros btnCirculo.setSelected(true); btnRectangulo.setSelected(false); btnLinea.setSelected(false); } else if (e.getActionCommand().equals("Rectangulo")) { // Marcar como seleccionado el Rectangulo, y quitar los otros } else if (e.getActionCommand().equals("Linea")) { // Marcar como seleccionada la Linea, y quitar los otros } } ...
El contenido de cada "if" o "else" deberéis rellenarlo vosotros.
e) Cómo responder a eventos sobre el área de dibujo
Cuando pinchemos con el ratón sobre el área de dibujo, empezará a dibujarse la figura que tengamos seleccionada de las tres de abajo. Cuando arrastremos el ratón con el botón pulsado, se redibujará la figura, con las nuevas dimensiones que le estemos dando. Finalmente, cuando soltemos el botón del ratón, se terminará de dibujar la figura.
Para controlar todo esto, debemos definir dos tipos de eventos de ratón sobre la clase MiAreaDibujo: uno de tipo MouseListener (para cuando pulsemos el botón y lo soltemos), y otro de tipo MouseMotionListener (para cuando arrastremos el ratón). También sería interesante definir 4 campos, xIni, yIni, xFin, yFin que marcarán los límites superior izquierdo e inferior derecho de la figura que dibujamos:
... public class JAplicGeom extends JFrame implements ActionListener { ... class MiAreaDibujo extends JPanel { int xIni, yIni, xFin, yFin; public MiAreaDibujo() { this.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { // Qué hacer al pulsar el boton } public void mouseReleased(MouseEvent e) { // Qué hacer al soltar el boton } }); this.addMouseMotionListener(new MouseMotionAdapter() { public void mouseDragged(MouseEvent e) { // Qué hacer al arrastrar el ratón } }); } } }
Al pulsar el botón del ratón, crearemos una figura del tipo que tengamos seleccionado:
public void mousePressed(MouseEvent e) { xIni = xFin = e.getX(); yIni = yFin = e.getY(); if (btnCirculo.isSelected()) { figActual = new Circulo(xIni, yIni, 0); } else if (btnRectangulo.isSelected()) { figActual = new Rectangulo(xIni, yIni, xFin, yFin); } else if (btnLinea.isSelected()) { figActual = new Linea(xIni, yIni, xFin, yFin); } }
Observa que el parámetro MouseEvent tiene métodos getX() y getY() que indican qué coordenadas estamos pinchando.
Al arrastrar el ratón, iremos modificando los parámetros de la figura que habíamos creado, modificando sus coordenadas, anchura, o lo que toque:
public void mouseDragged(MouseEvent e) { xFin = e.getX(); yFin = e.getY(); if (btnCirculo.isSelected()) { ((Circulo)figActual).setX((xIni + xFin) / 2); ((Circulo)figActual).setY((yIni + yFin) / 2); ((Circulo)figActual).setRadio(Math.min(Math.abs(xIni - xFin), Math.abs(yIni - yFin))); } else if (btnRectangulo.isSelected()) { ((Rectangulo)figActual).setX1(Math.min(xIni, xFin)); ((Rectangulo)figActual).setY1(Math.min(yIni, yFin)); ((Rectangulo)figActual).setX2(Math.max(xIni, xFin)); ((Rectangulo)figActual).setY2(Math.max(yIni, yFin)); } else if (btnLinea.isSelected()) { ((Linea)figActual).setX2(xFin); ((Linea)figActual).setY2(yFin); } repaint(); }
Observad que para el círculo reajustamos el centro en la mitad de los límites de la figura, y ponemos como radio el valor mínimo entre la anchura y la altura. En el rectángulo ponemos como x1 e y1 los valores menores de coordenadas, y como x2 e y2 los mayores. Al terminar hacemos un repaint para que actualice el dibujo en pantalla.
Al soltar el botón del ratón, añadimos la figura a la lista, y ponemos a null la figura actual:
public void mouseReleased(MouseEvent e) { xFin = e.getX(); yFin = e.getY(); figuras.add(figActual); figActual = null; repaint(); }
Ya hemos visto cómo se va actualizando la lista de figuras a medida que vamos poniendo nuevas con el ratón. Ahora falta en el método paint decir cómo se dibujan. Basta con recorrer la lista de figuras, y llamar al método dibuja de cada una, pasándole los gráficos del panel. Finalmente, dibujamos la figura actual (figActual), si no es null, puesto que será la figura que estamos modificando actualmente:
public void paint(Graphics g) { g.clearRect(0, 0, getWidth(), getHeight()); for (int i = 0; i < figuras.size(); i++) { Figura f = (Figura)(figuras.get(i)); f.dibuja(g); } if (figActual != null) figActual.dibuja(g); }
ELEMENTOS OPTATIVOS
Podéis añadir (si queréis) los elementos optativos que queráis. Aquí os proponemos algunos:
Como CURIOSIDAD, comprobad qué le pasa al menú si la clase MiAreaDibujo hereda de Canvas en lugar de JPanel.
Ejercicio 2. En este ejercicio vamos a hacer un juego Java que consistirá en lo siguiente: tendremos un bosque de fondo, y en la parte inferior de la pantalla, al oso Yogi, que se podrá mover de izquierda a derecha de la pantalla. El juego consistirá en que, desde arriba caerán una serie de alimentos, que Yogi deberá coger. Por cada alimento que coja antes de que llegue al suelo, sumará un punto, y por cada uno que no pueda recoger, perderá una vida.
La apariencia general del juego sería parecida a la siguiente:
Figura 2. Apariencia del juego Java
En la plantilla, en la carpeta Yogi tenéis una parte del juego hecha, para seguir desde ahí. Veréis una clase principal Yogi.java, que tendrá la parte importante del juego. Si la compiláis y ejecutáis (deberéis elegir el modo gráfico), veréis que simplemente aparece Yogi, y lo podréis mover de izquierda a derecha. Explicaremos a continuación cómo está hecho todo eso.
Deberéis completar el juego para que:
CONSIDERACIONES SOBRE LA IMPLEMENTACION
Tenéis libertad para hacer el juego como queráis, siempre que se cumplan los puntos anteriores. Aquí vamos a contaros cómo está hecha la parte que se os da, y cómo poder hacer lo que queda.
Conviene que leáis el primer apartado de los siguientes antes que nada, y entendáis con él el código que se os da en la plantilla, para poderlo modificar.
Clases hechas en la plantilla
Cómo cargar el fondo
Cómo dibujar los alimentos
Cómo dibujar las vidas
Cómo dibujar los puntos
a) Clases hechas en la plantilla
En la plantilla se os dan 3 clases:
Figura 3. Animaciones de Yogi
Figura 4. Moverse por los sprites de Yogi
Así podremos elegir en cada paso de animación qué figura dibujar.
CONSTANTES Y CAMPOS:
MÉTODOS:
Figura 5.Cómo animar a Yogi
En el código (método dibuja), esto queda reflejado como:
// Dibujar a Yogi Shape clip = g.getClip(); g.setClip(xIni, ALTO - SpriteYogi.ALTO, SpriteYogi.ANCHO, SpriteYogi.ALTO); g.drawImage(sprn.sprites, xIni - sprn.x, ALTO - SpriteYogi.ALTO, sprn.width, sprn.height, this); g.setClip(clip);
El área de recorte la definimos en el xIni que marca la
posición X actual de Yogi. Marcamos un recorte de lo que mida cada
sprite de animación (SpriteYogi.ANCHO x SpriteYogi.ALTO), y
dibujamos la imagen de sprites entera, pero desplazada para que el
sprite que toque "caiga" dentro del área de recorte. Antes de recortar, nos guardamos el area de recorte previa (para no
machacarla), con un getClip, y después de dibujar en el área de
recorte la volvemos a establecer con un setClip, para poder
seguir dibujando en toda la ventana (si no lo restablecemos, cualquier
cosa que dibujemos fuera del área de recorte no se verá).
Cargar el fondo del juego (imagen bosque.jpg de la plantilla) es muy sencillo. Podéis definir un campo global de tipo Image para guardar la imagen:
... public class Yogi extends JFrame implements Runnable { Image fondo = null; ...
Luego en el constructor, ayudándonos de la clase Toolkit le cargamos la imagen JPG:
... public Yogi() { ... Toolkit tk = Toolkit.getDefaultToolkit(); fondo = tk.createImage("bosque.jpg"); }
Tenéis que tener en cuenta que, si la imagen es muy grande (ocupa muchos KB), puede que tarde en cargarla, y puede que se cargue la ventana sin tener la imagen ya cargada. Para asegurarnos de que no va a abrir la ventana hasta tenerla cargada, podemos utilizar la clase MediaTracker, y le pasamos la imagen, para que no siga hasta tenerla cargada:
... public Yogi() { ... Toolkit tk = Toolkit.getDefaultToolkit(); fondo = tk.createImage("bosque.jpg"); MediaTracker mt = new MediaTracker(this); mt.addImage(fondo, 1); try { mt.waitForAll(); } catch (Exception e) { e.printStackTrace(); } }
lo que hacemos es añadirle la imagen al MediaTracker, y luego hacer que espere hasta que todas las imágenes que tenga añadidas (en este caso, el fondo) estén listas.
Finalmente, en el método dibuja dibujamos la imagen ANTES de dibujar cualquier otra cosa (para dejarla detrás de todo). Podemos dibujarla justo después de limpiar el área de dibujo:
public void dibuja(Graphics g) { // Limpiar area g.clearRect(0, 0, ANCHO, ALTO); // Dibujar fondo g.drawImage(fondo, 0, 0, ANCHO, ALTO, this); ...
Tenéis una imagen llamada food.png en la plantilla, con tres tipos diferentes de alimentos. Cada uno de ellos tiene un ancho y un alto de 20 píxeles.
Figura 6. Tipos de alimentos disponibles
De lo que se trata es de que construyáis una clase SpriteComida, similar a SpriteYogi, donde defináis los tamaños de cada alimento, y de la imagen entera, y podáis elegir con un setFrame qué comida mostrar. De hecho, el código será muy parecido a SpriteYogi, pero cambiando los tamaños de cada sprite y de la imagen.
Luego, en la clase principal, creáis un objeto de este tipo, y le ponéis la comida que queráis de la imagen (podéis generar qué sprite poner aleatoriamente, por ejemplo). De forma que dicho sprite se vaya moviendo en cada iteración hacia abajo, hasta llegar al suelo. En ese momento se creará otro sprite diferente y se repetirá el proceso.
Para mover la comida, conviene que actualicéis la posición Y del sprite dentro del método run, y luego en dibuja sólo tengáis que ver qué Y tiene, y dibujarlo como se hace con el sprite de Yogi. Notad que en este caso no hay que cambiar de frame en cada paso de animación: se mantiene siempre la misma comida hasta que termina de caer, y luego se crea otro sprite, con la misma comida u otra diferente. Así el setFrame para la comida sólo se ejecutaría al crearla, y no se variaría durante su caída.
Para las vidas tenéis una imagen que se llama vida.png en la plantilla. Basta con que la asignéis a un campo Image en la clase principal, y lo dibujéis en la parte superior derecha, tantas veces como vidas se tengan.
Figura 7.Imagen para las vidas
Como véis en la figura 2, los puntos son una cadena de texto en la parte superior izquierda. Para dibujar texto en un objeto Graphics tenéis el método drawString. Simplemente hay que pasarle la cadena a dibujar, y las coordenadas (X,Y) inferiores izquierdas.
Para aseguraros de que todo el texto os va a caer dentro de la pantalla, y no se os va a cortar nada por arriba, conviene que hagáis uso de la clase FontMetrics, que toma las medidas de la fuente establecida, y permite ver qué altura va a tener el texto.
public void dibuja(Graphics g) { ... Font fuentePuntos = new Font("Arial", Font.BOLD | Font.ITALIC, 15); FontMetrics fmPuntos = g.getFontMetrics(fuentePuntos); g.drawString("Puntos: ...los que sean... ", x, y); }
Tomamos en este caso una fuente Arial de 15 puntos, negrita y cursiva. Luego obtenemos su FontMetrics, y dibujamos la cadena, donde x e y se deben sustituir por la coordenada inferior izquierda desde donde dibujar la cadena. La x en nuestro caso sería 0 (dibujamos desde el borde izquierdo), y la y dependerá de la altura que vaya a tener el texto dibujado. Para obtener eso, el objeto FontMetrics tiene un método llamado getAscent() que nos indica qué altura va a tener el texto. Basta con dibujarlo con la Y en ese valor, para que al subir como mucho llegue a y = 0, y no se pase por arriba:
g.drawString("Puntos: ...los que sean... ", 0, fmPuntos.getAscent());
ALGUNAS CUESTIONES OPTATIVAS
Si tenéis tiempo (y ganas), podéis añadirle, de forma opcional, cualquier cosa que consideréis al juego. Aunque no las vayáis a hacer, os recomendamos que las leáis, para tener claro algunas mejoras que podrían ser importantes en cualquier juego.
Por ejemplo, os sugerimos las siguientes:
PARA ENTREGAR
ENTREGA FINAL DEL BLOQUE 3