Programmare le interfacce grafiche con Tkinter

In questo capitolo vedremo come viene costruito un programma GUI [Graphical User Interface, ovvero Interfaccia Grafica per l'Utente. N.d.t.] prima in termini generici e successivamente come ciò venga fatto utilizzando il "toolkit" per interfacce grafiche nativo di Pyton, Tkinter. Questo capitolo non contiene una descrizione esauriente di Tkinter e nemmeno pretende di essere una guida introduttiva completa. A questo scopo potete trovare un ottimo e dettagliato manuale introduttivo nel sito Internet di Python. Nelle pagine che seguono cercheremo comunque di guidare il lettore attraverso i concetti di base della programmazione di GUI, presenteremo alcuni dei componenti di base spiegando come usarli. Vedremo inoltre come la programmazione ad oggetti possa essere assai utile nell'organizzare applicazioni di tipo GUI.

I principi della programmazione delle interfacce grafiche

Premettiamo innanzitutto che nelle pagine che seguono non apprenderete niente di nuovo sulla programmazione. La programmazione di GUI è esattamente analoga ad ogni altro tipo di prgrammazione: useremo sequenze di istruzioni, cicli, fasi condizionali e moduli come abbiamo fatto finora. La differenza principale consiste nel fatto che nella programmazione di GUI normalmente viene usato un Toolkit [Insieme di attrezzi, N.d.t.] ed occorre quindi seguire il modello di programmazione previsto da chi ha prodotto il toolkit. Ogni nuovo toolkit ha una sua API [Application Programmer's Interface, ovvero una libreria di funzioni specifiche del toolkit, N.d.t.] ed un proprio insieme di regole per la programmazione che il programmatore deve imparare e seguire. Per questa ragione molti programmatori tendono ad utilizzare solo pochi toolkit, preferendo quelli che sono disponibili per vari linguaggi: imparare un nuovo toolkit tende ad essere assai più complicato che imparare un nuovo linguaggio di programmazione!

Qui tratteremo del toolkit Tk usato in Tcl, Perl e Python. I principi usati in Tk sono leggeremente diversi da quelli di altri toolkit, per cui concluderemo questo capitolo con un breve sguardo ad un'altro toolkit per Python (e C/C++) che ha un approccio un po' più convenzionale. Ma prima introduciamo alcuni principi generali:

Come abbiamo detto già in altre occasioni, le applicazioni GUI sono quasi sempre di tipo ad eventi. Se non ricordate cosa ciò significhi, tornate al capitolo: Programmazione ad eventi.

Daremo per scontato che abbiate già familiarità con le interfacce grafiche per averle utilizzate e ci concentreremo su come esse funzionino dal punto di vista del programmatore. Non entreremo in dettagli relativi alla programmazione di GUI complesse, con finestre multiple, interfacce MDI [Multiple Document Interface, ovvero interfaccia per documenti multipli, N.d.t.], ecc. Ci limiteremo ai concetti fondamentali per la creazione di applicazioni con una finestra singola, qualche etichetta, bottoni, aree di testo e di messaggi.

Per cominciare verifichiamo il vocabolario. La programmazione di GUI ha il proprio insieme di termini. I più comuni sono riportati nella seguente tabella:

TermineDescrizione
Finestra [In inglese: window, N.d.t.] Un'area dello schermo controllata da un programma. Le finestre sono normalmente rettangolari, ma alcuni ambienti GUI consentono altre forme. Le finestre possono contenere altre finestre e spesso ogni elemento di controllo grafico è a sua volta una finestra a pieno titolo.
Elemento di controllo [In inglese: control, N.d.t.] Un elemento di controllo è un oggetto grafico usato per controllare l'applicazione. Gli elementi di controllo hanno proprietà associate e normalmente generano eventi. Di solito gli elementi di controllo corrispondono ad oggetti al livello del programma e gli eventi sono associati a metodi dell'oggetto corrispondente in modo che quando si verifica l'evento l'oggetto manda in esecuzione uno dei propri metodi. L'ambiente di programmazione GUI solitamente fornisce un meccanismo per legare gli eventi ai metodi.
Widget [Contrazione di "Window Object", N.d.t.] Un elemento di controllo, solitamente corrispondente ad un elemento visibile. Alcuni elementi di controllo (ed esempio i "timer") possono essere associati ad una finestra, ma non sono visibili. I widget sono solo quegli elementi che sono visibili e che possono essere manipolati dall'utente o dal programmatore. I widget di cui tratteremo sono:
  • Frame [Riquadro, N.d.t.].
  • Label [Etichetta, N.d.t.].
  • Button [Bottone, N.d.t.].
  • Text Entry [Area per immissione di caratteri, N.d.t].
  • Message boxes [Area per messaggi, N.d.t.].

Quelli di cui non parleremo, ma che sono utilizzati in altre parti di questo manuale sono:

  • Text box [Area contenente testo, N.d.t.].
  • Radio Button [Pulsanti, N.d.t.]
Infine non parleremo affatto di:
  • Canvas [Tela, N.d.t.] - area per disegnare
  • Check button [Pulsanti, N.d.t.] - per selezioni multiple
  • Image [Immagine, N.d.t.]- per la visualizzazione di immagini BMP, GIF, JPEG e PNG.
  • Listbox [Elenco, N.d.t.] - per le liste
  • Menu/MenuButton - per costruire menu;
  • Scale/Scrollbar [Indice/Barra di scorrimento, N.d.t.] - per indicare posizioni
Frame Un tipo di widget usato per raggruppare altri widget. Spesso si utilizza un frame per rappresentare la finestra completa ed altri frames vengono inclusi al suo interno.
Layout [Disposizione, N.d.t.]. Gli elementi di controllo vengono disposti all'interno di un frame seguendo un particolare modello di disposizione. La disposizione può essere specificata in molti modi, ad esempio usando le coordinate dello schermo specificate in numero di pixel, usando la posizione relativa rispetto ad altri componenti (a sinistra, sopra, ecc.), oppure utilizzando una disposizione a griglia o a tabella. Il sistema basato su coordinate è facile da comprendere ma difficile da utilizzare quando ad una finestra vengono cambiate le dimensioni. Si consiglia ai principianti di utilizzare i metodi con specifica di coordinate solo se le finestre sono a dimensione fissa.
Child [Figlio, N.d.t.]. I programmi GUI sono usualmente strutturati come una gerarchia di widget e altri elementi di controllo. Il frame di più alto livello che coincide con la finestra dell'applicazione contiene sotto-frame che a loro volta contengono altri frame o elementi di controllo. Tutti questi elementi possono essere visualizzati sotto forma di una struttura ad albero in cui ciascun elemento ha un solo padre ed un numero variabile di figli. In pratica normalmente questa struttura viene esplicitamente memorizzata all'interno dei widget in modo che il programmatore, o più comunemente lo stesso ambiente GUI, possa applicare una operazione ad un elemento di controllo ed a tutti i suoi figli.

Uno sguardo ai widget più comuni

In questo paragrafo useremo il prompt interattivo di Python per creare alcune semplici finestre e alcuni widget. Notate che dato che IDLE è a sua volta un'applicazione Tkinter non è possibile utilizzare altre applicazioni Tkinter al suo interno in modo affidabile. Naturalmente potete usare IDLE come editore di testo per creare i programmi, ma poi questi devono essere mandati in esecuzione dal prompt del sistema operativo. Quelli che utilizzano Pythonwin possono invece lanciare applicazioni Tkinter dato che Pythonwin è costruito usando un proprio toolkit: MFC ["Microsoft Foundation Classes", N.d.t.]. Nonostante questo, comunque, anche utilizzando Pythonwin le applicazioni Tkinter mostrano comportamenti strani in qualche occasione. Per questi motivi useremo il prompt di Python.

>>> from Tkinter import *

Ogni programma Tkinter ha una necessità: deve importare i nomi dei widget. Naturalmente potremmo importare semplicemente il modulo, ma dovremmo poi ogni volta premettere il nome Tkinter ad ogni componente, e questo alla lunga risulta noioso.

>>> cima = Tk()

Questa istruzione crea il widget di primo livello nella nostra gerarchia di widget. Tutti gli altri widget saranno creati come discendenti di questo. Notate che dopo il comando compare una finestra completa di una barra per il titolo vuota ed il solito gruppo di bottoni di controllo (per ridurre a icona, passare a tutto schermo, ecc.). Adesso continueremo ad aggiungere componenti per creare il nostro programma.

>>> dir(cima)

['_tclCommands', 'children', 'master', 'tk']

La funzione dir visualizza i nomi che sono definiti all'interno dell'argomento. Può essere utilizzata per i moduli, ma in questo caso la stiamo utilizzando per guardare all'interno dell'oggetto cima, istanza della classe Tk. Ciò che vediamo sono gli attributi di cima; notate in particolare gli attributi children [figli, più spesso si usa però "discendenti", N.d.t.] e master [padrone, in italiano si usa preferibilmente "padre", N.d.t.] che sono i puntatori alla gerarchia di widget. Notate anche l'attributo _tclCommands, che deriva dal fatto che Tkinter è costruito intorno ad un toolkit per Tcl chiamato Tk.

>>> F = Frame(cima)

Crea un widget di tipo Frame destinato a contenere tutti gli elementi di controllo e widget che saranno creati come discendenti di questo. Frame specifica come primo (ed in questo caso, unico) argomento cima, ciò significa che F è un widget figlio di cima.

>>>F.pack()

Noterete che la finestra Tk si è adesso ridotta alla dimensione del widget Frame che per il momento è vuoto, quindi risulta adesso molto piccola! Il metodo pack() attiva un Controllo di disposizione [In inglese: "Layout Manager", N.d.t.], chiamato "impacchettatore", che risulta assai facile da usare per disposizioni semplici, ma che diventa complicato quando la complessità aumenta. Per il momento ci limiteremo a questo metodo per la sua semplicità. Notate che i widget non saranno visibili nella nostra applicazione fino a che non li impacchettiamo (oppure fino a che non chiamiamo qualche altro metodo per la loro disposizione).

>>>lCiao = Label(F, text="Ciao gente")

Abbiamo creato un nuovo oggetto lCiao, istanza della classe Label [Etichetta, N.d.t.] con un genitore, F ed un attributo text che vale "Ciao gente". Notate che, dato che i costruttori degli oggetti Tkinter tendono ad avere numerosi argomenti (ciascuno con un suo valore di default), è usuale utilizzare la tecnica di passaggio per nome degli argomenti di questi oggetti. Notate inoltre che l'oggetto non è visibile perché non abbiamo ancora chiamato il metodo pack().

Infine voglio farvi notare la convenzione per i nomi: ho usato una l minuscola (sta per "Label") come iniziale del nome, seguita da Ciao che mi ricorda lo scopo dell'oggetto. Al pari di gran parte delle convenzioni si tratta di gusto personale, ma ritengo che sia comunque valida.

>>>lCiao.pack()

Adesso la vediamo. Anche la vostra dovrebbe apparire cosí:

Finestra con etichetta

Possiamo specificare altre proprietà dell'oggetto Label, ad esempio il tipo di carattere o il colore aggiungendo argomenti al costruttore dell'oggetto. Le corrispondenti proprietà possono anche essere modificate usando il metodo configure definito per tutti gli ogetti Tkinter. Ad esempio:

>>> lCiao.configure(text="Arrivederci")

Il testo è cambiato. Facile, non è vero? configure si rivela una tecnica particolarmente adatta per cambiare molte proprietà in un'unica operazione dato che possono essere passate come argomenti tutte insieme. Tuttavia se dovete cambiare solo una proprietà, possiamo trattare l'oggetto come un dizionario, cioè:

>>> lCiao['text'] = "Ancora ciao"

Che è più breve e presumibilmente più facile da comprendere.

I widget Label sono decisamente noiosi, non fanno altro che visualizzarre un testo, sia pure in vari colori, tipi di carattere e dimensioni. In pratica possono anche visualizzare semplici disegni, ma vedremo poi come farlo.

Prima di passare ad altri tipi di oggetto c'è un'altra cosa da fare, ovvero dare un nome alla finestra. Lo facciamo usando un metodo del widget di primo livello cima:

>>> F.master.title("Ciao")

Avremmo potuto usare direttamente il nome cima ma come vedremo più avanti l'accesso tramite l'attributo master del Frame è una tecnica utile [L'attributo master del frame contiene un riferimento al padre, che in questo caso è cima, ovvero F.master e cima sono lo stesso oggetto; N.d.t.].

>>> bEsci = Button(F, text="Esci", command=F.quit)

Abbiamo creato un nuovo widget, un bottone. Il bottone ha un'etichetta "Esci" ed è associato al comando F.quit. Notate che abbiamo passato come argomento il nome del metodo, e non lo abbiamo eseguito come accadrebbe se mettessimo le parentesi tonde dopo il nome. Ciò significa che nel gergo di Python dobbiamo passare un oggetto di tipo funzione; questo può essere un metodo predefinito in Tkinter, come in questo caso, oppure qualunque altra funzione definita nel programma. La funzione, o il metodo, non devono richiedere argomenti. Il metodo quit [Esci, N.d.t.], come il metodo pack, è definito in una classe di base e viene ereditato da tutti i widget di Tkinter.

>>>bEsci.pack()

Anche in questo caso il metodo pack rende visibile il bottone.

>>>cima.mainloop()

Attiviamo il ciclo degli eventi di Tkinter. Notate che il prompt di Python >>> scompare. Ciò significa che il controllo è passato adesso a Tkinter. Se "premete" il bottone "Esci" il prompt riappare, dimostrando che l'opzione command ha funzionato.

Tenete conto del fatto che gli stessi comandi impartiti in Pythonwin o in IDLE possono dare risultati differenti. Se ciò accade provate a scrivere i comandi usati fino a questo momento dentro un file Python e a lanciare il file dal prompt del sistema operativo.

In effetti probabilmente questo è il momento adatto per seguire il consiglio: dopo tutto la maggior parte dei programmi Tkinter saranno preparati e mandati in esecuzione proprio in questo modo. Scrivete quindi i comandi principali che abbiamo visto finora:

from Tkinter import *

# Prepara la finestra
cima = Tk()
F = Frame(cima)
F.pack()

# Aggiungi i widget
lCiao = Label(F, text="Ciao")
lCiao.pack()
bEsci = Button(F, text="Esci", command=F.quit)
bEsci.pack()

# Attiva il ciclo
cima.mainloop()

La chiamata del metodo cima.mainloop attiva il ciclo di Tkinter che genera gli eventi. In questo caso l'unico evento che viene processato è il click sul bottone che è associato al metodo F.quit. Il metodo F.quit a sua volta termina l'applicazione. Provate il programma; dovrebbe apparire cosí:

Label with Button

Disporre i widget

Nota: Per il momento abbandoniamo il prompt >>> e presentiamo gli esempi scritti in file.

In questo paragrafo esaminiamo il modo in cui Tkinter posiziona i widget all'interno di una finestra. Abbiamo già incontrato i widget Frame, Label e Button e questo è tutto ciò che ci serve. Nell'esempio precedente abbiamo usato il metodo pack del widget per collocarlo all'interno del widget padre. Ovvero, dal punto di vista tecnico, abbiamo invocato il controllo di disposizione pack di Tk. Lo scopo del controllo di disposizione consiste nel determinare la migliore disposizione possibile per i widget, sulla base di suggerimenti forniti dal programmatore e di altri vincoli, come ad esempio la dimensione della finestra, controllati dall'utente. Alcuni controlli di disposizione richiedono l'esatta posizione all'interno della finestra, di solito specificata in numero di pixel. Questo metodo è molto usato negli ambienti di Windows quale ad esempio Visual Basic. Tkinter include un gestore di disposizione Placer che usa questa modalità mediante un metodo place [Posiziona, N.d.t.]. In queste pagine non tratteremo di questo tipo di modalità in quanto uno degli altri, più intelligenti, controlli di posizione è nella maggior parte dei casi più utile in quanto elimina la necessità per il programmatore di preoccuparsi di cosa accade quando la dimensione della finestra viene cambiata.

Il più semplice controllo di disposizione di Tkinter è packer, che abbiamo già usato. Packer, per default, semplicemente incolonna i widget uno sopra l'altro. Questo raramente rispecchia quello che vogliamo ottenere, però se costruiamo la nostra applicazione usando vari Frame e poi collochiamo i Frame uno sopra l'altro, possiamo avvicinarci al risultato voluto. Sarà poi possibile inserire altri widget all'interno dei frames utilizzando packer o un'altro controllo di disposizione appropriato per collocarli opportunamente all'interno dei Frame. Potete trovare un esempio di queste tecniche messe in pratica nel capitolo Un esempio pratico.

Comunque anche il semplice packer fornisce una moltitudine di opzioni. Ad esempio possiamo disporre i widget affiancati orizzontalmente, invece che impilati verticalmente, specificando un argomento side, come nell'esempio:

lCiao.pack(side="left")
bEsci.pack(side="left")

Questo impone al widget di disporsi a sinistra [in inglese: left, N.d.t] in modo che il primo widget (l'etichetta) si troverà sul lato sinistro, affiancato da quello successivo (il bottone). Se modificate le linee come indicato nell'esempio, otterete questo risultato:

widget affiancati a sinistra

Se invece sostituite a "left" la parola "right" [Destra, N.d.t.] l'etichetta compare all'estrema destra ed il bottone a sinistra di quest'ultima:

widget affiancati a destra

Noterete che l'aspetto non è particolarmente gradevole in quanto i widget sono molto ravvicinati. Packer ci fornisce strumenti anche per trattare questi casi. Il modo più facile è usare una spaziatore a cui specificheremo lo spazio orizzontale (padx) e quello verticale (pady) desiderato. I valori sono espressi in pixel. Aggiungiamo un po' di spazio orizzontale nel nostro esempio:

lCiao.pack(side="left", padx=10)
bEsci.pack(side='left', padx=10)

Dovremmo ottenere:

Spaziatura orizzontale

Se provate a cambiare dimensione alla finestra vedrete che i widget mantengono la posizione relativa e rimangono centrati nella finestra. Perché accade questo, visto che li abbiamo affiancati a sinistra? La risposta è che li abbiamo affiancati all'interno di un Frame, ma il Frame stesso è stato disposto con pack senza specificare un lato e quindi viene disposto in alto al centro, la posizione di default. Se vogliamo disporre i widget sul lato della finestra, dobbiamo specificarlo anche per il Frame:

F.pack(side='left')

Notate infine che aumentando la dimensione verticale della finestra i widget rimangono centrati in senso verticale, anche questo è determinato dal comportamento di default di packer.

Potete continuare a giocare con padx e pady per vedere gli effetti di valori diversi, combinazioni, ecc. La combinazione di side e padx/pady consente parecchia flessibilità nella disposizione di widget mediante packer. Sono disponibili numerose altre opzioni; ciascuna di esse aggiunge una ulteriore precisa possibilità di controllo. Siete invitati a verificare nel manuale di Tkinter tutti i dettagli del caso.

Tkinter fornisce un altro paio di controlli di posizione, detti rispettivamente grid [Griglia, N.d.t.] e placer [Posizionatore, N.d.t.]. Per usare grid occorre chiamare grid() al posto di pack() e per usare placer occorre chiamare place(). Ciascuno di essi ha un insieme di opzioni e per i dettagli siete invitati a fare riferimento al manuale di Tkinter, dato che in queste pagine sarà utilizzato solo packer. Gli aspetti essenziali da notare sono che grid dispone i widget in una griglia (sorpresa!) all'interno della finestra. Ciò può essere utile ad esempio per disporre aree per l'introduzione di righe di testo con le righe di testo allineate. Placer utilizza o coordinate in pixel o coordinate relative alla dimensione della finestra. Queste ultime consentono al widget di cambiare dimensione insieme alla finestra, ad esempio occupando sempre il 75% dello spazio disponibile verticalmente. Questo può essere utile per progettare finestre assai complesse ma richiede parecchio studio preliminare per il quale è consigliato usare carta quadrettata, matita e gomma da cancellare!

Come ottenere un certo aspetto usando Frame e Packer

Il widget Frame ha alcune utili proprietà che adesso vedremo in dettaglio. È effettivamente utile avere una cornice ideale intorno ai componenti, ma talvolta vorremmo avere anche qualche cosa di visibile. Questo è utile particolarmente per gruppi di pulsanti o di caselle da marcare. Il Frame risponde a questa richiesta fornendo, come molti altri widget, la proprietà Relief [Rilievo, N.d.t.]. La proprieta relief può avere molti valori: sunken [Incavato, N.d.t.], raised [In rilievo, N.d.t.], groove [scanalato, N.d.t.], ridge [bordo a rilievo, N.d.t.], o flat [piatto, N.d.t.]. Proviamo ad usare il valore sunken nella nostra semplice finestra. È sufficiente cambiare la linea di creazione del Frame in:

 F = Frame(cima,relief="sunken", border=1) 

Nota 1:Occorre aggiungere anche un bordo, altrimenti il frame risulta incavato, ma con un bordo invisibile e non si vede alcuna differenza.

Nota 2: La dimensione del bordo non è racchiusa fra apici. Uno degli aspetti che generano confusione programmando Tk consiste nel capire quando occorre mettere gli apici in un valore opzionale e quando invece no. In generale per valori numerici o costituiti da un unico carattere gli apici non sono necessari. Se il valore è un insieme di caratteri alfabetici e numeri o una stringa, allora sono necessari gli apici. Analoga confusione si ha relativamente all'uso di maiuscole o minuscole e per questo purtroppo non c'è soluzione, occorre imparare con l'esperienza. Python comunque spesso include un elenco di opzioni valide nei messaggi di errore!

Notate ancora che il Frame non riempie la finestra. Questo può essere modificato con un'altra opzione di packer detta, ovviamente, fill [Riempi, N.d.t.]. Quando viene chiamato pack si specifica:

F.pack(fill="x")

per riempire in senso orizzontale, per riempirla nei due sensi basta aggiungere fill='y'.

Il risultato finale quando si esegue il nostro programma è:

Frame incavato

Aggiungere altri widget

Esaminiamo ora un widget per l'immissione di testo: Entry. Si tratta della familiare area per immettere una linea di testo. Ha molti metodi in comune con un widget più sofisticato, Text, che qui non trattiamo. Confidiamo che l'uso dei metodi del widget Entry costituisca una base solida per esplorare il widget Text in un secondo tempo.

Riprendendo il nostro programma "Ciao gente" aggiungiamo un widget per l'introduzione di testo in un suo Frame specifico ed un bottone per cancellare il testo introdotto. Questo serve da dimostrazione non solo della creazione e dell'uso del widget Entry, ma anche di come definire una propria funzione per il trattamento di eventi e come collegarla ad un widget.

from Tkinter import *

# crea la funzione di gestione dell'evento 
def evCanc():
  eCiao.delete(0,END)

# crea finestra e frame di primo livello
cima = Tk()
F = Frame(cima)
F.pack(expand="true")

# aggiungi il frame di immissione del testo
fTesto = Frame(F, border="1")
eCiao = Entry(fTesto)
fTesto.pack(side="cima", expand="true")
eCiao.pack(side="left", expand="true")

# Infine il frame con i bottoni
# per evidenziarlo lo facciamo incavato
fBottoni = Frame(F, relief="sunken", border=1)
bCanc = Button(fBottoni, text="Cancella testo", command=evCanc)
bCanc.pack(side="left", padx=5, pady=2)
bEsci = Button(fBottoni, text="Esci", command=F.quit)
bEsci.pack(side="left", padx=5, pady=2)
fBottoni.pack(side="cima", expand="true")

# Adesso attiviamo il ciclo degli eventi
F.mainloop()

Notate che anche qui abbiamo passato il nome della funzione di gestione dell'evento (evCanc) mediante l'argomento command del bottone bCanc. Notate anche l'uso della convenzione dei nomi, evXXX per associare la funzione di gestione dell'evento con il widget corrispondente.

Il programma in esecuzione appare come segue:

Entry and button controls

E se immettete caratteri nell'area apposita e quindi premente il bottone "Cancella testo", i caratteri scompaiono.

Come legare gli eventi. Dai widget al codice

Fino ad ora abbiamo usato la proprietà "command" dei bottoni per associare funzioni Python ad eventi dell'interfaccia grafica. Talvolta abbiamo però bisogno di un controllo più preciso, ad esempio intercettare una particolare combinazione di tasti. Per fare ciò utilizziamo la funzione bind che lega esplicitamente un evento ad una funzione Python.

Definiremo un tasto speciale, supponiamo Control-C, per cancellare il testo nell'esempio precedente. Per ottere questo risultato occorre legare la combinazione di tasti Control-C allo stesso gestore di eventi a cui è legato il tasto "Cancella testo". Purtroppo c'è un ostacolo imprevisto. Quando si usa l'opzione "command" la funzione specificata non deve richiedere argomenti. Quando invece si usa la funzione bind per lo stesso scopo la funzione legata richiede un argomento. Allora è necessario creare una nuova funzione con un argomento che chiama evCanc. Dopo la definizione di evCanc occorre quindi aggiungere la seguente definizione:

def evTSpec(event):
    evCanc()

E dopo la definizione del widget Entry occorre aggiungere:

eCiao.bind("<Control-c>",evTSpec) # nella definizione del tasto occorre rispettare maiuscole/minuscole 

Mandate il programma in esecuzione e vedrete che il testo può essere cancellato sia premendo il bottone che con la sequenza di tasti Control-C. Potremmo usare bind anche per raccogliere eventi generati dai "click" sul mouse, o quando nella finestra entra o esce il cursore del mouse o ancora quando una finestra diviene visibile. Per ulteriori informazioni su questo punto si rimanda al manuale di Tkinter. La cosa più difficile è di solito indovinare l'esatto formato della descrizione di un evento.

Un breve messaggio

Possono essere visualizzati brevi messaggi indirizzati all'utente usando MessageBox [Area messaggio, N.d.t.]. Questo è assai facile in Tkinter usando le funzioni del modulo tkMessageBox nel modo seguente:

import tkMessageBox
tkMessageBox.showinfo("Intestazione della finestra", "Questo e' il messaggio") 
Sono disponibili anche aree messaggio per errori, avvisi, per rispondere Si/No, utilizabili medianti varie funzioni showXXX. Si distinguono attraverso differenti icone e tipi di bottoni. Le ultime due varianti usano funzioni askXXX invece di showXXX e restituiscon un valore per indicare quale opzione è stata scelta dall'utente:
res = tMessageBox.askokcancel("Quale?", "Hai finito?")
print res

Ecco alcune delle finestre per messaggi di Tkinter:

Finestra informativa   Finestra per errori   Finestra Si/No

Alla maniera di Tcl

Dato che abbiamo confrontato Python con Tcl per tutta la parte iniziale di questo testo, potrebbe essere interessante mostrare come si presenta il primo esempio di Label e Button nella forma originale di Tcl/Tk:

Label .lCiao -text "Ciao gente"
Button .bHello -text Esci -command "exit"
wm title . Hello
pack .lCiao .bHello

Come vedete è assai conciso. La gerarchia di widget viene creata tramite una convenzione di scelta dei nomi in cui '.' costituisce il widget di primo livello. Come abbiamo già visto in Tcl i widget sono comandi le cui proprietà vengono passate come argomenti. La traduzione dagli argomenti dei widget agli argomenti passati per nome della versione Python è intenzionalmente ovvia. Ciò significa che potrete usare la documentazione di Tcl/Tk (disponibile in gran quantità!) per risolvere problemi di programmazione con Tkinter: si tratta in gran parte dei casi di una semplice traduzione.

Non parlerò oltre di Tcl/Tk. Ma prima di concludere voglio mostrare una tecnica assai comune per assemblare applicazioni GUI con Tkinter sotto forma di oggetti.

Strutturare applicazioni come oggetti

Nella programmazione di applicazioni GUI è usuale strutturare l'intera applicazione come classe. Questo porta a domandarsi come è possibile inserire i widget dell'applicazione Tkinter all'interno della classe. Ci sono due possibilità: possiamo dichiarare l'applicazione come una sottoclasse della classe Frame di Tkinter, oppure possiamo definire un elemento della classe contenente il riferimento alla finestra di primo livello. Il secondo metodo è quello più usato in altri toolkit e quindi lo useremo anche qui. Potete trovare un esempio dell'altro metodo nel capitolo Programmazione ad eventi. In quell'esempio viene anche mostrato l'uso elementare del widget Text di Tkinter: un widget incredibilmente versatile.

Convertiremo l'esempio precedente che usa un campo Entry, un bottone Cancella ed un bottone Esci, in una struttura ad oggetti. Prima di tutto creiamo una classe Applicazione ed inseriamo all'interno del costruttore la definizione della parte visuale dell'interfaccia grafica.

Assegnamo il Frame corrispondente a self.finPrinc, in modo da consentire agli altri metodi della classe di accedere al Frame di primo livello. Altri widget ai quali può essere necessario accedere (ad esempio il campo di immissione dei caratteri) vengono analogamente assegnati a variabili membri della classe. Usando questo metodo i gestori degli eventi divengono metodi della classe applicazione ed hanno tutti accesso ad ogni altra variabile dell'applicazione (anche se in questo caso non ve ne è nessuna) attraverso il riferimento self. Ne risulta una naturale integrazione dell'interfaccia grafica con i restanti oggetti dell'applicazione:

from Tkinter import *
     
class AppCanc:
   def __init__(self, padre=0):
      self.finPrinc = Frame(padre)
      # crea il widget di immissione del testo
      self.immTesto = Entry(self.finPrinc)
      self.immTesto.insert(0,"Ciao gente")
      self.immTesto.pack(fill=X)
      
      # adesso aggiungi i due bottoni
      # usiamo una coppia di frame uno dentro l'altro per dare
      # un effetto di scanalatura
      fEsterno = Frame(self.finPrinc, border=1, relief="sunken")
      fBottoni = Frame(fEsterno, border=1, relief="raised")
      bCanc = Button(fBottoni, text="Cancella", 
                      width=8, height=1, command=self.cancTesto)
      bEsci = Button(fBottoni, text="Esci", 
                      width=8, height=1, command=self.finPrinc.quit)
      bCanc.pack(side="left", padx=15, pady=1)
      bEsci.pack(side="right", padx=15, pady=1)
      fBottoni.pack(fill=X)
      fEsterno.pack(fill=X)
      self.finPrinc.pack()

      # impostra il titolo
      self.finPrinc.master.title("Cancella")
      
   def cancTesto(self):
      self.immTesto.delete(0,END)
      
app = AppCanc()
app.finPrinc.mainloop()

E questo è il risultato

Versione OO

Il risultato è assai simile alla precedente versione, sebbene abbiamo un po' modificato il frame sottostante per dargli un gradevole aspetto scanalato ed abbiamo impostato la larghezza dei bottoni in modo da farli apparire più simili all'esempio che segue, realizzato usando wxPython.

Naturalmente possiamo strutturare come oggetto non solo l'applicazione principale. Potremmo, ad esempio, creare una classe basata su un Frame che contiene un insieme standard di bottoni e riusare la classe per costruire finestre di dialogo. Potremmo anche creare finestre di dialogo più complete ed utilizzarle in vari progetti diversi. O ancora possiamo estendere le funzione di un widget standard definendo una sottoclasse, ad esempio per creare un bottone che cambia colore a seconda del suo stato. Questo è ciò che èstato fatto con il toolkit Python Mega Widgets (PMW) che è un'estensione di Tkinter che può essere liberamente scaricata.

wxPython, un'alternativa

Esistono molti altri toolkit per creazione di GUI ma uno dei più popolari è il toolkit wxPython che, a sua volta, è costruito intorno al toolkit C++ wxWindows. wxPython è un toolkit assai più tipico di Tkinter. Fra l'altro fornisce funzionalità standard aggiuntive rispetto al semplice Tkinter, ad esempio: barre di stato, etichette di suggerimento ed altre cose che usando Tkinter dovrebbero essere costruite programmando. Useremo adesso wxWorks per ricreare l'esempio precedente.

Non forniremo dettagli approfonditi: se volete saperne di più sul funzionamento di wxPython dovrete scaricare il pacchetto dal sito web di wxPython. [il pacchetto di wxPython deve comunque essere scaricato se volete provare l'esempio seguente in quanto non viene distribuito normalmente con Python. N.d.T.]

In termini generali il toolkit definisce un quadro di riferimento che ci cosente di creare finestre, riempirle con elementi di controllo ed associare metodi agli elementi di controllo. Poiché è totalmente orientato agli oggetti occorre usare metodi e non funzioni. L'esempio è il seguente:

from wxPython.wx import *

# --- Definisci un Frame che costituisce la finestra principale ---
class FrameCiao(wxFrame):
   def __init__(self, padre, ID, titolo, pos, dimens):
        wxFrame.__init__(self, padre, ID, titolo, pos, dimens)
	# occorre un Panel per aggiustare lo sfondo
        panel = wxPanel(self, -1)

	# Si creano i widget per il testo ed i bottoni
	self.tCiao = wxTextCtrl(panel, -1, "Ciao gente", (3,3), (185,22))
        bottone = wxButton(panel, 10, "Cancella", (15, 32))
        bottone = wxButton(panel, 20, "Esci", (100, 32))

	# Poi si collega il bottone al gestore
        EVT_BUTTON(self, 10, self.OnCancella)
        EVT_BUTTON(self, 20, self.OnEsci)
	
   # questi sono i gestori degli eventi
   def OnCancella(self, evento):
       self.tCiao.Clear()
       
   def OnEsci(self, evento):
       self.Destroy()

# --- Definisci l'oggetto applicazione ---
# Notate che tutti i programmi wxPython DEVONO definire
# una classe applicazione derivata da wxApp
class AppCiao(wxApp):
   def OnInit(self):
       frame = FrameCiao(NULL, -1, "Ciao", (200,50),(200,90) )
       frame.Show(true)
       # self.setTopWindow(frame)
       return true

# Si crea l'istanza e si attiva il loop degli eventi
AppCiao().MainLoop()

Ed ecco il risultato:

wxPython Hello program

Notate che abbiamo usato una convenzione per i nomi dei metodi che vengono richiamati dagli elementi del toolkit: OnXXXX. Notate anche le funzioni EVT_XXX che legano gli eventi ai widget: ne esiste un'intera famiglia. wxPython fornisce un ampio numero di widget, molti più di Tkinter, e con essi potete costruire interfacce assai sofisticate. Putroppo essi tendono a preferire il posizionamento basato su coordinate che dopo un po' diventa assai noioso. È anche possibile usare uno schema assai simile a quello del packer di Tkinter, ma non è molto ben documentato. Esiste un ambiente di programmazione grafica commerciale e forse qualcuno presto ne diffonderà uno di libero uso.

Per inciso può essere interessante notare che questo esempio e quello molto simile che usa Tkinter presentato sopra hanno entrambi esattamente lo stesso numero di linee di codice eseguibile: 21.

Per concludere, se dovete programmare una semplice interfaccia grafica per un programma a linee di testo, Tkinter dovrebbe rappresentare una soluzione di minimo sforzo. Se invece dovete programmare un'applicazione grafica complessa e portabile su altre piattaforme prendete in considerazione wxPython.

Fra gli altri toolkit si possono citare: MFC, pyQt, pyGTK, quest'ultimo al momento è disponibile solo per Linux anche se potrebbe essere portato facilmente in ambiente Windows dato che la libreria GTK sulla quale si basa è già disponibile su Windows. Infine esiste "curses" che è una specie di GUI di tipo testuale. Molte delle nozioni che abbiamo appreso relativamente a Tkinter valgono anche per gli altri toolkit ma ciascuno ha le sue caratteristiche ed idiosincrasie. Sceglietene uno, imparate ad usarlo e godetevi il creativo mondo della programmazione grafica.

Dato che non era nostra intenzione scrivere un manuale di riferimento per Tkinter ma solo una breve introduzione, questo per il momento può bastare. Potete guardare il capitolo dedicato a Tkinter nel sito web di Python per altri link sull'argomento.

Sono anche reperibili numerosi testi relativi a Tcl/Tk ed almeno uno su Tkinter. E comunque ne riparleremo nel capitolo sull'esempio concreto, dove mostreremo un modo per incapsulare un programma di tipo batch in una interfaccia grafica per renderlo più facilmente usabile.


Precedente Successivo Indice
 


Se avete domande o suggerimenti relativi a questa pagina mandate un e-mail all'autore: alan.gauld@btinternet.com o al traduttore italiano: lfini@arcetri.astro.it