Práctica 4. Interfaces Gráficas de Usuario Duración 1 sesión Índice 1.- Programación dirigida por eventos 2.- Ejemplo. Una calculadora sencilla. 3.- Bibliografía. 1.- Programación dirigida por eventos. La interacción con un programa a través de una interfaz gráfica, simplifica la labor del usuario, pero complica la del programador. En su implementación podemos distinguir dos tareas fundamentales: diseño y programación. El diseño supone determinar la cantidad de componentes, su agrupación y su ubicación en el espacio, mientras que la programación supone determinar las acciones que se llevarán a cabo cuando el usuario interactúe con cada componente. El modelo utilizado hasta el momento, entrada/salida por consola, establece una línea de ejecución secuencial. El programa empieza con la primera instrucción del método main y secuencialmente se van ejecutando una tras otra sus instrucciones. Si alguna es de salida, por ejemplo System.out.println (“hola”), el programa muestra en la consola el texto “hola”. Si se trata de una instrucción de entrada el programa espera hasta que el usuario haya introducido el dato demandado, y una vez introducido el programa sigue su ejecución. El modelo de entrada/salida orientado a interfaz gráfica modifica ligeramente la idea de secuencialidad. Cuando ejecutamos un programa en modo gráfico (todos somos usuarios de aplicaciones con interfaz gráfica), el programa no esta esperando a que introduzcamos un número o una frase, sino que gran cantidad de posibilidades se nos brindan, una por cada componente de la interfaz. Consecuentemente a priori el programa no sabe que acción va realizar el usuario. Por lo tanto, se tiene que programar cada una de las acciones sin asumir a priori ninguna ordenación temporal de las acciones. Para entender los conceptos subyacentes al diseño y programación de interfaces gráficas veamos un ejemplo sencillo. Supongamos un programa que presenta una interfaz gráfica con dos botones y un campo de texto ( área para introducir y visualizar texto). De forma que si el usuario pulsa el botón etiquetado con el número uno, en el campo de texto aparece la frase “hola”, y si pulsa el botón etiquetado con el número dos, en el campo de texto aparece la palabra “adiós”. En Java dicha interfaz tendría el siguiente aspecto: Ahora vamos a analizar cada uno de los elementos que vemos en la interfaz así como las acciones asociadas. El marco principal La interfaz del programa esta delimitada por un marco o ventana, que en su parte superior izquierda viene etiquetada con el texto “saludos”, y en su parte superior derecha por los habituales botones de minimizar, maximizar y cerrar . En Java existe una clase denominada Frame de la librería java.awt , que representa un marco en su forma básica. En la librería java.awt, se encuentran la mayoría de las componentes de interfaz. Un marco es un contenedor de componentes que a su vez puede contener otros contenedores. En nuestro ejemplo, todas las componentes de la interfaz, están dentro de un marco. Un esqueleto de una clase que representa un marco principal de aplicación es: class Igu extends Frame { // Los atributos pueden ser componentes de la interfaz como //botones y campos de texto // En el constructor podemos tener un parámetro que sea el //título que le queremos dar al marco, dicho parámetro se lo //pasaremos al constructor Igu (String nombre) { }} super(nombre); // a continuación se definen y añaden las componentes de la //interfaz Hay que tener en cuenta que la componente Frame, tiene programados los botones de minimización y maximización pero no el de cierre (su programación la veremos en el ejemplo del apartado siguiente). Las componentes del marco Siguiendo con el ejemplo, dentro del marco principal tenemos dos botones y un campo de texto. Al igual que ocurre con los marcos, existen clases predefinidas en Java que representan la forma básica de una componente. En general para cada componente de interfaz que estáis acostumbrados a utilizar existe una clase predefinida en Java. La clase Button representa los botones y la clase TextField los campos de texto, ambas de la librería java.awt. Cuando se necesite tener un botón o campo de texto muy personalizado se puede diseñar una clase que extienda de Button o TextField. El código para definir uno de los botones del ejemplo es: Button hola= new Button(“1”); donde el “1” indica el texto que aparecerá dentro del botón. Un vez creada una componente se puede modificar su apariencia utilizando los métodos apropiados. Para poder visualizar una componente tendremos que añadirla a un contenedor, por ejemplo a un marco, y luego hacer visible el contenedor. Para añadir una componente a un contenedor hay que invocar el método add del contenedor. Por ejemplo: add (hola); que añade el botón “hola” al marco. El código en Java para diseñar la interfaz del ejemplo es: import java.awt.*; class Igu extends Frame { // Los atributos Button hola, adios; TextField campo_texto; Igu (String nombre) { super(nombre); hola= new Button(“1”); adios= new Button(“2”); campo_texto= new TextField(12); add(hola); add(adios); add(campo_texto); }} class Principal { // Los atributos static void main (String []args) { Igu interfaz=new Igu(“Saludos”); // colocamos el tamaño inicial de la ventana interfaz.setSize(500,500); // y la visualizamos interfaz.setVisible(true);}} La clase Principal se usa para lanzar a ejecución la interfaz gráfica. Crea un objeto de la clase Igu, establece su tamaño inicial y lo visualiza. Ubicación de las componentes en el marco Hasta el momento hemos visto como añadir componentes a la ventana o marco principal, pero hemos pasado por alto su ubicación dentro del espacio. Esto es, cada vez que ejecutamos el método add del marco ¿dónde se coloca la componente?. En Java esto se resuelve asociando un diseñador del espacio al marco. Existen clases predefinidas que hacen la función de diseñadores, el más sencillo de todos, es el FlowLayout que coloca las componentes de izquierda a derecha y de arriba abajo según el orden de ejecución del método add. El siguiente código añade un diseñador a un contenedor: FlowLayout diseñador= new FlowLayout(); setLayout(diseñador); Este código se tiene que poner antes de las instrucciones add del contendor. De momento sólo nos hemos centrado en la parte visual de la interfaz no en la programación de las componentes. De manera que si implementamos y ejecutamos el código descrito, la interfaz no responde a casi nada. Dispositivos físicos de interacción con la interfaz y eventos Cuando el usuario utiliza el ratón, el teclado o cualquier otros dispositivo físico para interaccionar con las componentes de una interfaz gráfica, la máquina “virtual” (en ejecución) produce un evento que representa dicha interacción, o en términos Java se crea un objeto de la clase AWTEvent en la librería java.awt.event. Por ejemplo, si el usuario presiona un botón, el evento es de la clase ActionEvent, subclase de AWTEvent; si el usuario cierra un marco, el evento es de la clase WindowEvent; si el usuario toca el botón del ratón, el evento es de la clase MouseEvent; etc. Además del tipo de interacción, un objeto evento almacena datos como la posición de pantalla donde se produjo (para consultarla, getX() y getY()), y la componente de la interfaz involucrada o fuente del evento (para consultarla, getSource()). Tratamiento de los eventos Cuando se produce una interacción pero no se ha programado nada para tratarla, simplemente se ignora. Es más, para que se llegue a crear un objeto evento la máquina virtual tiene que tener registrado un responsable de su tratamiento, i.e. una clase oyente. Para que una clase oyente tenga la capacidad de efectivamente tratar un tipo de evento dado, por ejemplo un ActionEvent, tiene que implementar la interfaz de escucha de dicho tipo de evento, por ejemplo ActionListener para eventos ActionEvent. Así, la clase oyente deberá implementar los distintos métodos del interfaz de escucha, por ejemplo implementará actionPerformed que es él único método del interfaz ActionListener indicando qué tratamiento se da a cada evento ActionEvent en función de su fuente. Siguiendo con el ejemplo, la clase Oyente responsable del botón ”hola” es: class Oyente implements ActionListener { // clase oyente de eventos de botón public void actionPerformed(ActionEvent e) { String res =”hola”; campo_texto.setText(res); }} Para que la clase Oyente pueda acceder al botón hola y al campo de texto campo_texto, se declara como clase interna de la clase Igu. Pese a que el concepto de clase interna no ha sido desarrollado en clase de teoría es fácil entender que su visibilidad esta restringido a la clase que la contiene y en este caso a la propia máquina virtual. De forma alternativa se podría otorgar funcionalidad de oyente a la propia clase Igu, si ésta implementara la interfaz ActionListener. En el apartado siguiente se presenta una solución que hace uso de esta estrategia. Una vez definida la clase Oyente, el código para añadirlo al marco es: Oyente oy=new Oyente(); hola.addActionListener(oy); De forma similar al botón hola, se realiza la programación del botón adiós. Como hemos comentado anteriormente, los botones de minimizar y maximizar del marco ya están programados. Sin embargo, el botón de cierre del marco no. Pese a que también se trata de un botón, su programación es un poco diferente al del resto de botones, puesto que el evento que se genera cuando se interactúa con él es WindowEvent. Para que una clase sea oyente de estos eventos tiene que implementar la interfaz WindowListener, que define 7 métodos. Como únicamente estamos interesados en programar el botón de cierre y el método que se ejecuta cuando se pulsa es windowClosing, sólo escribiremos código para ese método. Del resto de métodos sólo pondremos su cabecera. Para evitar esto último, la API de Java tiene unas clases denominadas adaptadores, que implementan las interfaces oyentes, dejando el cuerpo de los métodos vacíos. De manera que si una clase oyente la hereda no necesita poner la cabecera de los métodos que no quiere programar. En el ejemplo, el oyente del cierre del marco es: class Oyente_marco extends WindowAdapter implements WindowListener { / public void windowClosing(WindowEvent e) { dispose(); System.exit(0); }} Y el código para añadirlo el oyente de marco al marco es: Oyente_marco oy1=new Oyente_marco(); this.addWindowListener(oy1); 2.- Ejemplo. Una calculadora sencilla. Visualmente nuestra calculadora en su versión más básica tendrá el siguiente aspecto: Primera solución La primera solución del ejercicio se fundamenta en las siguientes directrices: • La lógica del programa esta fusionada con la programación de eventos • Una única clase Oyente interna que atienda los eventos de los dos botones. • El diseñador del marco es el FlowLayout. • No se realiza ningún tratamiento de errores debidos a la inserción de datos no numéricos. import java.awt.*; import java.awt.event.*; /** * La clase calculadora representa una calculadora básica. public class Calculadora extends Frame { // Label eti1,eti2,eti3; TextField t1,t2,t3; Button b1,b2; public Calculadora(String nombre) { super(nombre); // el gestionador de diseño es el BorderLayout setLayout(new FlowLayout()); // Añadimos las componentes propias de la interfaz eti1=new Label("primer elemento"); eti2=new Label("segundo elemento"); eti3=new Label("resultado"); t1=new TextField(12); t2=new TextField(12); t3=new TextField(12); b1= new Button("+"); b2= new Button("*"); Oyente oy=new Oyente(); add(eti1);add(t1); add(eti2);add(t2); add(eti3); add(t3); add(b1); b1.addActionListener(oy); add(b2); b2.addActionListener(oy); Oyente_marco oy1=new Oyente_marco(); this.addWindowListener(oy1); } class Oyente implements ActionListener { // clase oyente de evetos de raton public void actionPerformed(ActionEvent e) { String n1,n2,res; double num1, num2,result; n1=t1.getText(); num1=Double.parseDouble(n1); n2=t2.getText(); num2=Double.parseDouble(n2); if (e.getSource()==b1) result=num1+num2; else result=num1*num2; res=Double.toString(result); t3.setText(res); } } class Oyente_marco extends WindowAdapter implements WindowListener { public void windowClosing(WindowEvent e) { dispose(); System.exit(0); }} } Segunda solución La segunda solución del ejercicio se fundamenta en : • la lógica del programa se separa de la programación de eventos. De forma que aparece una nueva clase Calculo sobre la que se realizan las operaciones aritméticas. • La clase Igu asume el papel de oyente único para los dos botones. • No se realiza ningún tratamiento de errores debidos a la inserción de datos no numéricos. Por lo tanto aparecerá un clase Calculo que suma y multiplica los valores que se le pasan como argumentos. public class Calculo { public static double suma(double x, double y) { return x + y;} public static double producto(double x, double y) { return x * y;}} En la cabecera de la clase Igu tendremos que indicar que se va a implementar la interfaz ActionListener. La clase interna Oyente desaparece y el método actionPerformed se reescribe de la siguiente forma: public void actionPerformed(ActionEvent e) { String n1,n2,res; double num1, num2,result; n1=t1.getText(); num1=Double.parseDouble(n1); n2=t2.getText(); num2=Double.parseDouble(n2); if (e.getSource()==b1) result=Calculo.sumar(num1,num2); else result=Calculo.producto(num1,num2); res=Double.toString(result); t3.setText(res);} Pese a que la modificación parece muy leve en el contexto del problema, la aplicación sistemática del criterio de separar la lógica del programa de la programación de la interfaz, dará como resultado programas más claros, modulares y acordes con los principios de programación orientada a objetos. Tercera solución La tercera solución del ejercicio asume las directrices de la solución anterior y realiza el tratamiento de errores debidos a la inserción de datos no numéricos. Su aspecto será el siguiente: Como se observa en al imagen, se ha añadido una nueva componente para la visualización de los mensajes de error (provocados por la inserción incorrecta de datos). Además de algunos detalles que vienen comentados directamente en el código, el concepto más importante que presenta es la agrupación de componentes, a través del contenedor de componentes Panel, de manera que el diseño del marco se divide en el panel Norte (pNorte) y el panel Sur (pSur). En cada panel se añaden las componentes y luego el panel se añade al marco. El diseñador que se ha utilizado es el BorderLayout, que divide el espacio en 5 zonas: norte, sur, este y oeste. El código de esta solución está en :/labos/asignaturas/EI/eda/practica4. 3.- Bibliografía. Weiss. Estructuras de datos en Java. Apéndice D. Ed Addison-Wesley.