GUI
Finora abbiamo usato Python tramite un'interfaccia utente guidata da comandi puramente testuali, Command-Line User Interface. Ma quasi tutte le applicazioni usano un'interfaccia utente grafica, Graphical User Interface o più brevememente GUI. Una GUI fornisce modi più semplici ed intuitivi per usare un'applicazione come finestre, menù, bottoni, campi, ecc.
Un programma con una Command-Line UI ha il controllo completo dell'interazione con l'utente essendo questa guidata dal programma stesso lungo binari molto rigidi. Invece, una GUI dà maggiore controllo all'utente e per questo il programma deve avere una struttura differente che lo rende pronto a rispondere a eventi che possono accadere in un qualsiasi momento. Questo modo di scrivere un programma viene chiamato appunto Event-Driven Programming, cioè, programmazione guidata dagli eventi.
Python ha molte librerie per lo sviluppo di GUI. Una di queste, Tkinter
, è incorporata nella libreria standard ed è quindi immediatamente disponibile in ogni installazione di Python. Come vedremo Tkinter
offre tutti gli elementi di base di una GUI, finestre, menù, bottoni, campi, ecc. Questi sono chiamati widget nella terminologia di Tkinter
e anche di molte altre librerie per la costruzione di GUI.
Una GUI è costruita componendo vari widget e programmando la risposta di questi agli eventi prodotti dall'utente (click del mouse, tasti, ecc.). Per fortuna una grossa parte del lavoro è fatto dalla libreria, nel nostro caso Tkinter
, che si occupa di visualizzare i widget sullo schermo, di produrre la risposta visiva agli eventi (ad es. mostrare un bottone premuto) e di gestire a basso livello gli eventi. Il nostro compito si riduce, si fa per dire, a decidere quali widget usare, la loro disposizione e le azioni da compiere in risposta agli eventi.
Per la documentazione di Tkinter
si può consultare o quella fornita dalla libreria standard tkinter o una fonte più ricca di esempi come tkinter doc.
La prima GUI
Come primo esempio di programma con GUI, vogliamo scrivere un text editor minimale. La nostra piccola applicazione avrà una finestra in cui sarà contenuto il testo da editare e un menù con i comandi per aprire un file esistente (Open), salvare le modifiche (Save) o salvare in un nuovo file (Save As). Ma procediamo un passo alla volta. Prima di tutto la finestra:
import Tkinter as tk
win = tk.Tk() # Crea una finestra (top level)
tk.mainloop() # Entra il loop d'attesa degli eventi
Bisogna prima di tutto importare il modulo Tkinter
dandogli il nome più breve tk
. Poi creiamo la finestra tramite il costruttore Tk()
e assegnamo il relativo oggetto alla variabile win
perchè poi ci servirà. Infine la funzione mainloop()
entra il cosidetto loop degli eventi. Questo significa che l'applicazione sarà completamente guidata dagli eventi che l'utente produrrà e che saranno gestiti dal modulo Tkinter
. Al momento i soli eventi che l'utente potrà produrre riguarderanno la finestra (spostamento, ridimensionamento, chiusura). Questi sono gestiti direttamente da Tkinter
. La finestra ma anche molti dei widget che vedremo in seguito saranno visualizzati secondo il look and feel della piattaforma.
Ora aggiungiamo un widget alla finestra che permette di gestire un testo editabile. Si chiama Text
e al pari di tutti i widget è un oggetto che per essere usato deve essere creato e posizionato in una finestra.
import Tkinter as tk
win = tk.Tk()
txt = tk.Text(win) # Crea un Text widget contenuto in win
txt.grid() # Posiziona il widget secondo il grid layout
tk.mainloop()
Quando si crea un qualsiasi widget w, che non è una finestra top level, bisogna fornire come primo argomento del costruttore il widget (che di solito è chiamato master o parent widget) in cui w deve essere contenuto. Inoltre, bisogna specificare dove deve essere posizionato. Per fare ciò Tkinter
offre dei cosidetti layout, cioè delle modalità con cui i widget inclusi in un master widget sono disposti. Vedremo solamente il grid layout perchè è quello più flessibile e prevedibile. Il grid layout posiziona i widget nelle celle di una griglia immaginaria le cui righe e colonne sono numerate a partire da 0
. La chiamata del metodo grid()
che abbiamo usato, txt.grid()
, posiziona txt
nella posizione di default che corrisponde alla cella della riga 0
e colonna 0
(che si trova in alto a sinistra).
Parametri opzionali
Prima di proseguire dobbiamo spiegare una caratteristica dei parametri delle funzioni in Python. Come vedremo moltissimi metodi di Tkinter
hanno decine di parametri ma possono essere chiamati anche fornendo solamente un piccolo sottoinsieme di questi. Questo è possibile grazie al fatto che Python permette di assegnare a un parametro di una funzione un valore di default che sarà usato solo se non è fornito esplicitamente un valore al momento della chiamata. La sintassi è la seguente:
def func(parametri, par1=val1, par2=val2,…):
istruzioni
I parametri par1
, par2
prendono come valori di default i valori val1
, val2
. Come esempio consideriamo una funzione total
che prende in input una lista e due parametri start
e end
e deve ritornare la somma dei valori dela lista tra gli indici start
e end
. Vogliamo che i parametri start
e end
possano essere omessi e in tal caso avranno come valori quelli di default che rappresenteranno l'inizio e la fine della lista, rispettivamente.
def total(lst, start=0, end=None):
if end == None:
end = len(lst)
s = 0
for i in range(start, end):
s += lst[i]
return s
Così possiamo chiamare la funzione in tutti i modi seguenti:
>>> total(range(10))
45
>>> total(range(10), 2) # start prende il valore 2
44
>>> total(range(10), end=7)
21
>>> total(range(10), start=3, end=8)
25
>>> total(range(10), 3, 8)
25
Quando nella chiamata è specificato il nome del parametro l'ordine non conta. Ad es. si può anche scrivere total(end=8, start=3, lst=range(10))
. Python è ancora più flessibile permettendo che una funzione possa avere un numero arbitrario di parametri, ma lo vedremo più avanti.
Espandibilità e Posizionamento
Torniamo ora alla nostra piccola applicazione. Si sarà notato che ridimensionando la finestra la dimensione del widget di testo rimane fissa. Infatti, di default le righe e le colonne del grid layout assumono una dimensione che è quella sufficiente per accomodare tutti i widget contenuti in esse. Per far sì che invece si allarghino quando la finestra si allarga bisogna specificarlo tramite i metodi rowconfigure()
e columnconfigure()
.
import Tkinter as tk
win = tk.Tk()
win.rowconfigure(0, weight=1) # Riga 0 è espandibile
win.columnconfigure(0, weight=1) # Colonna 0 è espandibile
txt = tk.Text(win)
txt.grid(sticky='nsew') # txt è attaccato ai 4 lati
# della sua cella
tk.mainloop()
La chiamata win.rowconfigure(0, weight=1)
fa sì che la riga 0
sia espandibile e lo stesso fa win.columnconfigure(0, weight=1)
per la colonna 0
. Il parametro sticky
dice a quali lati della cella il widget deve rimanere attaccato se la cella è più grande del widget. I lati sono denominati con le direzioni geografiche n
(north, alto), s
(south, basso), e
(east, sinistra), w
(west, destra). Siccome vogliamo che il widget di testo si allarghi in tutte le direzioni impostiamo sticky='nsew'
. Se avessimo voluto che fosse espandibile solamente in verticale avremmo scritto sticky='ns'
. Di default il valore di sticky
è 'center'
che significa che il widget non si espande e rimane centrato nella cella.
Menù
Aggiungiamo ora un piccolo menù ala nostra applicazione. Per adesso il menù non sarà operativo cioè selezionando una voce non accade nulla. Le seguenti istruzioni possono essere aggiunte appena prima della chiamata a mainloop()
:
mb = tk.Menu(win) # Crea la barra del menu (vuota)
win.config(menu=mb) # e la aggiunge alla finestra
fm = tk.Menu(mb) # Crea il menù File
fm.add_command(label='Open...') # e vi aggiunge la voce Open
fm.add_separator() # una linea di separazione
fm.add_command(label='Quit') # infine, la voce Quit
mb.add_cascade(label='File', menu=fm) # Aggiunge il menù
# File alla barra
Per creare un widget di tipo menù si usa il costruttore Menu()
. Il primo parametro è il master del menù. Nel caso della barra del menù (o menu bar) il master è una finestra a cui la barra si attacca nella parte superiore (sotto Linux e Windows, mentre sotto Mac OS X si attacca alla parte superiore dello schermo). La barra di menù è un menù speciale le cui voci si sviluppano in orizzontale mentre per i menù "normali" si sviluppano verticalmente. Ma i metodi per crearli e configurarli sono gli stessi. La barra del menù dopo la creazione va attaccata alla finestra con il metodo config(menu=menubar)
. Poi per creare un menù si usa sempre il costruttore Menu()
(adesso però il master è la barra di menù). Per attaccare una voce (o comando) ad un menù si può usare il metodo add_command()
, il parametro label
fornisce il nome che verrà visualizzato. Per attaccare un sotto-menù ad un menù si usa il metodo add_cascade()
, in cui il parametro label
fornisce il nome e il parametro menu
fornisce l'oggetto menù da attaccare. Qui lo abbiamo usato per attaccare il menù 'File'
alla barra di menù.
Per rendere i menù operativi, dobbiamo associare ad ogni voce di menù, come Open
o Quit
, un opportuno comando (cioè una funzione senza parametri) che sarà eseguito in risposta alla selezione della voce. Nel caso della voce Quit
possiamo definire una funzione do_quit()
che semplicemente termina l'applicazione chiamando il metodo win.quit()
:
def do_quit():
win.quit()
Poi quando aggiungiamo la voce Quit
al menù associamo tale funzione come comando:
fmenu.add_command(label='Quit', command=do_quit)
In questo modo, quando la voce Quit
viene selezionata la funzione do_quit()
sarà eseguita.
Finestre di Dialogo
Per la voce Open
le cose sono un po' più complicate perché vogliamo che sia mostrata una finestra di dialogo in cui poter scegliere il file da aprire. Il modulo tkFileDialog
contiene funzioni che permettono di aprire dialoghi per aprire o salvare files. Noi useremo la funzione askopenfilename()
:
import tkFileDialog as fd
def do_open():
path = fd.askopenfilename(title='Scegli un file',
filetypes=[('text', '*.txt'),
('python', '*.py')])
if len(path) > 0:
txt.delete('1.0', 'end')
with open(path, 'U') as f:
txt.insert('1.0', f.read())
fmenu.add_command(label='Open...', command=do_open)
nella chiamata alla funzione askopenfilename()
tramite il parametro opzionale title
possiamo fornirgli il titolo e tramite il parametro filetypes
una lista di coppie di stringhe che determinano i files che possono essere aperti. La funzione ritorna una stringa contenente il percorso del file selezionato oppure la stringa vuota se l'utente ha scelto Cancel
. La funzione do_open()
, se è stato selezionato un file, prima di tutto cancella il contenuto del widget txt
tramite il metodo delete()
: '1.0'
denota il primo carattere della prima linea del testo (le linee sono numerate a partite da 1
mentre i caratteri a partire da 0
) e 'end'
denota la prima posizione dopo l'ultimo carattere dell'intero testo. Poi legge il contenuto del file e lo inserisce tramite il metodo insert()
nel widget txt
a partire dalla prima posizione.
Per aggiungere la voce Save As
possiamo procedere in modo analogo:
def do_saveas():
path = fd.asksaveasfilename(title='Dove Salvare')
if len(path) > 0:
with open(path, 'w') as f:
f.write(txt.get('1.0', 'end'))
fmenu.add_command(label='Save As...', command=do_saveas)
Il metodo get('1.0', 'end')
ritorna una stringa contenente il testo di txt
dal primo all'ultimo carattere.
Variabili Globali
Per aggiungere la voce Save
dobbiamo salvare il percorso del file corrente in una variabile globale FPATH
così che la funzione do_save()
possa salvare il contenuto del widget txt
nel file FPATH
. Inoltre, dobbiamo modificare anche le funzioni do_open()
e do_saveas()
affinchè modifichino opportunamente il valore della variabile FPATH
. Per variabile globale intendiamo una variabile che è definita all'esterno di ogni funzione.
Le funzioni che abbiamo scritto do_quit()
, do_open()
e do_saveas()
già leggono le variabili globali win
e txt
. Ma non ne modificano i valori, cioè non le scrivono. In generale, una funzione in Python può leggere il valore di una qualsiasi variabile globale che è visibile alla funzione quando questa viene eseguita. Se c'è una variabile locale alla funzione che ha lo stesso nome della variabile globale, la variabile locale nasconde la variabile globale. Siccome la scrittura di una variabile globale può conforndersi con l'introduzione di una nuova variabile locale, Python richiede che per poter scrivere una variabile globale all'interno di una funzione la variabile globale vada preventivamente dichiarata come tale tramite il costrutto global
. Ecco alcuni esempi che dovrebbero chiarire le cose:
g = 10
>>> def incr(x):
return x + g # Legge la varibile globale g
>>> incr(5)
15
>>> def incr2(x):
g = 1 # Questa è una variabile locale
return x + g
>>> incr2(5)
6
>>> g # La var. globale g non è cambiata
10
>>> def incr3(x):
global g # Dichiarazione: g è una var. globale
g = 1 # Modifica la var. globale g
return x + g
>>> incr3(5)
6
>>> g # Infatti la va. globale g è stata
1 # modificata
Il programma finale
Adesso possiamo introdurre la variabie globale FPATH
, scrivere la funzione do_save()
e modificare le funzioni do_open()
e do_saveas()
. Inoltre, è buona norma identificare variabili globali con nomi in maiuscolo, così mettiamo in maisucolo anche i nomi delle variabili win
e txt
. Ecco l'intero codice finale:
import Tkinter as tk
import tkFileDialog as fd
FPATH = None
WIN = tk.Tk()
WIN.title('Senza Titolo')
WIN.rowconfigure(0, weight=1)
WIN.columnconfigure(0, weight=1)
TXT = tk.Text(WIN)
TXT.grid(sticky='nsew')
def do_quit():
WIN.quit()
def do_open():
path = fd.askopenfilename(title='Scegli un file',
filetypes=[("text", "*.txt"),
('python', '*.py')])
if len(path) > 0:
global FPATH
TXT.delete('1.0', 'end')
with open(path, 'U') as f:
TXT.insert('1.0', f.read())
WIN.title(path)
FPATH = path
def do_saveas():
path = fd.asksaveasfilename(title='Dove Salvare')
if len(path) > 0:
global FPATH
with open(path, 'w') as f:
f.write(TXT.get('1.0', 'end'))
WIN.title(path)
FPATH = path
def do_save():
if FPATH != None:
with open(FPATH, 'w') as f:
f.write(TXT.get('1.0', 'end'))
mb = tk.Menu(WIN)
WIN.config(menu=mb)
fm = tk.Menu(mb)
fm.add_command(label='Open...', command=do_open)
fm.add_command(label='Save', command=do_save)
fm.add_command(label='Save As...', command=do_saveas)
fm.add_separator()
fm.add_command(label='Quit', command=do_quit)
mb.add_cascade(label='File', menu=fm)
tk.mainloop()
Chiaramente è un text editor molto spartano. Ad esempio, manca il controllo che prima di chiudere l'applicazione o prima di passare ad aprire un nuovo file, le modifiche fatte al file corrente siano state salvate.