Cap. 10 - Eventos
Muchos programas y dispositivos como los teléfonos celulares responden a eventos - cosas que ocurren.
- Por ejemplo, si mueves el mouse, la computadora responde.
- O si haces click en un botón, el programa hace algo como respuesta.
En este capítulo veremos muy brevemente cómo funciona la programación orientada a eventos.
10.1) Eventos de teclado (keypress)
Aquí hay un programa con ciertas funcionalidades. Cópialo en tu espacio de trabajo y ejecútalo. Cuando se abra la ventana, presiona las teclas y la tortuga tess se moverá!
import turtle turtle.setup(400,500) # Determino el tamaño de la ventana wn = turtle.Screen() # Obtengo una referencia a la ventana wn.title("Detectando acciones del teclado!") # Modifico el título de la ventana wn.bgcolor("lightgreen") # Color de fondo tess = turtle.Turtle() # Creamos una tortuga # Las próximas 4 funciones son nuestros "manejadores de eventos" def h1(): tess.forward(30) def h2(): tess.left(45) def h3(): tess.right(45) def h4(): wn.bye() # Cerramos la ventana # Estas líneas conectan las teclas concretas presionadas con los manejadores de eventos que definimos antes wn.onkey(h1, "Up") wn.onkey(h2, "Left") wn.onkey(h3, "Right") wn.onkey(h4, "q") # Ahora le decimos a la ventana comience a oír eventos, # Si cualquiera de las teclas anteriores son presionadas, # el evento será "oído" y su correspondiente manejador será llamado wn.listen() wn.mainloop()
Algunas observaciones:
- Necesitamos el llamado al método listen de la ventana, pues de lo contrario ignoraría las teclas presionadas en el teclado.
- Nombramos a nuestras funciones manejadoras de eventos h1, h2, etc., pero podemos elegir mejores nombres. Los manejadores pueden ser funciones de complejidad arbitraria, pueden llamar a otras funciones, etc.
- Cuando el usuario presione la tecla q el programa llamará al manejador h4 (porque ligamos la tecla "q" a la función h4). Mientras se ejecute h4, el método bye de la ventana cerrará a la ventana de la tortuga, lo que hará a su vez que el llamado a mainloop de la línea final se dé por terminado. Como no escribirmos más código después de esa línea, esto hará que el programa termine por completo.
- Preguntas: Si hubiera código después de la mainloop éste se ejecutaría al cerrarla? y de ser así, ¿se pueden utilizar varias mainloops en el mismo programa? (revisarlo y responder)
- Podemos referirnos a teclas del teclado por su código de carácter (como hicimos en el caso "q") o por su nombre simbólico. Algunos de los nombres simbólicos que se pueden probar son Cancel (la tecla Break) BackSpace, Tab, Return (la tecla Enter), Shift_L (cualquier tecla Shift), Control_L (cualquier tecla Control), Alt_L (cualquier tecla Alt), Pause, Caps_Lock, Escape, Prior (Page Up), Next (Page Down), End, Home, Left, Up, Right, Down, Print, Insert, Delete, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, Num_Lock y Scroll_Lock.
10.2) Eventos del mouse
Un evento del mouse es un poco distinto de uno de teclado porque su manejador necesita dos parámetros para recibir las coordenadas x, y indicando dónde estaba el mouse cuando ocurrió el evento.
import turtle turtle.setup(400,500) wn = turtle.Screen() wn.title("Cómo manejar clicks del mouse en la ventana!") wn.bgcolor("lightgreen") tess = turtle.Turtle() tess.color("purple") tess.pensize(3) tess.shape("circle") def h1(x, y): tess.goto(x, y) wn.onclick(h1) # Responder a un click en la ventana. wn.mainloop()
Aquí usamos por primera vez el método goto de la tortuga, que permite moverla hacia una posición de coordenadas absolutas (en casi todos los ejemplos anteriores móviamos la tortuga relativamente a su posición actual). Entonces lo que este programa hace es mover la tortuga (y dibujar una línea al hacerlo) hacia donde se haga click con el mouse. Si no lo has probado es tiempo de hacerlo!
Si agregamos esta línea como la primera del cuerpo de h1, aprenderemos un útil truco de debugging:
wn.title("Hubo un click en las coordenadas {0}, {1}".format(x, y))
Dado que es fácil cambiar el texto en la barra de título (title bar) de la ventana, éste resulta un lugar práctico para mostrar información de debugging o del status actual del programa. (Por supuesto, éste no es el propósito de la barra del título) (y usarla de esta forma es sólo un truco recomendable para la fase de desarrollo, no para el momento en que uno llegó a la versión final)
Pero hay más!
No sólo la ventana puede recibir eventos del mouse, también cada tortuga puede tener manejadores propios de eventos del mouse. Así que crearemos dos tortugas, cada una con un manejador ligado a su propio evento onclick. Y los dos manejadores pueden hacer cosas distintas para sus respectivas tortugas:
import turtle turtle.setup(400,500) # Determinar el tamaño de ventana wn = turtle.Screen() # Obtener una referencia a la ventana wn.title("Evento click con 2 tortugas!") # Cambiar el título de la ventana wn.bgcolor("lightgreen") # Establecer el color de fondo tess = turtle.Turtle() # Crear dos tortugas tess.color("purple") alex = turtle.Turtle() alex.color("blue") alex.forward(100) # Separarlas entre sí def handler_para_tess(x, y): wn.title("Tess responde al click en {0}, {1}".format(x, y)) tess.left(42) tess.forward(30) def handler_para_alex(x, y): wn.title("Alex responde al click en {0}, {1}".format(x, y)) alex.right(84) alex.forward(50) tess.onclick(handler_para_tess) alex.onclick(handler_para_alex) wn.mainloop()
Ejecútalo y fíjate qué sucede si haces click sobre las tortugas!
10.3) Eventos automáticos desde un timer
Los relojes despertadores, temporizadores de cocina, y contadores de cuentas regresivas para bombas y explosivos en algunas películas están diseñados para crear un evento "automático" tras cierto intervalo de tiempo. El módulo turtle de Python tiene un timer que puede causar un evento cuando pasó cierta cantidad de tiempo.
import turtle turtle.setup(400,500) wn = turtle.Screen() wn.title("Probando un timer") wn.bgcolor("lightgreen") tess = turtle.Turtle() tess.color("purple") tess.pensize(3) def h1(): tess.forward(100) tess.left(56) wn.ontimer(h1, 2000) wn.mainloop()
El timer es inicializado en el llamado al método ontimer de la ventana, seteado para responder en 2 segundos (2000 milisegundos). Cuando ocurre el evento, el manejador h1 es llamado y tess se mueve 100 pasos hacia la izquierda (tras lo cual rota 56°).
Cuando se setea un timer éste sólo se ejecuta una vez. Así que es habitual reiniciar el timer desde el propio handler, para habilitar así a que se disparen nuevos eventos. Esto se ilustra en el siguiente programa:
import turtle turtle.setup(400,500) wn = turtle.Screen() wn.title("Probando un timer") wn.bgcolor("lightgreen") tess = turtle.Turtle() tess.color("purple") tess.pensize(3) def h1(): tess.forward(100) tess.left(56) wn.ontimer(h1, 60) h1() wn.mainloop()
10.4) Un ejemplo: máquinas de estado
Una máquina de estado es un sistema que puede estar en uno de un pequeño conjunto de estados. Dibujamos un diagrama de estados para representar la máquina, en que cada estado es dibujado en un círculo o elipse. Ciertos eventos ocurren que hace que el sistema deje un estado y transite hacia uno diferente. Estas transiciones de estados se dibujan habitualmente como flechas en el diagrama.
Esta idea no es nueva. Cuando prendemos un celular por primera vez, va hacia un estado que podríamos llamar "esperando PIN". Cuando se ingresa el PIN correcto, va hacia un estado diferente (digamos: "Ready"). Luego podemos bloquear el teléfono, y entraría en un estado "Locked", etc.
- Otros ejemplos habituales: los estados de un mensaje en Watsapp, los estados de una persona en Watsapp, los estados de un envío por MercadoLibre, los estados del tiempo (soleado, lluvioso, etc.)
Una máquina de estado simple que encontramos en la vida cotidiana es un semáforo (luces de tráfico). Aquí hay un diagrama de estados que muestra que la máquina circula continuamente a través de tres diferentes estados, que enumeramos como 0, 1 y 2.
Construiremos un programa que usa la tortuga para simular las luces de tráfico. Esta tarea nos dará 3 lecciones importantes:
- La primera: mostrar cómo hay múltiples maneras de usar a las tortugas.
- La segunda: mostrar cómo se puede programar una máquina de estados en Python, mediante una variable que guarda el estado actual y una serie de sentencias if para chequear el estado actual y realizar las acciones necesarias en caso de que se cambie de estado.
- La tercera: usar eventos del teclado para disparar (trigger) los cambios de estado.
Copia y ejecuta este programa. Asegúrate de comprender lo que hace cada línea, consultando la documentación en caso de ser necesario.
import turtle # La tortuga tess se convierte en unas luces de tráfico turtle.setup(400,500) wn = turtle.Screen() wn.title("Tess se convierte en un semáforo!") wn.bgcolor("lightgreen") tess = turtle.Turtle() def dibujar_semaforo(): """ Dibujar un recuadro que haga del cuerpo del semáforo """ tess.pensize(3) tess.color("black", "darkgrey") tess.begin_fill() tess.forward(80) tess.left(90) tess.forward(200) tess.circle(40, 180) tess.forward(200) tess.left(90) tess.end_fill() dibujar_semaforo() tess.penup() # Ubicar a tess en el lugar en que debería ir la luz verde tess.forward(40) tess.left(90) tess.forward(50) # Convertir a tess en un círculo verde grande tess.shape("circle") tess.shapesize(3) tess.fillcolor("green") # Las luces de tráfico son un tipo de máquina de estado con tres estados, # Verde, Amarillo y Rojo. Nombramos a estos estados 0, 1 y 2 # Cuando la máquina cambia de estado, cambiamos la posición de tess # y su color. # Esta variable lleva la cuenta del estado actual de la máquina state_num = 0 def avanzar_estado_de_la_maquina(): global state_num if state_num == 0: # Transición del estado 0 al estado 1 tess.forward(70) tess.fillcolor("orange") state_num = 1 elif state_num == 1: # Transición del estado 1 al estado 2 tess.forward(70) tess.fillcolor("red") state_num = 2 else: # Transición del estado 2 al estado 0 tess.back(140) tess.fillcolor("green") state_num = 0 # Asociar el manejador de estados a la tecla barra espaciadora wn.onkey(avanzar_estado_de_la_maquina, "space") wn.listen() # Escuchar eventos wn.mainloop()
La novedad en cuanto a instrucciones de Python está en la primera línea de la función avanzar_estado_de_la_maquina, en la que utilizamos la palabra clave global. Ésta indica a Python que la variable estado_actual no debe ser local (a pesar de que la función la utilizará para hacer chequeos y asignaciones en varios puntos del código), sino que siempre ha de ser considerada como una referencia a la variable de ese mismo nombre que había sido definida antes de la definición de avanzar_maquina_de_estado.
Lo que hace el código de esta función es avanzar desde del estado actual hacia el próximo. Al cambiar de estado se mueve a tess hacia su nueva posición, se le cambia el color y se le asigna a estado_actual el número del nuevo estado que acabamos de ingresar.
Cada vez que el usuario presione la barra espaciadora, el manejador de eventos hará que la máquina de estados de tres luces pase a su nuevo estado.
10.5) Glosario
- ligar (bind) (ligar o asociar una función a un evento es establecer que dicha función será ejecutada cada vez que el evento en cuestión ocurra)
- evento (algo que ocurre "por fuera" del flujo normal de nuestro programa, habitualmente a raíz de alguna acción del usuario - los casos típicos son acciones con el mouse o el teclado, pero también hemos visto que un temporizador puede utilizarse para crear eventos)
- manejador (función que es llamada como respuesta a un evento)
10.6) Ejercicios
1) Agrega algunos manejadores de teclado al programa de la sección 10.1: (hacerlo)
- Al presionar R, G o B, tess debería cambiar de color a Rojo, Verde o Azul.
- Al presionar las teclas + o - se debería aumentar o disminuir el ancho del lápiz de tess. Asegurando que el tamaño permanezca entre 1 y 20 (inclusive).
- Utiliza más teclas para cambiar otros atributos de tess, o atributos de la ventana, o para darle a la tortuga algún nuevo comportamiento que pueda controlarse desde el teclado.
2) Cambiar el programa de las luces de tráfico para que el cambio ocurra automáticamente, según vaya pasando el tiempo. (hacerlo)
3) En un capítulo anterior vimos dos métodos, hideturtle y showturtle que permiten ocultar o hacer visible a una tortuga. Esto sugiere que podríamos implementar las luces de tráfico de un modo distinto. Modifica el programa para que cree 3 tortugas distintas para cada una de las luces, y en vez de mover a tess a diferentes posiciones, cambiando su color a cada paso, simplemente haga que una de las 3 tortugas sea visible cada vez, y haga invisibles a las otras dos. Una vez que hayas hecho los cambios, detente un momento y medita sobre ambos programas: si bien ambos parecen hacer lo mismo, ¿uno de los dos métodos es preferible al otro? ¿cuál de los dos se parece más a lo que ocurre en un semáforo real? (hacerlo)
4) Ahora que ya tenemos una tortuga para cada luz de tráfico, cabe pensar que tal vez no fue una buena idea lo de la visibilidad/invisibilidad. Si miramos las luces de tráfico reales, se prenden y apagan - pero cuando se apagan siguen ahí, sólo que con un color oscuro. Modifica el programa para que las luces no desaparezcan, sino que simplemente estén prendidas o apagadas (siguen visibles cuando están apagadas) (hacerlo)
5) Tu programa controlador de luces de tráfico fue patentado, y estás a punto de hacerte rico. Pero tu nuevo cliente necesita un ajuste. Quieren 4 estados en su máquina de estado. Verde, luego Verde y Amarillo juntos, luego Amarillo y luego Rojo. Adicionalmente, quiere que se pase una distinta cantidad de tiempo en cada estado. La máquina debería estar 3 segundos en el estado Verde, luego 1 segundo en el estado Verde+Amarillo, luego 1 segundo en el estado Amarillo y por último 2 segundos en el estado Rojo. Modifica la lógica de tu máquina de estado para reflejarlo. (hacerlo)
6) Si no sabes cómo se computan los puntos en un partido de tenis, pregúntale a un amigo o revisa Wikipedia. Un game de tenis entre un jugador A y uno B siempre tiene un score. Queremos pensar en el "estado del score" como una máquina de estados. El juego empieza en el estado (0, 0), significando que ninguno de los jugadores tiene ningún puntaje todavía. Asumiremos que el primer elemento en este par es el puntaje del jugador A. Si el jugador A gana el primer punto, el score pasa a ser (15, 0). Si en cambio B gana el primer punto, el score pasa a ser (0, 15). En lo que sigue hay unos pocos estados y transiciones para un diagrama de estados. En este diagrama, cada estado tiene dos posibles salidas (según que sea A o B quien gane el siguiente punto), y la flecha de más arriba es siempre la transición que ocurre cuando A gana el punto. Completar el diagrama, mostrando todas las transiciones y todos los estados. (hacerlo
- Pista: hay 20 estados, se se incluye el estado "duece", los estados "ventaja" y los estados "A gana" y "B gana" en el diagrama.
- Observación: Por lo que dice la letra, cualquier llegada a 60 es "A gana" o "B gana", es decir, no distinguimos entre (60, 0), (60, 15) o (60, 30), todos son "A gana" - de lo contrario habrían más de 20 estados.