Classi
In questa lezione vedremo come si possono introdurre nuovi tipi in Python tramite il costrutto class
. L'utilità di introdurre nuovi tipi è illustrata tramite un semplice programma d'animazione in cui dischi con diversi tipi di movimento si muovono in una area rettangolare. I diversi tipi di dischi saranno definiti tramite opportune nuove classi, ovvero tipi.
Canvas
Il widget Canvas
di Tkinter
è un'area rettangolare in cui possiamo disegnare (ellissi, dischi, rettangoli, linee, ecc.), immergere immagini e testo o altri widget. Vediamo prima di tutto come creare una canvas in modo che sia ridimensionabile insieme alla finestra. Alcune funzioni di Tkinter
già le conosciamo:
import Tkinter as tk
win = tk.Tk()
win.rowconfigure(0, weight=1)
win.columnconfigure(0, weight=1)
CANVAS = tk.Canvas(win, bg='black') # Crea una canvas
CANVAS.grid(row=0, column=0, sticky='nsew')
win.mainloop()
Il costruttore tk.Canvas(win, bg='black')
crea un widget di tipo Canvas
contenuto nella finestra win
e con colore di background black
, cioè nero. Come al solito usiamo il grid layout, posizioniamo la canvas nella cella (0
, 0
) e attaccandola a tutti e quattro i lati della cella con sticky='nsew'
.
Eventi
Ora vogliamo far sì che quando il mouse è premuto sulla canvas venga disegnato sulla un disco bianco centrato nella posizione del mouse. Per rispondere a eventi relativi a un widget w
si usa il metodo w.bind(event_type, func)
che "lega" la func
all'evento di tipo event_type
, cioè ogniqualvolta si verifica un evento di tipo event_type
, la funzione func
viene chiamata passandogli come parametro un oggetto di tipo Event
che contiene informazioni circa l'evento. La specifica del tipo dell'evento è una stringa del tipo '<tipoEvento>'
, dove tipoEvento
può essere Button
(è stato premuto uno dei bottoni del mouse), Button-2
(è stato premuto il secondo bottone del mouse), ButtonRealease
(è stato rilasciato un bottone del mouse), KeyPress
(è stato premuto un tasto), KeyPress-A
(è stato premuto il tasto A
),
ecc., ecc. Nel nostro caso gli eventi che ci interessano sono specificati da <Button>
.
import Tkinter as tk
def on_mouse(evt): # Chiamata quando il mouse e'
# premuto sulla CANVAS
x, y = evt.x, evt.y # Le coordinate del mouse
r = 20
# Crea un disco di raggio r e centrato in x, y
CANVAS.create_oval(x - r, y - r, x + r, y + r, fill='white')
win = tk.Tk()
win.rowconfigure(0, weight=1)
win.columnconfigure(0, weight=1)
CANVAS = tk.Canvas(win, bg='black')
CANVAS.grid(row=0, column=0, sticky='nsew')
# Lega la funzione on_mouse all'evento <Button> sulla CANVAS
CANVAS.bind('<Button>', on_mouse)
win.mainloop()
La funzione on_mouse(evt)
prende in input un oggetto di tipo Event
che tra le altre informazioni contiene le coordinate, (evt.x
, evt.y
), del mouse nel momento in cui si è verificato l'evento (le coordinate sono relative al widget). Il metodo create_oval(x1, y1, x2, y2)
crea un oggetto di tipo ellisse inscritto nel rettangolo delimitato dallo spigolo in alto a sinistra (x1, y1)
e dallo spigolo in basso a destra (x2, y2)
. Il parametro opzionale fill
determina il colore di riempimento, nel nostro caso bianco.
Movimento
Ora vogliamo che i nostri dischi una volta creati inizino a muoversi. Uno dei moti più semplici da simulare approssimativamente è quello browniano. Per muovere un disco in una canvas è necessario che periodicamente (diciamo una trentina di volte al secondo) la sua posizione sia aggiornata. Tkinter
permette di chiamare una funzione dopo un dato intervallo di tempo: il metodo w.after(ms, func)
di un widget w
dice al loop degli eventi di Tkinter
di chiamare la funzione func
(senza parametri) fra ms
millisecondi. Se la funzione func
chiama a sua volta w.after(ms, func)
, il risultato è che func
verrà chiamata periodicamente ogni ms
millisecondi (approssimativamente).
Quindi dobbiamo definire una funzione frame()
che si occuperà di aggiornare periodicamente la posizione dei nostri dischi. Per fare ciò è necessario che i dati dei dischi siano salvati in un semplice dizionario e che questi dizionari siano registrati in una lista. Per ogni disco oltre alla posizione (coordinate del suo centro) e al raggio ci occorre anche l'identificativo del corrispondente oggetto nella canvas. Grazie all'identificativo possiamo modificare la posizione del disco nella canvas grazie ad un opportuno metodo che adesso vedremo.
import Tkinter as tk
import random as rnd
BALLS = [] # Lista dei dischi in movimento
def on_mouse(evt):
x, y = evt.x, evt.y
r = 20
# Dizionario per i dati del disco
b = {'x': x, 'y': y, 'r':r}
# Crea un oggetto disco di raggio r e centrato in x, y e
# ritorna un indentificatore dell'oggetto
b['id'] = CANVAS.create_oval(x - r, y - r, x + r, y + r,
fill='white')
BALLS.append(b)
def frame(): # Chiamata periodicamente
for b in BALLS: # Aggiorna la posizione di ogni disco
x, y, r = b['x'], b['y'], b['r']
x += rnd.randint(-r, r)/2
y += rnd.randint(-r, r)/2
CANVAS.coords(b['id'], x - r, y - r, x + r, y + r)
b['x'], b['y'] = x, y
CANVAS.after(20, frame) # Richiamata tra 20 millisecondi
win = tk.Tk()
win.rowconfigure(0, weight=1)
win.columnconfigure(0, weight=1)
CANVAS = tk.Canvas(win, bg='black')
CANVAS.grid(row=0, column=0, sticky='nsew')
CANVAS.bind('<Button>', on_mouse)
frame()
win.mainloop()
Tutti i metodi che creano oggetti in una canvas, come create_oval()
ritornano l'identificativo id
dell'oggetto creato. Tale identificativo può poi essere usato in altri metodi come il metodo coords(id, …)
che modifca le coordinate dell'oggetto. Nel caso di un oggetto di tipo ellisse le coordinate sono quelle del rettangolo che racchiude l'ellisse.
Classi
Ma se volessimo aggiungere anche altri tipi di dischi, ad esempio dischi che si muovono di moto rettilineo uniforme e che rimbalzano sui bordi della canvas? Chiaramente possiamo farlo in modo diretto ma questo comporta di complicare parecchio la funzione frame()
che dovrà distinguere tra i due tipi di dischi e anche la funzione on_mouse()
. Un modo più naturale e che porta a un codice più leggibile e estensibile, e di introdurre un nuovo tipo per ognuno dei tipi di disco.
Python come molti altri linguaggi object oriented permette di introdurre nuovi tipi tramite il concetto di classe. Una classe è la definizione di un tipo i cui valori sono oggetti che hanno uno stato e che possono avere delle operazioni (metodi) che possono essere eseguite su di essi. Ad esempio, tutti i tipi di Python (int
, float
, str
, ecc.) sono definiti tramite classi e tutte le operazioni o metodi che possono essere eseguiti su di essi sono definiti nella loro classe. Nel nostro caso vorremmo creare un nuovo tipo che potremmo chiamare Brownian
per i dischi con movimento browniano. Ogni oggetto di tipo Brownian
rappresenta uno specifico disco che ha uno stato determinato dalla posizione, il raggio e l'id del corrispondente oggetto nella canvas. Inoltre, la classe Brownian
dovrebbe anche definire un metodo che effettua l'aggiornamento della posizione. In questo modo l'aggiornamento della posizione di un disco (cioè il modo in cui il disco si muove) è incorporato nella definizione di quel tipo di dischi e non si trova disperso in altre parti del codice.
Per definire una classe in Python la sintassi è la seguente:
class NomeTipo(object): # object è una parola chiave
definizione dei metodi
Quindi la parola chiave class
seguita dal nome che si vuole dare al nuovo tipo (per convenzione dovrebbe iniziare con una lettera maiuscola) e dopo i :
e indentati le definizioni dei metodi del tipo NomeTipo
. Alcuni metodi hanno nomi speciali caratterizzati dal fatto che iniziano e finiscono con due underscore __
. Questi servono per definire il comportamento del tipo relativamente a vari operatori (+
, in
, str
, ecc.). Uno di questi è particolarmente importante perché è chiamato ogniqualvolta un nuovo oggetto di quel tipo viene creato. È il costruttore e il suo nome è __init__
. Il costruttore è usato per inizializzare lo stato dell'oggetto che sta per essere creato. Vediamo un esempio molto semplice. Vogliamo creare un tipo Color
, ogni oggetto di questo tipo rappresenta uno specifico colore:
class Color(object):
def __init__(self, r, g, b):
self.r = r
self.g = g
self.b = b
def inverse(self):
return (255 - self.r, 255 - self.g, 255 - self.b)
Il parametro self
è un parametro speciale e deve sempre essere il primo parametro di un metodo. self
ha come valore l'oggetto relativamente al quale il metodo è stato chiamato. Nel caso del costruttore il valore di self
e l'oggetto che si sta costruendo. Python automaticamente assegna il valore corretto a self
quindi non deve essere specificato quando si chiama un metodo. La sintassi del .
, come già sappiamo, permette di accedere ai cosidetti attributi di un oggetto che possono avere come valori funzioni (cioè metodi) o valori di qualsiasi altro tipo. Nel caso di self.r
, self.g
e self.b
sono valori numerici. Vediamo come si può usare questo nuovo tipo:
>>> c = Color(120, 0, 56) # Crea un oggetto di tipo Color
>>> c.inverse() # Ritorna il colore inverso
(135, 255, 199)
>>> c.r = 200 # Modifica il colore
>>> c.inverse()
(55, 255, 199) # L'inverso ora è differente
Adesso possiamo tornare ai nostri dischi. Definiamo la classe Brownian
:
class Brownian(object): # Classe per dischi con
# moto browniano
def __init__(self, x, y): # Costruttore
r = 20
self.id = CANVAS.create_oval(x - r, y - r, x + r,
y + r, fill='white')
self.x, self.y, self.r = x, y, r
def update(self): # Aggiorna la posizione
x, y, r = self.x, self.y, self.r
x += rnd.randint(-r, r)/2
y += rnd.randint(-r, r)/2
CANVAS.coords(self.id, x - r, y - r, x + r, y + r)
self.x, self.y = x, y
Grazie a questa definizione, le funzioni on_mouse()
e frame()
diventano molto più semplici e leggibili.
def on_mouse(evt):
BALLS.append(Brownian(evt.x, evt.y))
def frame():
for b in BALLS:
b.update()
CANVAS.after(20, frame)
A questo punto possiamo anche introdurre dischi con moto lineare e rimbalzanti sui bordi della canvas:
class Bouncing(object): # Classe per dischi con moto
# lineare e rimbalzanti
def __init__(self, x, y):
r = 20
self.id = CANVAS.create_oval(x - r, y - r, x + r,
y + r, fill='red')
self.x, self.y, self.r = x, y, r
self.vx, self.vy = rnd.randint(-10, 10),
rnd.randint(-10, 10) # Velocita'
def update(self):
# Dimensioni della CANVAS
w, h = CANVAS.winfo_width(), CANVAS.winfo_height()
x, y, r = self.x, self.y, self.r
vx, vy = self.vx, self.vy
x += vx
y += vy
# Collisione con i bordi verticali
if x > w - r or x < r:
x = (r if x < r else w - r)
vx = -vx
# Collisione con i bordi orizzontali
if y > h - r or y < r:
y = (r if y < r else h - r)
vy = -vy;
CANVAS.coords(self.id, x - r, y - r, x + r, y + r)
self.x, self.y = x, y
self.vx, self.vy = vx, vy
Dobbiamo però anche aggiunegre all'interfaccia utente dei bottoni per poter decidere quali tipi di dischi si vogliono creare:
import ttk
BALLTYPE = None # Il tipo dei dischi
def on_mouse(evt):
if BALLTYPE != None:
BALLS.append(BALLTYPE(evt.x, evt.y))
def do_brownian(): # Imposta il tipo dei dischi Brownian
global BALLTYPE
BALLTYPE = Brownian
def do_bouncing(): # Imposta il tipo dei dischi Bouncing
global BALLTYPE
BALLTYPE = Bouncing
frm = ttk.Frame(win)
btn = ttk.Button(frm, text='Brownian', style='Toolbutton',
width=10, command=do_brownian)
btn.grid(row=0, column=0)
btn = ttk.Button(frm, text='Bouncing', style='Toolbutton',
width=10, command=do_bouncing)
btn.grid(row=0, column=1)
frm.grid(row=1, column=0)
La variabile globale BALLTYPE
mantiene il tipo corrente dei dischi che è stato scelto tramite i due bottoni. Abbiamo usato il modulo ttk
per i bottoni perché contiene alcuni controlli che il modulo Tkinter
non ha.
Ora ci si può sbizzarrire a introdurre tutti i tipi di dischi che si vuole e la struttura del programma rimane la stessa. Grazie alle classi ogni aggiunta di nuovi tipi di dischi segue uno schema preciso e ordinato e l'intero codice ha una struttura modulare. Infatti, se volessimo introdurre un nuovo tipo di disco, dovremmo solamente definire la corrispondente classe e aggiungere un nuovo bottone, tutto qui. Le funzioni on_mouse()
e frame()
rimangono invariate e anche il resto.