6: CONOSCERE GLI EVENTI

COME FUNZIONA UNA GUI

Per ora siamo riusciti a creare una finestra e a disegnare qualcosa su di essa. Per cominciare ad interagire con il nostro programma è necessario conoscere meglio il funzionamento di un'interfaccia grafica.

Windows, Mac, Linux o il nostro smartphone usano delle interfacce grafiche (in inglese GUI: Graphic User Interface) tra l'utente ed il computer: in un'applicazione moderna quasi tutta l'interazione tra l'uomo ed il computer avviene muovendo il mouse, cliccando su immagini, bottoni, menu, ecc.

Come funziona di fatto tutto questo? Bene, mentre noi lavoriamo al computer il sistema operativo controlla continuamente tutto quello che avviene: la tastiera, il cursore del mouse, i pulsanti del mouse, ecc. Ogni volta che accade qualcosa (muoviamo il mouse, facciamo clic, premiamo un tasto della tastiera, apriamo una nuova finestra...) esso genera un evento di sistema, cioè un messaggio che contiene informazioni su quello che è accaduto, e lo invia a tutte le applicazioni aperte in quel momento. Ogni applicazione che lo riceve può analizzarlo, capire che cosa è successo e comportarsi di conseguenza (ad esempio un clic sul bottone "Chiudi" provocherà la chiusura di una finestra, ecc). Poichè gli eventi possono susseguirsi anche in maniera molto rapida esiste una coda degli eventi per ogni applicazione, alla quale vengono spediti gli eventi in ordine di arrivo (come le persone all'ufficio postale); le applicazioni li esaminano uno dopo l'altro decidendo cosa fare (possono anche ignorare quelli che non interessano).

Ad esempio, un effetto ormai comune in tutte le applicazioni è l'evidenziazione delle varie icone quando il mouse vi passa sopra (lo faremo come esercizio nel prossimo capitolo). Ogni volta che il mouse si muove, il sistema operativo genera un evento che contiene le coordinate del puntatore e lo invia alle applicazioni aperte in quel momento. Il programma lo riceve, fa qualche calcolo e rileva se il puntatore è entrato nel rettangolo dove è disegnata l'icona; se la risposta è sì cambia l'immagine dell'icona con quella dell'icona evidenziata. Allo stesso modo, quando il puntatore esce dal rettangolo, cambia nuovamente l'immagine in quella normale.

GLI EVENTI IN PYGAME

Proprio trattando gli eventi possiamo capire l'utilità della programmazione ad oggetti: quando il nostro programma riceve un evento dal sistema operativo deve innanzitutto capire che cosa è successo (clic del mouse, tastiera, ecc.) ed in base a ciò avrà bisogno poi di altre informazioni (per un clic bisognerà sapere in che punto è avvenuto, per la tastiera quale tasto è stato premuto, etc.). In pygame esiste un oggetto apposito, l'oggetto Event, nel quale tutte queste informazioni sono "impacchettate" sotto forma di attributi.

L'attributo più importante di un Event è il type, che contiene il tipo dell'evento; i vari tipi sono identificati da delle costanti di pygame (cioè delle variabili predefinite in pygame stesso, scritte tutte in caratteri maiuscoli) con nomi che ricordano facilmente l'evento. Ad esempio:

MOUSEMOTION il mouse è stato mosso
MOUSEBUTTONDOWN clic del mouse (è stato premuto un bottone)
KEYDOWN un tasto della tastiera è stato premuto
KEYUP un tasto della tastiera è stato rilasciato
QUIT la finesta è stata chiusa (clic sull'icona della crocetta in alto a destra)

Quindi, ad esempio, se ev è una variabile di tipo Event, per conoscere il tipo di evento dovremo esaminare l'attributo ev.type; scrivendo if ev.type == MOUSEBUTTONDOWN: potremo capire se l'utente ha cliccato sulla nostra finestra. Se non capite che tipo di dato sono le variabili scritte tutte in maiuscolo potete consultare il mio approfondimento qui.

RICEVERE GLI EVENTI

Vediamo infine come fare per ricevere gli eventi dal sistema operativo. Il sottomodulo event di pygame contiene alcune funzioni per gestire gli eventi che il sistema operativo trasmette al nostro programma. Abbiamo detto che al nostro programma è assegnata una coda di eventi (per noi invisibile perchè gestita da Windows): la funzione pygame.event.get() prende gli eventi dalla coda, li trasforma in oggetti Event di pygame e ci restituisce una lista di Python che contiene i nostri Event in ordine cronologico. Ecco un esempio molto semplice dell'uso di questa funzione:

NUOVO PROGRAMMA: prova_eventi.py

import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))

# riceve tutti gli eventi arrivati dal sistema
eventlist = pygame.event.get()
for ev in eventlist:
    print("Ho ricevuto l'evento", ev)
      
# fine del ciclo e termine del programma
input("Premi INVIO per terminare il programma")
pygame.quit()

Nella riga #7 chiamiamo la get(): il suo risultato è la lista degli oggetti Event arrivati da Windows e viene assegnato alla variabile eventlist; nella #8 facciamo un ciclo su di essa stampando sullo schermo ogni evento.

Se lanciamo il programma vedremo nella finestra di IDLE alcune stringhe piuttosto lunghe e non facilmente comprensibili: quando chiediamo a Python di "stampare" un Event il risultato è una lunga stringa che contiene i valori degli attributi. Proprio all'inizio possiamo riconoscere il tipo dell'evento ricevuto (VideoExpose: l'apertura della finestra, etc.).

L'esempio precedente si limita a prelevare da Windows i primi quattro-cinque eventi, dopodichè il ciclo termina e non potremo fare altro che chiudere la finestra. Inserendo a sua volta la get() in un ciclo possiamo fare in modo che il programma riceva continuamente gli eventi da Windows. Modificate il programma così:


import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))

while 1:
    # riceve tutti gli eventi arrivati dal sistema
    eventlist = pygame.event.get()
    for ev in eventlist:
        print("Ho ricevuto l'evento", ev)
      
# fine del ciclo e termine del programma
input("Premi INVIO per terminare il programma")
pygame.quit()

Abbiamo inserito (in #6) il nostro ciclo all'interno di un altro ciclo while: ora ogni volta che gli eventi sono finiti la get() viene chiamata nuovamente (la funzione cancella i vecchi eventi nella coda di Windows, cosicchè alla chiamata successiva troverà solo i nuovi eventi arrivati nel frattempo). Se lanciate ora il programma potete divertirvi a muovere il mouse (dentro la finestra di pygame), fare click, premere tasti della tastiera, e vedrete che il vostro programma si "accorgerà" di tutto quello che fate!

Ma c'è ancora un problema: così com'è il nostro programma non terminerà mai! Infatti, come saprete, il ciclo while 1: è un ciclo senza fine, ed in realtà non arriveremo mai alle ultime righe del programma (potreste anche cancellarle). Vedremo nel prossimo paragrafo la soluzione generalmente adottata in tutti i programmi per pygame, che ci permetterà anche di chiudere il programma direttamente cliccando la casella con la crocetta sulla finestra.

L'EVENTO QUIT

E' l'evento che viene generato quando si clicca sulla crocetta in alto a destra di una finestra, che dovrebbe provocare la sua chiusura. Ora possiamo fare in modo che esso provochi l'uscita dal programma. Modificate il programma così:


import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))

done = False
# si esce dal ciclo quando done diventa True
while not done:
    # riceve tutti gli eventi arrivati dal sistema
    eventlist = pygame.event.get()
    for ev in eventlist:
        print("Ho ricevuto l'evento", ev)
        # se l'evento e' la fine del programma pone done = True
        if ev.type == pygame.QUIT:
            done = True
      
# fine del ciclo e termine del programma
pygame.quit()

Per gestire l'uscita dal ciclo usiamo la variabile done: nella riga #6 la poniamo uguale a False e nella #8 facciamo ripetere il ciclo while finchè rimarrà tale. All'interno del ciclo abbiamo aggiunto alcune righe: nella #14 controlliamo se l'evento è un QUIT, e, se questo è vero, poniamo done = True provocando l'uscita dal ciclo all'iterazione successiva. Ricordate che in Python un ciclo finisce quando troviamo un'istruzione allo stesso livello di indentazione, quindi il nostro ciclo comprende le righe dalla #8 alla #15.

In questo modo abbiamo finalmente eliminato l'istruzione input("Premi INVIO per terminare il programma") che ci costringeva ad usare la finestra di IDLE: ora non è più necessaria, perchè sarà pygame stesso ad aspettare, ripetendo il ciclo principale finchè non riceve l'evento QUIT.

LA STRUTTURA DI UN PROGRAMMA

Quella vista nell'ultimo esempio è la struttura tipica di un programma scritto per un'interfaccia grafica: c'è una prima parte di inizializzazione in cui si definiscono tutte le variabili necessarie, dopodichè si entra in un ciclo (che nel seguito chiameremo ciclo principale); in questo ciclo il programma aspetta gli eventi dal sistema operativo, li esamina e poi esegue i compiti richiesti da ogni evento (per il momento abbiamo solo stampato la descrizione degli eventi ricevuti). Il ciclo si interrompe di solito solo quando l'utente chiude la finestra principale, provocando l'uscita dal programma.

Ecco in particolare la nostra implementazione in Python e pygame.

  1. Inizializzazione: chiameremo la pygame.init(), apriremo la finestra principale screen e definiremo le principali variabili che ci serviranno.
  2. Ciclo principale: definiremo la variabile booleana done, ponendola uguale a False, ed useremo subito dopo l'istruzione while not done: che provocherà la ripetizione del blocco successivo finchè done rimarrà False. All'interno del ciclo chiameremo la pygame.event.get() ed esamineremo uno dopo l'altro tutti gli eventi arrivati dal sistema, decidendo che cosa fare in conseguenza. Per uscire dal ciclo principale basterà porre done = True in qualche istruzione al suo interno.
  3. Termine del programma: chiameremo la funzione pygame.quit() che chiude correttamente tutti i moduli di pygame ed esce senza provocare errori.

Facciamo ancora qualche aggiustamento in modo da ottenere uno "scheletro" di base:


import pygame
from pygame.locals import *

# inizializzazione
pygame.init()
screen = pygame.display.set_mode((800, 600))

# ciclo principale (si esce quando done diventa True)
done = False
while not done:
    # riceve tutti gli eventi arrivati dal sistema
    for ev in pygame.event.get():
        # se l'evento e' la fine del programma pone done = True
        if ev.type == QUIT:
            done = True

# fine del ciclo e termine del programma
pygame.quit()

Commentiamo i cambiamenti:

Salvate quest'ultimo esempio come prova_eventi.py: lo riprenderemo più volte nel seguito per espanderlo con nuove funzionalità.

NUOVO PROGRAMMA: prova_eventi1.py

Ricaricate ora il nostro esempio, salvatelo come prova_eventi1.py ed aggiungete queste righe:


.   .   .
    for ev in pygame.event.get():
        # se l'evento e' la fine del programma pone done = True
        if ev.type == QUIT:
            done = True
        # se l'evento e' il click del mouse lo scrive su IDLE
        elif ev.type == MOUSEBUTTONDOWN:
            print("Hai fatto click")
.   .   .

Ora il programma reagisce anche all'evento MOUSEBUTTONDOWN, avvisandoci quando clicchiamo sulla sua finestra con un messaggio sulla finestra di IDLE. Nei prossimi esercizi faremo altre modifiche.

ESERCIZIO 6.1: Ampliate prova_eventi1.py avvisando l'utente anche quando preme un tasto della tastiera (evento KEYDOWN) e quando muove il mouse (evento MOUSEMOTION).
SUGGERIMENTO: Dovete aggiungere altri due elif nel ciclo principale, attenti all'indentazione.
ESERCIZIO 6.2: Provocate l'uscita dal programma anche quando l'utente preme un tasto della tastiera.
ESERCIZIO 6.3: Scrivete un programma click_colori.py che cambi in maniera casuale il colore dello schermo ad ogni clic del mouse.
SUGGERIMENTO: Per questo programma dovete importare il modulo random, che contiene le funzioni per generare i numeri casuali (ricordate che la funzione randrange(n) restituisce un numero casuale tra 0 e n - 1). Nel ciclo principale dovete monitorare l'evento MOUSEBUTTONDOWN; quando accade dovete: SOLUZIONI

Fine della lezione