Fecha y hora actual: Lunes 23 Sep 2019 10:52
Índice del Foro

Foros de programación informática, diseño gráfico y Web

En esta comunidad intentaremos dar soporte de programación a todos los niveles, desde principiantes a profesionales de la informática, desarrollo de programas, programación web y mucho más.

Lección 99: JBuscaminas - GUI - Mouse Event

Responder al Tema

Índice del Foro > Programación en general > Lección 99: JBuscaminas - GUI - Mouse Event

Autor Mensaje
Kyshuo Ayame
Moderador Global


Registrado: 07 Ene 2011
Mensajes: 1044

Mensaje Publicado: Lunes 19 Ago 2013 16:55

Título del mensaje: Lección 99: JBuscaminas - GUI - Mouse Event

Responder citando

Interfaz gráfica:

Con todo lo que hemos visto hasta ahora sobre Swing estamos más que preparados para implementar una interfaz gráfica como la que ven en este buscaminas, sin embargo hay algunas cuestiones que habrá que resaltar y que corresponden más con lo que es implementar un videojuego que con lo que es Swing en sí. Además aprenderemos a manejar eventos de Mouse para facilitar la vida del usuario, aunque sería posible implementar el juego sin usar este tipo de eventos.

Para empezar tenemos campos de texto en nuestra GUI que únicamente pueden contener números y estos además deben ser positivos. Por tanto para esto crearemos una clase que represente a campos de texto que solo admitan números y que no entreguen el foco a menos que el valor sea positivo. Por supuesto, primero deben crear un paquete llamado Visual y luego en él una clase CampoNumerico con el siguiente código:

Código:
  1. package Visual;
  2.  
  3. import javax.swing.InputVerifier;
  4. import javax.swing.JComponent;
  5. import javax.swing.JTextField;
  6. import javax.swing.text.AttributeSet;
  7. import javax.swing.text.BadLocationException;
  8. import javax.swing.text.PlainDocument;
  9.  
  10. public class CampoNumerico extends JTextField{
  11.  
  12. public CampoNumerico(int columnas){
  13. super(columnas);
  14. setDocument(new Documento());
  15. setInputVerifier(new ControlFoco());
  16. }
  17.  
  18. public int getValor(){
  19. if(getText().equalsIgnoreCase(""))
  20. return 0;
  21. else
  22. return Integer.parseInt(getText());
  23. }
  24.  
  25. private class Documento extends PlainDocument{
  26.  
  27. @Override
  28. public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
  29. String fullText= getText()+str;
  30. if(fullText.matches("[0-9]+"))
  31. super.insertString(offs, str, a);
  32. }
  33. }
  34.  
  35. private class ControlFoco extends InputVerifier{
  36.  
  37. @Override
  38. public boolean verify(JComponent input) {
  39. return Integer.parseInt(getText())>0;
  40. }
  41.  
  42. }
  43. }


No he usado nada nuevo así que dejaré ese código sin comentarios. Toda duda pueden preguntarla aunque sería mejor que repasen los temas anteriores primero.

----------------------------------------------------------------------------------

Celda de nuestra GUI:

Ya irán viendo que tenemos un tablero en nuestra lógica el cual ya han implementado y ahora tenemos que implementar lo que es el tablero en nuestra interfaz gráfica. La idea es que el tablero de la GUI represente el estado del tablero en la lógica de modo que el usuario lo entienda y pueda jugar. Así, primero tenemos que pensar en qué es una celda en nuestro tablero gráfico.

En sí una celda es un botón del tipo JButton y ya, no tiene mucha ciencia. Sin embargo hay una dificultad inherente al juego Buscaminas en sí que es, dado un botón, saber en qué posición del tablero está con el fin de poder luego pasar esa información a la lógica. Por ejemplo, si presiono el botón en la fila 0 y la columna 0 (el primero del tablero) debo poder indicar a la lógica que quiero despejar la celda [0,0] del tablero lógico.
Ya podrán asumir que si tengo un tablero de botones y presiono uno de ellos se ejecutará un evento de acción y que mediante este puedo obtener el botón presionado pero no puedo saber la posición que este tenía en el tablero a menos que haga una evaluación exhaustiva de todos los botones del tablero comparando uno a uno llevando una cuenta de las filas y columnas recorridas. Todo esto se simplifica mucho con la herencia. Así, lo que yo hice fue crear una clase llamada CeldaVisual que herede de JButton y agregue dos nuevos atributos: fila y columna, de modo que directamente un botón pueda indicarme su posición en el tablero:

Código:
  1. package Visual;
  2.  
  3. import javax.swing.JButton;
  4.  
  5. public class CeldaVisual extends JButton{
  6. private int fila, columna;
  7.  
  8. private CeldaVisual(){}
  9.  
  10. public CeldaVisual(int fila, int columna){
  11. this.fila= fila;
  12. this.columna= columna;
  13. }
  14.  
  15. public int getColumna() {
  16. return columna;
  17. }
  18.  
  19. public int getFila() {
  20. return fila;
  21. }
  22. }


---------------------------------------------------------------------------------

Ventana de juego:

Tenemos todo lo necesario para implementar la ventana del juego. En este punto dicha ventana se dedicará únicamente a dibujarse, es decir, no dará funcionalidad a sus componentes ya que crearemos una clase que haga de controlador para ejecutar el juego dejando a la ventana la única responsabilidad de mantener la visual.

Para implementarla definiremos, además del panel principal, tres paneles tal como se muestra en la figura:



Iremos paso a paso creando esta clase ya que es extensa y si bien no es muy compleja tiene algunas particularidades que estaría bueno que vean en detalle. Crearemos una clase Ventana en el paquete Visual para la cual declararemos en principio los paneles que tendrá y los componentes del panel superior:

Código:
  1. package Visual;
  2.  
  3. import Excepciones.ExcepcionCeldaFueraDeRango;
  4. import Excepciones.ExcepcionDimensionesErroneas;
  5. import java.awt.FlowLayout;
  6. import javax.swing.*;
  7.  
  8. public class Ventana extends JFrame{
  9. private JPanel panelPrincipal, panelIniciar, panelTablero, panelDatos;
  10. private JLabel etiquetaOpcionesInicializacion, etiquetaFilas, etiquetaColumnas,
  11. etiquetaMinas;
  12. private CampoNumerico campoFilas, campoColumnas, campoMinas;
  13. private JRadioButton opcionFacil, opcionMedio, opcionDificil, opcionPersonalizado;
  14. private ButtonGroup grupoBotones;
  15. private JButton botonIniciar;
  16. private int filas, columnas;
  17.  
  18. public Ventana(int filas, int columnas) throws ExcepcionDimensionesErroneas{
  19. if(filas<=0||columnas<=0)
  20. throw new ExcepcionDimensionesErroneas("Dimensiones de tablero incorrectas.");
  21. else{
  22. /*
  23.   * CREANDO PANEL DE INICIO
  24.   */
  25. panelIniciar= new JPanel(new FlowLayout(FlowLayout.CENTER));
  26. etiquetaOpcionesInicializacion= new JLabel("Opciones de inicialización:");
  27. opcionFacil= new JRadioButton("Fácil");
  28. opcionFacil.setSelected(true);
  29. opcionMedio= new JRadioButton("Medio");
  30. opcionDificil= new JRadioButton("Dificil");
  31. opcionPersonalizado= new JRadioButton("Personalizado: ");
  32. grupoBotones= new ButtonGroup();
  33. grupoBotones.add(opcionFacil);
  34. grupoBotones.add(opcionMedio);
  35. grupoBotones.add(opcionDificil);
  36. grupoBotones.add(opcionPersonalizado);
  37. etiquetaFilas= new JLabel("Filas");
  38. campoFilas= new CampoNumerico(2);
  39. campoFilas.setEnabled(false);
  40. etiquetaColumnas= new JLabel("Columnas");
  41. campoColumnas= new CampoNumerico(2);
  42. campoColumnas.setEnabled(false);
  43. etiquetaMinas= new JLabel("Minas");
  44. campoMinas= new CampoNumerico(2);
  45. campoMinas.setEnabled(false);
  46. botonIniciar= new JButton("Iniciar juego");
  47. panelIniciar.add(etiquetaOpcionesInicializacion);
  48. panelIniciar.add(opcionFacil);
  49. panelIniciar.add(opcionMedio);
  50. panelIniciar.add(opcionDificil);
  51. panelIniciar.add(opcionPersonalizado);
  52. panelIniciar.add(etiquetaFilas);
  53. panelIniciar.add(campoFilas);
  54. panelIniciar.add(etiquetaColumnas);
  55. panelIniciar.add(campoColumnas);
  56. panelIniciar.add(etiquetaMinas);
  57. panelIniciar.add(campoMinas);
  58. panelIniciar.add(botonIniciar);
  59. }


Lo único que he hecho allí es inicializar el panel Iniciar que es el que irá arriba del todo. Allí no hay nada nuevo ni extraño. Deben leer línea por línea para comprender qué se está haciendo aunque no será difícil ya que básicamente se inicializan componentes y se agregan al panel. Lo que sí cabe destacar es que el constructor lanza la excepción ExcepcionDimensionesErroneas cuando las dimensiones pasadas son negativas o cero. Ustedes deben implementar esa excepción en el paquete Excepciones.

Ahora debemos inicializar el panel del tablero, el cual a mi juicio es el más interesante. Para empezar debemos declarar un nuevo atributo para nuestra clase que sea un arreglo bidimensional de botones (de tipo CeldaVisual, no JButton):

Código:
  1. private CeldaVisual[][] tableroCeldas;


Agregamos a nuestro constructor el siguiente código:

Código:
  1. /*
  2.   * CREANDO EL TABLERO
  3.   */
  4. this.filas= filas; this.columnas= columnas;
  5. tableroCeldas= new CeldaVisual[filas][columnas];
  6. panelTablero= new JPanel(new GridLayout(filas,columnas));
  7. panelTablero.setPreferredSize(new Dimension(800,600));
  8.  
  9. for(int i=0; i<filas; i++){
  10. for(int j=0; j<columnas; j++){
  11. tableroCeldas[i][j]=new CeldaVisual(i,j);
  12. }
  13. }
  14.  
  15. for(int i=0; i<filas; i++){
  16. for(int j=0; j<columnas; j++){
  17. panelTablero.add(tableroCeldas[i][j]);
  18. }
  19. }


Vean que se inicializan los atributos filas y columnas según los parámetros pasados. Luego verán para qué los usaremos. Lo que se hace después es inicializar el arreglo bidimensional tableroCeldas, es decir que le damos dimensiones a nuestro tablero de botones. Luego inicializamos nuestro panel con un GridLayout con las mismas dimensiones que nuestro arreglo de celdas y además le damos un tamaño preferido de 800x600 píxeles (porque me parece adecuado, nada más).

Después usamos dos FOR anidados como se suele hacer para trabajar con arreglos bidimensionales para inicializar cada objeto de nuestro arreglo de celdas. Esto hace falta porque cuando hacemos

Código:
  1. tableroCeldas= new CeldaVisual[filas][columnas];


únicamente damos dimensiones al arreglo, pero sus objetos son inicializados en null. Hay que inicializar a mano luego a cada uno. Vean que cada celda se inicializa pasando como fila y columna el valor de i y j que se corresponde con la posición en el arreglo. Teniendo ya el tablero inicializado correctamente usamos otra vez dos FOR anidados para agregar cada celda al panel, de este modo los botones están en un arreglo donde nos queda más cómodo trabajar y además están en el panel y serán dispuestos por el GridLayout que, como tienen las mismas dimensiones que el arreglo, es un reflejo visual del mismo.

El panel de datos será mucho más sencillo de inicializar:

Código:
  1. /*
  2.   * CREANDO EL PANEL DE DATOS
  3.   */
  4. etiquetaOcultas= new JLabel("Ocultas:");
  5. campoOcultas= new JTextField(3);
  6. campoOcultas.setEnabled(false);
  7. etiquetaMarcadas= new JLabel("Marcadas:");
  8. campoMarcadas= new JTextField(3);
  9. campoMarcadas.setEnabled(false);
  10. etiquetaDescubiertas= new JLabel("Descubiertas:");
  11. campoDescubiertas= new JTextField(3);
  12. campoDescubiertas.setEnabled(false);
  13. etiquetaMinasTotales= new JLabel("Minas:");
  14. campoMinasTotales= new JTextField(3);
  15. campoMinasTotales.setEnabled(false);
  16. panelDatos= new JPanel(new FlowLayout(FlowLayout.CENTER));
  17. panelDatos.add(etiquetaOcultas);
  18. panelDatos.add(campoOcultas);
  19. panelDatos.add(etiquetaMarcadas);
  20. panelDatos.add(campoMarcadas);
  21. panelDatos.add(etiquetaDescubiertas);
  22. panelDatos.add(campoDescubiertas);
  23. panelDatos.add(etiquetaMinasTotales);
  24. panelDatos.add(campoMinasTotales);


Noten que para los campos de texto usé JTextField y no CampoNumerico. Esto es porque como esos campos serán usados internamente y no por el usuario no hay que controlar nada, aunque sería bueno igual usar la clase que implementamos. Lo único que queda ahora es inicializar el panel principal, añadirlo como panel de contenido y configurar la ventana:

Código:
  1. panelPrincipal= new JPanel();
  2. panelPrincipal.setLayout(new BoxLayout(panelPrincipal,BoxLayout.Y_AXIS));
  3. panelPrincipal.add(panelIniciar);
  4. panelPrincipal.add(panelTablero);
  5. panelPrincipal.add(panelDatos);
  6.  
  7. setContentPane(panelPrincipal);
  8. setTitle("JBuscaMinas");
  9. setVisible(true);
  10. setLocationRelativeTo(null);
  11. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  12. pack();


Vean que para este panel he usado BoxLayout para que los demás paneles queden dispuestos verticalmente uno encima del otro. Si no lo hacen así verán que queda todo mal.

Ahora resta agregarle algunas operaciones interesantes a la ventana tales como los getter de siempre para obtener sus componentes:

Código:
  1. public JButton getBotonIniciar()
  2. public CampoNumerico getCampoColumnas()
  3. public JTextField getCampoDescubiertas()
  4. public CampoNumerico getCampoFilas()
  5. public JTextField getCampoMarcadas()
  6. public CampoNumerico getCampoMinas()
  7. public JTextField getCampoMinasTotales()
  8. public JTextField getCampoOcultas()
  9. public JRadioButton getOpcionDificil()
  10. public JRadioButton getOpcionPersonalizado()
  11. public JRadioButton getOpcionFacil()
  12. public JRadioButton getOpcionMedio()


Verán que el tablero de botones no es retornado. Esto es porque no me interesa que fuera de Ventana tengan acceso completo a él para modificarlo, de este modo lo que sí me interesa es obtener un botón específico de una posición dada, así agregamos esta operación:

Código:
  1. public JButton getCelda(int fila, int columna) throws ExcepcionCeldaFueraDeRango


Se retorna la celda que está en la posición dada; se lanza la excepción si la posición no es válida. Ustedes deben implmentarla.
Ahora agregamos la siguiente operación:

Código:
  1. public void reiniciarTablero(int filas, int columnas) throws ExcepcionDimensionesErroneas{
  2. if(filas<0||columnas<0)
  3. throw new ExcepcionDimensionesErroneas("Dimensiones de tablero incorrectas.");
  4. else{
  5. this.filas= filas; this.columnas= columnas;
  6. tableroCeldas= new CeldaVisual[filas][columnas];
  7. panelTablero.removeAll();
  8. panelTablero.setLayout(new GridLayout(filas,columnas));
  9. panelTablero.setSize(new Dimension(800,600));
  10. panelTablero.setPreferredSize(new Dimension(800,600));
  11.  
  12. for(int i=0; i<filas; i++){
  13. for(int j=0; j<columnas; j++){
  14. tableroCeldas[i][j]=new CeldaVisual(i,j);
  15. }
  16. }
  17.  
  18. for(int i=0; i<filas; i++){
  19. for(int j=0; j<columnas; j++){
  20. panelTablero.add(tableroCeldas[i][j]);
  21. }
  22. }
  23. }
  24. }


Es casi lo mismo que hace el constructor al iniciar el panel del tablero solo que además se utiliza el procedimiento removeAll del panel para borrar los anteriores componentes que pudiera tener. Incluso en el constructor podríamos haber hecho un llamado a esta operación en vez de implementarlo todo.

==============================================

Eventos de Mouse:

Antes de terminar de implementar el juego veremos una breve descripción de lo que son los eventos de Mouse. Como todos los eventos de Swing se gestionan con una interfaz que los representa. La interfaz para eventos de ratón es MouseListener que define las siguientes operaciones:

  • public void mouseClicked(MouseEvent e): Es invocada cuando se hace un clic con uno de los botones del mouse.
  • public void mousePressed(MouseEvent e): Es invocada cuando se presiona uno de los botones del mouse. Es más adecuada para ver cuando un botón se mantiene presionado.
  • public void mouseReleased(MouseEvent e): Es invocada cuando se suelta uno de los botones del mouse.
  • public void mouseEntered(MouseEvent e): Es invocada cuando el puntero del mouse señala al componente en cuestión. Por ejemplo, si es un botón, esta operación será llamada cuando el puntero del Mouse entre en el área del botón.
  • public void mouseExited(MouseEvent e): Es invocada cuando el puntero del mouse sale del componente en cuestión, es decir, deja de apuntarlo.


Como verán todas las operaciones reciben un objeto MouseEvent como argumento el cual contendrá la información del evento ocurrido. Les dejo un ejemplo simple de una clase Ventana que tiene un botón y un área de texto para que vean cómo usar estos eventos. Básicamente el área de texto mostrará a la operación llamada al gestionar eventos de Mouse en el botón:

Código:
  1. import java.awt.FlowLayout;
  2. import java.awt.event.MouseEvent;
  3. import java.awt.event.MouseListener;
  4. import javax.swing.*;
  5.  
  6. public class Ventana extends JFrame{
  7. JPanel panelPrincipal;
  8. JButton boton;
  9. JTextArea areaTexto;
  10. JScrollPane barras; //Para el área de texto.
  11. EventosRaton gestorEventos;
  12.  
  13. public Ventana(){
  14. panelPrincipal= new JPanel(new FlowLayout(FlowLayout.CENTER));
  15. boton= new JButton("Soy un botón");
  16. areaTexto= new JTextArea(15,20);
  17. barras= new JScrollPane(areaTexto);
  18. panelPrincipal.add(boton);
  19. panelPrincipal.add(barras);
  20.  
  21. gestorEventos= new EventosRaton();
  22. boton.addMouseListener(gestorEventos);
  23.  
  24. setVisible(true);
  25. setContentPane(panelPrincipal);
  26. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  27. setLocationRelativeTo(null);
  28. setSize(300,320);
  29. }
  30.  
  31.  
  32. private class EventosRaton implements MouseListener{
  33.  
  34. @Override
  35. public void mouseClicked(MouseEvent e) {
  36. areaTexto.append("\nSe hizo un click.");
  37. }
  38.  
  39. @Override
  40. public void mousePressed(MouseEvent e) {
  41. areaTexto.append("\nSe presionó un botón.");
  42. }
  43.  
  44. @Override
  45. public void mouseReleased(MouseEvent e) {
  46. areaTexto.append("\nSe soltó un botón.");
  47. }
  48.  
  49. @Override
  50. public void mouseEntered(MouseEvent e) {
  51. areaTexto.append("\nEl mouse está en el botón.");
  52. }
  53.  
  54. @Override
  55. public void mouseExited(MouseEvent e) {
  56. areaTexto.append("\nEl mouse ya no está en el botón.");
  57. }
  58.  
  59. }
  60. }


Noten que se ha usado la operación addMouseListener de JButton para agregar escuchadores de eventos de Mouse al botón de nuestra ventana. No hay mucha más ciencia en esto.

-----------------------------------------------------------------------------------

Controlador principal de JBuscaMinas:

El buscaminas tiene cuatro modalidades:

  • Nivel principiante: 8 × 8 casillas y 10 minas.
  • Nivel intermedio: 16 × 16 casillas y 40 minas.
  • Nivel experto: 16 × 30 casillas y 99 minas.
  • Nivel personalizado: se elijen las dimensiones y la cantidad de minas a gusto.


Para poder implementar todo lo que el juego hace tendremos que usar eventos de artículo (itemEvent) para los botones de radio, eventos de Mouse (para trabajar con el tablero) y eventos de acción (para el botón que inicia el juego). Nuestra clase principal (la que tiene a main) será la encargada de todo, además no usaremos clases internas para gestionar eventos sino que será la propia clase principal quién implementará las interfaces necesarias. Crearemos así un paquete llamado Launcher y dentro de él una clase con el mismo nombre.

Comenzaremos viendo las primeras líneas de esta clase y su constructor:

Código:
  1. package Launcher;
  2.  
  3. import Excepciones.ExcepcionCeldaFueraDeRango;
  4. import Excepciones.ExcepcionDimensionesErroneas;
  5. import Logica.Celda;
  6. import Logica.Juego;
  7. import Visual.CeldaVisual;
  8. import Visual.Ventana;
  9. import java.awt.event.*;
  10. import javax.swing.JOptionPane;
  11.  
  12. public class Launcher implements MouseListener, ItemListener, ActionListener{
  13. private Ventana ventana;
  14. private Juego juego;
  15.  
  16. public Launcher() throws ExcepcionDimensionesErroneas, ExcepcionCeldaFueraDeRango{
  17. ventana= new Ventana(1,1);
  18. ventana.getCelda(0,0).setEnabled(false);
  19. ventana.getBotonIniciar().addActionListener(this);
  20. ventana.getOpcionFacil().addItemListener(this);
  21. ventana.getOpcionMedio().addItemListener(this);
  22. ventana.getOpcionDificil().addItemListener(this);
  23. ventana.getOpcionPersonalizado().addItemListener(this);
  24. }


Verán que lo que tiene como atributos son una Ventana y un Juego, es decir que esta clase será la encargada de mantener actualizada la visual según el estado de Juego y además interactuará con él. Noten que al final de cuentas las clases Ventana y Juego no se conocen entre sí ni tienen nada que las vincule, es decir que son independientes.
El constructor de Launcher inicializa la ventana con una única celda en el tablero y además la deshabilita (segunda línea). Luego agrega a los botones de radio y al botón de inicio un escuchador de eventos correspondientes usándose a sí misma ya que es capaz de escuchar tres tipos de eventos distintos. Esto es porque tenemos que esperar a que el usuario elija el nivel de dificultad con el que va a jugar antes de inicializar el tablero. Por tanto esto se realizará luego de que el usuario presione el botón Iniciar juego así que será la operación actionPerformed la encargada de preparar todo para comenzar:

Código:
  1. @Override
  2. public void actionPerformed(ActionEvent e) {
  3. int filas, columnas, minas;
  4. if(ventana.getOpcionFacil().isSelected()){
  5. filas=columnas=8; minas=10;
  6. }else if(ventana.getOpcionMedio().isSelected()){
  7. filas=columnas=16; minas=40;
  8. }else if(ventana.getOpcionDificil().isSelected()){
  9. filas=16; columnas=30; minas=99;
  10. }else{
  11. filas= ventana.getCampoFilas().getValor();
  12. columnas= ventana.getCampoColumnas().getValor();
  13. minas= ventana.getCampoMinas().getValor();
  14. }
  15.  
  16. try{
  17. ventana.getBotonIniciar().setEnabled(false);
  18. iniciarJuego(filas,columnas,minas);
  19. }catch(Exception ex){
  20. JOptionPane.showMessageDialog(ventana,
  21. ex.getMessage(),"Error",JOptionPane.ERROR_MESSAGE);
  22. }
  23. }


Si leen con atención verán que esta operación inicializa el número de filas y columnas en función del radio botón elegido. Luego deshabilita el botón Iniciar juego y hace un llamado a la operación iniciarJuego que aún no hemos visto. Noten que, como estamos en la clase que da ejecución al juego tenemos que encargarnos de capturar las excepciones, por tanto donde pueda ocurrir alguna esta es capturada y un diálogo con un mensaje será mostrado al usuario para indicarle el error. Veamos entonces qué hace iniciarJuego:

Código:
  1. public void iniciarJuego(int filas, int columnas, int minas)throws ExcepcionDimensionesErroneas{
  2. ventana.reiniciarTablero(filas,columnas);
  3. ventana.paintComponents(ventana.getGraphics());
  4.  
  5. try{
  6. for(int i=0; i<filas; i++)
  7. for(int j=0; j<columnas; j++)
  8. ventana.getCelda(i, j).addMouseListener(this);
  9.  
  10. juego= new Juego(filas, columnas, minas);
  11. ventana.getCampoDescubiertas().setText(juego.getDescubiertas()+"");
  12. ventana.getCampoMarcadas().setText(juego.getMarcadas()+"");
  13. ventana.getCampoOcultas().setText((juego.getTotalCeldas()-juego.getDescubiertas())+"");
  14. ventana.getCampoMinasTotales().setText((juego.getBombas()-juego.getMarcadas())+""); }catch(ExcepcionCeldaFueraDeRango e){
  15. JOptionPane.showMessageDialog(ventana, e.getMessage(), "Error",JOptionPane.ERROR_MESSAGE);
  16. }
  17. }


Primero reiniciamos el tablero con las nuevas dimensiones. Esto se hace dentro de la clase Ventana y por tanto ya vimos su funcionamiento. Verán que luego de esa invocación se llama a paintComponents de JFrame. ¿Qué es esto? Pues como hemos quitado componentes de un panel, le hemos cambiado su layout manager y luego le hemos agregado nuevos elementos hace falta volver a pintar (dibujar) la ventana. En general una ventana se dibuja en pantalla cuando es inicializada y luego cuando pasa de estar inactiva a estar activa (por ejemplo al minimizar y restaurar). Si ustedes no hacen el llamado a paintComponents verán que la ventana no cambiará de aspecto respecto a su imagen inicial hasta que ustedes la obliguen, sea minimizándola y restaurándola o poniéndole otra ventana por delante para luego hacerla activa. Este es un aspecto importante y que posiblemente les de problemas en algún proyecto personal, ya que a veces no queda claro cuando Swing actualiza la visualización de los componentes de una ventana y, en general, nos acostumbra a que esto sucede de inmediato.

Luego, con dos FOR anidados se recorren todas las celdas de la ventana y se les agrega el escuchador de eventos de Mouse. Lo que queda ahora es inicializar a nuestra variable juego la cual representa al buscaminas en sí, todo lo otro es configuración visual. Finalmente se actualizan los datos en los campos de texto destinados a mostrar el estado actual del juego.

Como habrán visto entonces, los botones del tablero gestionan eventos de Mouse y no de acción. ¿Por qué? Pues porque el juego buscaminas nos da dos opciones al presionar una celda:

  • Descubrirla
  • Marcarla, que en general se muestra con una banderita.


Esto se hace en los juegos con el botón izquierdo y derecho del Mouse respectivamente. Si usamos eventos de acción no tenemos forma de diferenciar esto, sin embargo los eventos de Mouse nos permiten saber qué botón fue el que desencadenó el evento, así, si fue el izquierdo sé que debo descubrir la celda (si no está marcada) y si fue el derecho sé que debe marcar o desmarcar. De este modo entonces la operación que se hará cargo de eso será mouseClicked:

Código:
  1. @Override
  2. public void mouseClicked(MouseEvent e) {
  3. CeldaVisual c= (CeldaVisual)e.getSource();
  4. try{
  5. switch(e.getButton()){
  6. case 1: juego.descubrir(c.getFila(),c.getColumna());
  7. break;
  8. case 3: juego.marcarDesmarcarCelda(c.getFila(), c.getColumna());
  9. break;
  10. }
  11. actualizarJuego();
  12. }catch(Exception ex){
  13. JOptionPane.showMessageDialog(ventana,ex.getMessage(),"Error",JOptionPane.ERROR_MESSAGE);
  14. }
  15. }


La clase MouseEvent tiene una operación llamada getButton que retorna un número que representa al botón presionado:

  • Devuelve 1 para el botón izquierdo.
  • Devuelve 2 para la rueda o botón medio.
  • Devuelve 3 para el botón derecho.


Lean la documentación para saber más al respecto.
La primera línea de la operación obtiene la fuente del evento, es decir el botón que fue presionado en el tablero y lo castea a CeldaVisual. Luego con un switch diferenciamos el número obtenido por getButton para saber qué hacer; si fue el botón izquierdo llamamos a la operación descubrir de juego pasando las posiciones del botón presionado en el tablero, sino, llamamos a marcarDesmarcarCelda pasando también las posiciones correspondientes. Esto claramente afecta el estado del juego y del tablero lógico, por tanto hay que actualizar dicho estado para que sea reflejado por la GUI. Esto es típico de los videojuegos, actualizar el estado hacia la interfaz gráfica. Para esto implementaremos una operación llamada actualizarJuego que haga todo lo necesario, la cual es llamada fuera del switch:

Código:
  1. public void actualizarJuego(){
  2. try{
  3. int circundantes;
  4. for(int i=0; i<juego.getFilas();i++)
  5. for(int j=0; j<juego.getColumnas(); j++){
  6. if(juego.getEstadoCelda(i, j)==Celda.Estado.MARCADA)
  7. ventana.getCelda(i, j).setText("!");
  8. else if(juego.getEstadoCelda(i, j)==Celda.Estado.DESCUBIERTA){
  9. circundantes= juego.getCircundantes(i, j);
  10. if(juego.tieneBomba(i, j))
  11. ventana.getCelda(i, j).setText("*");
  12. else
  13. ventana.getCelda(i, j).setText(""+circundantes);
  14. ventana.getCelda(i, j).setEnabled(false);
  15. }else if(juego.getEstadoCelda(i, j)==Celda.Estado.OCULTA){
  16. ventana.getCelda(i, j).setText("");
  17. }
  18. }
  19. }catch(Exception e){
  20. JOptionPane.showMessageDialog(ventana, e.getMessage(),"Error",JOptionPane.ERROR_MESSAGE);
  21. }
  22.  
  23. if(juego.getEstado()==Juego.Estado.GANADO)
  24. JOptionPane.showMessageDialog(ventana,"¡¡¡EXCELENTE!!! ¡¡¡HAS GANADO!!!","¡¡¡GANASTE!!!",JOptionPane.INFORMATION_MESSAGE);
  25. else if(juego.getEstado()==Juego.Estado.PERDIDO)
  26. JOptionPane.showMessageDialog(ventana, "Lo lamento... has perdido...","¡¡¡PERDISTE!!!",JOptionPane.INFORMATION_MESSAGE);
  27.  
  28. ventana.getCampoDescubiertas().setText(juego.getDescubiertas()+"");
  29. ventana.getCampoMarcadas().setText(juego.getMarcadas()+"");
  30. ventana.getCampoOcultas().setText((juego.getTotalCeldas()-juego.getDescubiertas())+"");
  31. ventana.getCampoMinasTotales().setText((juego.getBombas()-juego.getMarcadas())+"");
  32. }


La idea es la siguiente: Recorrer todo el tablero lógico actualizando por cada celda de él la visual de los botones. Si una celda del tablero está oculta el botón queda como está, si está descubierta el botón queda inhabilitado y además agregamos un texto con las bombas circundantes, si está marcada escribimos un ! para indicarlo (por ahora no usaremos íconos ni cosas bonitas). Todo eso se hace con los dos FOR anidados que tenemos. Finalmente al final vemos el estado final del juego para indicar si ganamos o perdimos y para actualizar la información en los campos de texto del panel inferior. Sería bueno que lean línea a línea lo que hice.

Nos queda por ver la operación itemStateChanged de los botones de radio que únicamente habilita o deshabilita los campos de texto para elegir un juego personalizado:

Código:
  1. @Override
  2. public void itemStateChanged(ItemEvent e) {
  3. boolean habilitar=e.getSource().equals(ventana.getOpcionPersonalizado());
  4. ventana.getCampoFilas().setEnabled(habilitar);
  5. ventana.getCampoColumnas().setEnabled(habilitar);
  6. ventana.getCampoMinas().setEnabled(habilitar);
  7. }


Y el procedimiento main que da inicio a todo:

Código:
  1. public static void main(String[] args){
  2. try{
  3. Launcher aplicacion= new Launcher();
  4. }catch(ExcepcionDimensionesErroneas | ExcepcionCeldaFueraDeRango e){
  5. JOptionPane.showMessageDialog(null, e.getMessage());
  6. }
  7. }


Con esto hemos terminado de implementar el buscaminas, no sin que nos queden algunos cabitos sueltos:

  • Sería interesante que el juego controlara la cantidad de celdas marcadas de modo que no puedan marcarse más celdas que la cantidad de minas.
  • Bloquear el tablero al finalizar el juego ya que, tal como está ahora, luego de recibir el mensaje adecuado es posible seguir presionando celdas obteniendo mensajes erróneos.
  • Sería bueno poder reiniciar el juego sin tener que cerrar y abrir de nuevo.
  • Mi implementación de la clase Juego provoca un error cuando se colocan tantas minas como celdas, es decir, cuando todas las celdas tienen minas. Si se fijan verán que reciben el mensaje de que han ganado al presionar una celda. ¿A ustedes les pasa lo mismo?
  • Cuando se inicia el juego, además de deshabilitar el botón Iniciar juego habría que deshabilitar los botones de radio.
  • La opción personalizado debería controlar que la cantidad de minas no exceda el total de celdas en el tablero.


Estas, y más correcciones deberían ser implementadas por ustedes como ejercicio.

Volver arriba
Ver perfil del usuario Enviar mensaje privado
jlrods
Usuario Inquieto


Registrado: 03 May 2014
Mensajes: 60

Mensaje Publicado: Viernes 24 Jun 2016 21:18

Título del mensaje: Lección 99: JBuscaminas - GUI - Mouse Event

Responder citando

Hola de nuevo,

Mucho tiempo sin postear por aca. Solo queria felicitar una vez mas a Kyshuo Ayame y a todos los colaboradores que siempre comentaron y aclararon dudas en su momento. Sinceramente, gracias mil!

Sin duda alguna este curso no tiene precio, al menos para mi, por su alta calidad y robustez. Un curso que no tiene desperdicio y que introduce de una manera altamente didactica conceptos basicos y no tan basicos (si, me refiero a la recursion) de la programacion a aquellos que no han tenido la posibilidad de entrarse en el maravilloso mundo del coding.

Por alla en el 2013 empece a buscar cursitos online que me dieran aunque sea una vaga idea de como programar. Tuve la gran suerte de dar con este curso y disfrutarlo en su totalidad. Aunque no tuve el privilegio de seguirlo en tiempo real, pude leer las lecciones en mi tiempo libre, postear preguntas y discutir diversos temas con aquellos que tuvieron la gentileza de tomar parte de su tiempo para leer y comentar trozos de codigos de un principiante, en especial su moderador Kysuo Ayame.

Para hacer la historia corta, mientras seguia este curso inicie otros cursos de programacion con clases presenciales y posteriormente tuve la oportunidad de iniciar un diplomado avanzado a nivel univeritario en Ciencias de la Computacion como programa de conversion para profesionales de otras areas y asi incluirlos en el area de TI. Hoy dia estoy por iniciar mi trabajo de grado y eventualmente culminare los estudios a finales de anio. Espero entonces que lo que incio como un hobby y sea vuelto mi pasion, sea tambien mi fuente de empleo.

Todo esto gracias al apoyo inicial que recibi aqui y la buena base que me lleve de estas lecciones. Estoy muy agradecido y ojala pueda retribuir en el futuro con el foro de alguna manera.

Finalmente, como no me gusta dejar las cosas sin acabar decidi seguir las lecciones de este curso, asi que progrme el juego buscaminas una vez mas, no en java sino en C#, ya que es el lenguaje que venia utilizando ultimamente. La estructura de la logica es practimente la sugerida por Kysuo y la interfaz grafica la realice usando WPF para acortar tiempo. A continuacion dejo algunas imagenes de la visual del juego. Espero poder completar el proyecto final de la leccion 100 (Master mind) y asi oficialmente culminar el curso.

No me queda mas que decir muchas gracias!

Aca dejo un link con pantallazos del Buscaminas que programe( Y si, el metodo "descubrir" para descubrir una celda es recursivo!!!):

https://www.dropbox.com/sh/a33a7gs958eppus/AABCrFVhs9gtRSHN_-nGp75-a?dl=0

Volver arriba
Ver perfil del usuario Enviar mensaje privado
Kyshuo Ayame
Moderador Global


Registrado: 07 Ene 2011
Mensajes: 1044

Mensaje Publicado: Martes 28 Jun 2016 20:44

Título del mensaje: Lección 99: JBuscaminas - GUI - Mouse Event

Responder citando

Estimado jlrods, la verdad que tus palabras son un gran incentivo para mí. El hecho de que el tiempo invertido en escribir esas lecciones sea de ayuda real para la vida de una persona no tiene precio. Es un valor que se extiende e incrementa en el tiempo.

No he podido finalizar este curso en el foro, es decir, ha quedado inconcluso.

Actualmente estoy en un proyecto personal en el que iniciaré una academia particular, y tus palabras son un empujón más para seguir adelante con este emprendimiento.

Muchas gracias por tomarte el tiempo de devolver tu experiencia.

Volver arriba
Ver perfil del usuario Enviar mensaje privado
Responder al Tema
Mostrar mensajes anteriores:   
Ir a:  
Todas las horas están en GMT + 2 Horas

Temas relacionados

Tema Autor Foros Respuestas Publicado
El foro no contiene ningún mensaje nuevo

Lección 101: Lectura/Escritura de archivos de t...

Kyshuo Ayame Programación en general 3 Sábado 26 Oct 2013 03:04 Ver último mensaje
El foro no contiene ningún mensaje nuevo

Lección 100: JMasterMind

Kyshuo Ayame Programación en general 7 Lunes 02 Sep 2013 20:29 Ver último mensaje
El foro no contiene ningún mensaje nuevo

Lección 98: JBuscaMinas - Lógica

Kyshuo Ayame Programación en general 7 Martes 06 Ago 2013 18:23 Ver último mensaje
El foro no contiene ningún mensaje nuevo

Lección 97: JCalculadora

Kyshuo Ayame Programación en general 10 Lunes 29 Jul 2013 17:46 Ver último mensaje
El foro no contiene ningún mensaje nuevo

Lección 96: Bordes predefinidos y Layout Managers

Kyshuo Ayame Programación en general 4 Miércoles 17 Jul 2013 18:30 Ver último mensaje
Panel de Control
No puede crear mensajes, No puede responder temas, No puede editar sus mensajes, No puede borrar sus mensajes, No puede votar en encuestas,