Universidad Carlos III de Madrid

Grado en Ingeniería Telemática/Grado en Ingeniería de Sistemas de Comunicaciones

Enero-Mayo 2014

Interfaces Gráficas de Usuario

Lab Section1. Sesión 4 (laboratorio): Interfaces Gráficas de Usuario (II)

Exercise Section1.1. Calculadora en Swing, ahora con eventos

En este ejercicio debes finalizar la calculadora que empezaste a programar en la práctica anterior. Utiliza tu solución a dicha práctica como punto de partida. Si no tienes el código a mano, utiliza la solución oficial que se ha publicado para dicho ejercicio.

Para simplificar el problema, esta versión de la calculadora sólo debe trabajar con números enteros. Por tanto, deshabilita en la interfaz el botón ".", de tal forma que no sea pinchable.

Se te proporciona la lógica interna de la calculadora ya programada en la clase CalculatorLogic que se muestra a continuación, pero necesitarás enlazarla adecuadamente con tu interfaz gráfica. Esta clase dispone de métodos públicos para que, cada vez que el usuario pulse un botón, se lo comuniques. Además, tras comunicar cada pulsación debes invocar al método getDisplay(), que te indicará qué debes mostrar en la pantalla de la calculadora.

/**
 * Class that implements the logic of a basic calculator for integer
 * numbers.
 *
 */
public class CalculatorLogic {
    public static final int OPERATOR_NONE = 0;
    public static final int OPERATOR_SUM = 1;
    public static final int OPERATOR_SUB = 2;
    public static final int OPERATOR_MUL = 3;
    public static final int OPERATOR_DIV = 4;

    private int accumulator;
    private int currentOperand;
    private int currentOperator;
    private boolean operandAvailable;

    /**
     * Creates a new instance of the calculator logic. In its initial
     * state the calculator displays just a zero, the value of its
     * internal accumulator is also zero and there is no operator
     * pending to be applied.
     *
     */
    public CalculatorLogic() {
        pressReset();
    }

    /**
     * Returns the information that should be shown at the display of
     * the calculator for its current state.
     *
     * @return The information to be shown at the display.
     *
     */
    public String getDisplay() {
        String display;
        if (operandAvailable) {
            display = Integer.toString(currentOperand);
        } else {
            display = Integer.toString(accumulator);
        }
        return display;
    }

    /**
     * Notifies that the user has pressed a button with a digit.
     *
     * @param digit The digit the user has pressed (0 to 9).
     *
     */
    public void pressDigit(int digit) {
        currentOperand = currentOperand * 10 + digit;
        operandAvailable = true;
    }

    /**
     * Notifies that the user has pressed a button with an operand.
     *
     * @param operator The operand expressed with one of the constants
     *                 OPERATOR_ADD, OPERATOR_SUB, OPERATOR_MUL or
     *                 OPERATOR_DIV.
     *
     */
    public void pressOperator(int operator) {
        if (operandAvailable) {
            performPendingOperation();
        }
        expectNewOperand(operator);
    }

    /**
     * Notifies that the user has pressed the "=" button.
     *
     */
    public void pressEqual() {
        if (operandAvailable) {
            performPendingOperation();
        }
        expectNewOperand(OPERATOR_NONE);
    }

    /**
     * Notifies that the user has pressed the "C" button, which removes
     * the last digit that was typed by the user.
     *
     */
    public void pressDelete() {
        currentOperand /= 10;
        if (currentOperand == 0) {
            operandAvailable = false;
        }
    }

    /**
     * Notifies that the user has pressed the "AC" button, which
     * resets the state of the calculator to its initial state
     * (display 0, accumulator 0, no pending operators.)
     *
     */
    public void pressReset() {
        accumulator = 0;
        expectNewOperand(OPERATOR_NONE);
    }

    private void expectNewOperand(int operator) {
        currentOperator = operator;
        currentOperand = 0;
        operandAvailable = false;
    }

    private void performPendingOperation() {
        switch (currentOperator) {
        case OPERATOR_NONE:
            accumulator = currentOperand;
            break;
        case OPERATOR_SUM:
            accumulator += currentOperand;
            break;
        case OPERATOR_SUB:
            accumulator -= currentOperand;
            break;
        case OPERATOR_MUL:
            accumulator *= currentOperand;
            break;
        case OPERATOR_DIV:
            accumulator /= currentOperand;
            break;
        }
    }
}

Soluciones

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

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

public class Calculator extends JFrame {

    public Calculator() {
        super("Calculator");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        getContentPane().setLayout(new BorderLayout());
        JPanel buttonsPanel = new JPanel(new GridLayout(6, 3));
        CalculatorLogic logic = new CalculatorLogic();
        JLabel display = new JLabel(logic.getDisplay(), SwingConstants.RIGHT);
        ActionListener digitListener = new DigitListener(logic, display);
        ActionListener operatorListener = new OperatorListener(logic, display);
        addButton(buttonsPanel, "AC", operatorListener);
        addButton(buttonsPanel, "+", operatorListener);
        addButton(buttonsPanel, "-", operatorListener);
        addButton(buttonsPanel, "C", operatorListener);
        addButton(buttonsPanel, "*", operatorListener);
        addButton(buttonsPanel, "/", operatorListener);
        addButton(buttonsPanel, "7", digitListener);
        addButton(buttonsPanel, "8", digitListener);
        addButton(buttonsPanel, "9", digitListener);
        addButton(buttonsPanel, "4", digitListener);
        addButton(buttonsPanel, "5", digitListener);
        addButton(buttonsPanel, "6", digitListener);
        addButton(buttonsPanel, "1", digitListener);
        addButton(buttonsPanel, "2", digitListener);
        addButton(buttonsPanel, "3", digitListener);
        addButton(buttonsPanel, ".", null);
        addButton(buttonsPanel, "0", digitListener);
        addButton(buttonsPanel, "=", operatorListener);
        getContentPane().add(display, BorderLayout.NORTH);
        getContentPane().add(buttonsPanel, BorderLayout.CENTER);
        pack();
        setVisible(true);
    }

    private void addButton(Container container, String command,
                           ActionListener listener) {
        JButton button = new JButton(command);
        if (listener != null) {
            button.addActionListener(listener);
        } else {
            button.setEnabled(false);
        }
        container.add(button);
    }

    public static void main(String[] args) {
        //Schedule a job for the event-dispatching thread:
        //creating and showing this application's GUI.
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    Calculator gui = new Calculator();
                }
            });
    }
}
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JLabel;


public class DigitListener implements ActionListener {

    private CalculatorLogic logic;
    private JLabel display;

    public DigitListener(CalculatorLogic logic, JLabel display) {
        this.logic = logic;
        this.display = display;
    }

    public void actionPerformed(ActionEvent e) {
        logic.pressDigit(Integer.parseInt(e.getActionCommand()));
        display.setText(logic.getDisplay());
    }
}
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JLabel;


public class OperatorListener implements ActionListener {

    private CalculatorLogic logic;
    private JLabel display;

    public OperatorListener(CalculatorLogic logic, JLabel display) {
        this.logic = logic;
        this.display = display;
    }

    public void actionPerformed(ActionEvent e) {
        String command = e.getActionCommand();
        if (command.equals("+")) {
            logic.pressOperator(CalculatorLogic.OPERATOR_SUM);
        } else if (command.equals("-")) {
            logic.pressOperator(CalculatorLogic.OPERATOR_SUB);
        } else if (command.equals("*")) {
            logic.pressOperator(CalculatorLogic.OPERATOR_MUL);
        } else if (command.equals("/")) {
            logic.pressOperator(CalculatorLogic.OPERATOR_DIV);
        } else if (command.equals("AC")) {
            logic.pressReset();
        } else if (command.equals("=")) {
            logic.pressEqual();
        } else if (command.equals("C")) {
            logic.pressDelete();
        }
        display.setText(logic.getDisplay());
    }
}

Exercise Section1.2. Seguimiento del cursor

En este ejercicio programarás una interfaz gráfica en que los ojos de un muñeco deben seguir la posición del cursor cuando este se encuentre dentro de la ventana. El programa debe tener una apariencia similar a la siguiente:

El contorno exterior del ojo se está dibujando como un círculo de radio 60. La pupila tiene radio 10. Su posición depende de la posición del cursor dentro de la ventana: debe estar siempre sobre la recta que une el cursor con el centro del ojo, a exactamente 30 píxeles de este último. A continuación se muestra un ejemplo:

Como puedes observar en el esquema que se muestra a continuación, si se conocen las coordenadas del centro del ojo (O), las coordenadas del cursor (C) y la distancia d = 30 píxeles, calcular la posición en que se debe situar la pupila (X) consiste en resolver un problema de semejanza de triángulos rectángulos:

Apartado 1

En primer lugar, escribe el código necesario para que la aplicación funcione con respecto a un punto fijo. Esto es, en vez de seguir el cursor, las pupilas se alinearán con respecto a un punto fijo de prueba que definas. De esta forma, no necesitarás manejar eventos por el momento.

Puedes basarte en el código del ejercicio de dibujo de la práctica anterior. Para hacerte más fácil el dibujo de círculos, puedes añadir a tu código el método siguiente, que recibe las coordenadas del centro del círculo, su radio, el color a usar y un parámetro booleano que indica si se debe pintar el relleno o el borde. Para pintar un círculo con relleno blanco y borde negro, pinta primero un círculo relleno de color blanco y después un círculo sin relleno de color negro.

    private void drawCircle(Graphics g, int x, int y, int radius, Color color,
                            boolean filled) {
        int posX = x - radius;
        int posY = y - radius;
        g.setColor(color);
        if (filled) {
            g.fillOval(posX, posY, 2 * radius, 2 * radius);
        } else {
            g.drawOval(posX, posY, 2 * radius, 2 * radius);
        }
    }

Prueba la aplicación con distintos puntos fijos.

Apartado 2

Ahora debes hacer que los ojos sigan la posición del cursor. Para ello, debes hacer que tu panel de dibujo ejerza de escuchador MouseMotionListener para él mismo. Esto es, desde el propio panel debes manejar los eventos que se producen cuando se mueve el cursor en el interior del panel.

Cada vez que detectes que se ha movido el cursor debes recoger las nuevas coordenadas del mismo y forzar que se actualice el dibujo invocando al método repaint() que tiene el panel. Cuando invocas a este método, estás pidiendo al panel que se vuelva a dibujar. Como parte de dicha operación de dibujo el panel volverá a invocar al método paintComponent() que has programado, que debe hacer de nuevo el dibujo teniendo en cuenta la nueva posición del cursor.

Apartado 3

Ahora queremos que, cada vez que el usuario pulse el botón izquierdo del ratón, el programa conmute entre dos modos. En el primer modo hace seguimiento del ratón como en el apartado anterior. En el segundo modo no hace seguimiento del ratón, y las pupilas se colocan en posición de reposo en el centro del ojo.

Para detectar cuándo el usuario presiona un botón del ratón debes hacer que tu panel también ejerza de MouseListener para él mismo.

Apartado 4

Ahora queremos que, cuando el cursor abandone el área del panel, el muñeco muestre una cara de enfado como se ve en la imagen siguiente, y que recupere la sonrisa cuando el cursor vuelva a entrar. Examina la API de MouseListener para saber cómo detectar cuándo entra o sale el cursor.

Soluciones

import java.awt.Dimension;

import javax.swing.JFrame;

public class EyesApplication extends JFrame {

    public EyesApplication() {
        super("Eyes");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        getContentPane().add(new EyesPanel(new Dimension(640, 480)));
        pack();
        setVisible(true);
    }

    public static void main(String[] args) {
        //Schedule a job for the event-dispatching thread:
        //creating and showing this application's GUI.
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    EyesApplication gui = new EyesApplication();
                }
            });
    }
}
import java.awt.Dimension;
import java.awt.Graphics;

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

import javax.swing.JPanel;


public class EyesPanel extends JPanel
    implements MouseListener, MouseMotionListener {
    private Dimension preferredSize;
    private Eye leftEye;
    private Eye rightEye;
    private Mouth mouth;
    private int cursorX;
    private int cursorY;
    private boolean track;
    private boolean happy;

    public EyesPanel(Dimension preferredSize) {
        this.preferredSize = preferredSize;
        int centerX = (int) preferredSize.getWidth() / 2;
        int centerY = (int) preferredSize.getHeight() / 2;
        leftEye = new Eye(centerX - 80, centerY, 60, 30, 10);
        rightEye = new Eye(centerX + 80, centerY, 60, 30, 10);
        mouth = new Mouth(centerX, centerY + 100, 260, 30);
        cursorX = centerX;
        cursorY = centerY;
        track = true;
        happy = false;
        addMouseListener(this);
        addMouseMotionListener(this);
    }

    public Dimension getPreferredSize() {
        return preferredSize;
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        leftEye.draw(g, cursorX, cursorY, track);
        rightEye.draw(g, cursorX, cursorY, track);
        mouth.draw(g, happy);
    }

    public void mouseClicked(MouseEvent e) {
        if (e.getButton() == MouseEvent.BUTTON1) {
            track = !track;
            repaint();
        }
    }

    public void mouseEntered(MouseEvent e) {
        happy = true;
        repaint();
    }

    public void mouseExited(MouseEvent e) {
        happy = false;
        repaint();
    }

    public void mousePressed(MouseEvent e) {
        // Nothing to do
    }

    public void mouseReleased(MouseEvent e) {
        // Nothing to do
    }

    public void mouseMoved(MouseEvent e) {
        if (track) {
            cursorX = e.getX();
            cursorY = e.getY();
            repaint();
        }
    }

    public void mouseDragged(MouseEvent e) {
        mouseMoved(e);
    }


}
import java.awt.Color;
import java.awt.Graphics;


public class Eye {
    private int centerX;
    private int centerY;
    private int outerRadius;
    private int innerRadius;
    private int pupilRadius;

    public Eye(int centerX, int centerY, int outerRadius, int innerRadius,
               int pupilRadius) {
        this.centerX = centerX;
        this.centerY = centerY;
        this.outerRadius = outerRadius;
        this.innerRadius = innerRadius;
        this.pupilRadius = pupilRadius;
    }

    public void draw(Graphics g, int cursorX, int cursorY, boolean track) {
        // Draw the outer circle of the eye
        drawCircle(g, centerX, centerY, outerRadius, Color.WHITE, true);
        drawCircle(g, centerX, centerY, outerRadius, Color.BLACK, false);

        // Draw the pupil of the eye
        if (track) {
            int cursorRelX = cursorX - centerX;
            int cursorRelY = cursorY - centerY;
            double hypotenuse1 = Math.sqrt(cursorRelX * cursorRelX
                                           + cursorRelY * cursorRelY);
            double ratio = hypotenuse1 / innerRadius;
            int eyeRelX = (int) (cursorRelX / ratio);
            int eyeRelY = (int) (cursorRelY / ratio);
            drawCircle(g, centerX + eyeRelX, centerY + eyeRelY,
                       pupilRadius, Color.BLACK, true);
        } else {
            drawCircle(g, centerX, centerY, pupilRadius, Color.BLACK, true);
        }
    }

    private void drawCircle(Graphics g, int x, int y, int radius, Color color,
                            boolean filled) {
        int posX = x - radius;
        int posY = y - radius;
        g.setColor(color);
        if (filled) {
            g.fillOval(posX, posY, 2 * radius, 2 * radius);
        } else {
            g.drawOval(posX, posY, 2 * radius, 2 * radius);
        }
    }
}
import java.awt.Color;
import java.awt.Graphics;


public class Mouth {
    private int posX;
    private int posY;
    private int width;
    private int height;

    public Mouth(int posX, int posY, int width, int height) {
        this.posX = posX;
        this.posY = posY;
        this.width = width;
        this.height = height;
    }

    public void draw(Graphics g, boolean happy) {
        if (happy) {
            g.setColor(Color.WHITE);
            g.fillPolygon(new int[] {posX - width / 2, posX + width / 2, posX},
                          new int[] {posY, posY, posY + height}, 3);
            g.setColor(Color.BLACK);
            g.drawPolygon(new int[] {posX - width / 2, posX + width / 2, posX},
                          new int[] {posY, posY, posY + height}, 3);
        } else {
            g.setColor(Color.BLACK);
            g.drawLine(posX - width / 2, posY, posX + width / 2, posY);
        }
    }
}

Homework Section2. Actividades para casa

Exercise Section2.1. Tres En Raya

En este ejercicio vamos a utilizar un juego conocido, el "Tres en Raya", como ejemplo de Interfaz Gráfica de Usuario (GUI) que integra la correspondiente Gestión de Eventos.

Apartado 1

Implementa en Java el tablero de un tres en raya, tal y como se presenta en la figura. Para ello crea una clase TicTacToe y haz uso de las clases JFrame, JPanel , JLabel y JButton del paquete javax.swing y de las clases BorderLayout y GridLayout del paquete java.awt. Incluye un atributo denominado player en tu clase para mantener el turno del jugador e implementa los métodos get y set correspondientes. Finalmente, implementa un método main que te permita probar esta clase

Apartado 2

Implementa en Java una clase TicTacToeListener que recoja los eventos de los usuarios al pulsar los botones que forman el tablero del tres en raya. Esta clase implementará la interfaz ActionListener y tendrá dos atributos, la instancia de la clase principal TicTacToe a usar, porque necesitarás acceder a sus métodos desde el escuchador, y la instancia de la etiqueta en que se indica quién es el siguiente jugador. El constructor de esta clase se encargará de inicializar estos dos atributos.

El método actionPerformed que tendrás que implementar en esta clase debe incluir la lógica siguiente:

  • Deshabilitar el botón que fue pulsado

  • Mostrar una "X" si el jugador 1 pulsó el botón y una "O" si el jugador 2 pulsó el botón.

  • Actualizar el valor de player para cambiar el turno de juego

  • Actualizar la etiqueta del tablero para mostrar el jugador que tiene el turno y su símbolo

Apartado 3

Implementa en la clase TicTacToe el método boolean isWinner(String value) que compruebe si un jugador ha ganado a partir del valor de su símbolo. Ten en cuenta todas las posibles combinaciones del tres en raya para que exista un ganador.

Invoca a este método desde actionPerformed. En caso de que algún usuario sea ganador muestra un mensaje por pantalla utilizando la clase JOptionPane.

Soluciones

import java.awt.BorderLayout;
import java.awt.GridLayout;

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

public class TicTacToe extends JFrame {

    private boolean player = false;
    JButton[] buttons = new JButton[9];			
	
    public TicTacToe() {
        super("TicTacToe");
        getContentPane().setLayout(new BorderLayout());		
        JLabel display = new JLabel("Player 1: X");
        JPanel buttonsPanel = new JPanel(new GridLayout(3, 3));
        for (int i = 0; i < 9; i++){
            buttons[i] = new JButton();
            buttonsPanel.add(buttons[i]);
            buttons[i].addActionListener(new TicTacToeListener(this, display));
        }
        getContentPane().add(display, BorderLayout.NORTH);
        getContentPane().add(buttonsPanel, BorderLayout.CENTER);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(320,240);
        setVisible(true);
    }
		
    public boolean getPlayer(){
        return player;
    }
	
    public void setPlayer(boolean player){
        this.player = player;
    }	

    /**
     * Calculates if a player is a winner checking the eight possible ways
     * of being a winner in tictactoe
     * @param value. The value representing the player ("X") or ("O")
     * @return If the player represented by the value is a winner
     * 
     */
    
    public boolean isWinner(String value){
        if (buttons[0].getText().equals(value) && buttons[1].getText().equals(value)
            && buttons[2].getText().equals(value)){
            return true;
        } else if (buttons[3].getText().equals(value) && buttons[4].getText().equals(value)
            && buttons[5].getText().equals(value)){
           return true;
        } else if (buttons[6].getText().equals(value) && buttons[7].getText().equals(value)
            && buttons[8].getText().equals(value)){
           return true;
        } else if (buttons[0].getText().equals(value) && buttons[3].getText().equals(value)
            && buttons[6].getText().equals(value)){
           return true;
        } else if (buttons[1].getText().equals(value) && buttons[4].getText().equals(value)
            && buttons[7].getText().equals(value)){
           return true;
        } else if (buttons[2].getText().equals(value) && buttons[5].getText().equals(value)
            && buttons[8].getText().equals(value)){
           return true;
        } else if (buttons[0].getText().equals(value) && buttons[4].getText().equals(value)
            && buttons[8].getText().equals(value)){
           return true;
        } else if (buttons[2].getText().equals(value) && buttons[4].getText().equals(value)
            && buttons[6].getText().equals(value)){
           return true;
        } else {
           return false;
        }	
    }
	
    public static void main(String[] args) {
        //Schedule a job for the event-dispatching thread:
        //creating and showing this application's GUI.
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                TicTacToe gui = new TicTacToe();
            }
        });
    }
}
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;

public class TicTacToeListener implements ActionListener {

    private TicTacToe t;
    private JLabel display;
	
    public TicTacToeListener(TicTacToe t, JLabel display){
        this.t = t;
        this.display = display;	
    }
	
    public void actionPerformed(ActionEvent e){
        JButton clickedButton = (JButton) e.getSource(); 
        clickedButton.setEnabled(false);
        clickedButton.getParent();
        if (t.getPlayer() == false){
            clickedButton.setText("X");
            t.setPlayer(true);
            display.setText("Player 2: O");
            if (t.isWinner("X")){
            JOptionPane.showMessageDialog(t, "Player 1 wins");
            }
        } else {
            clickedButton.setText("O");
            t.setPlayer(false);
            display.setText("Player 1: X");			
            if (t.isWinner("O")){
                JOptionPane.showMessageDialog(t, "Player 2 wins");
            }			
        }
    }
}