Tkinter per sopravvivere

Autore: M. Piai
Data: 2005-03-01
Aggiornamenti:2005-03-02, 2005-03-04, 2005-03-13, 2005-03-14, 2005-03-15, 2005-03-16, 2005-03-17, 2005-03-19, 2005-03-20, 2005-03-23

Indice


1   Premessa

Il presente articolo trova la sua motivazione nella quasi totale assenza di documentazione in lingua italiana sugli argomenti trattati (programmazione GUI in Python, usando le librerie Tk).

Nella speranza di fornire un utile servizio alla comunità, l'autore ha sostanzialmente tradotto ampi brani dell'opera di Stephen Ferg citata nella sezione denominata «Copyright».

Ovviamente, alcuni contenuti e l'ordine degli argomenti sono stati adattati ai gusti dell'autore del presente articolo. Gli errori presenti sono di conseguenza esclusiva responsbilità di quest'ultimo, che se ne scusa anticipatamente.

Nelle successive versioni dell'articolo verranno introdotti ulteriori esempi originali, e una serie di esercizi proposti al lettore (con o senza soluzione).

3   Introduzione

3.1   I quattro compiti fondamentali della programmazione «UI»

Quando si sviluppa una interfaccia-utente («user interface», «UI») c'è un insieme standard di compiti da eseguire:

  1. Si deve specificare l'aspetto dell'interfaccia. Ossia, bisogna scrivere del codice che determina ciò che l'utente vedrà sullo schermo dell'elaboratore.
  2. Si deve decidere il funzionamento dell'interfaccia. Ossia, bisogna scrivere del codice che realizzi i compiti del programma.
  3. Si deve accoppiare l'aspetto con il funzionamento. Ossia, bisogna scrivere del codice che associ ciò che l'utente vede al codice che è stato scritto per eseguire i compiti del programma.
  4. Infine, bisogna scrivere del codice che rimanga in attesa di ingresso dall'utente.

3.2   Un po' di lessico GUI

La programmazione GUI ha uno speciale lessico attinente ai compiti fondamentali sopra descritti:

  1. Si specifica l'aspetto dell'interfaccia-utente mediante la descrizione dei widget che si vogliono presentare, nonché le loro relazioni spaziali (ossia, se un widget sta sopra o sotto, o a destra o a sinistra, di altri widget).
  2. Le parti di codice che concretamente realizzano il lavoro dell'interfaccia si chiamano gestori di eventi («event handlers»). Gli eventi sono gli ingressi, come le pressioni dei tasti dei dispositivi di puntamento (o «topi», o «mouse») o della tastiera. Queste parti si chiamano «gestori» in quanto «gestiscono» (nel senso che «reagiscono a») tali eventi.
  3. L'accoppiamento fra un gestore di eventi e un widget si dice collegamento (o «binding»). Grossomodo, il processo di collegamento implica l'associazione di tre diverse entità:
    1. un tipo di evento (ad esempio la pressione del tasto sinistro del dispositivo di puntamento, oppure la pressione del tasto [Invio] sulla tastiera),
    2. un widget (ad esempio un pulsante), e
    3. del codice di gestione degli eventi.

Per esempio, si potrebbe collegare (a) una singola pressione del tasto sinistro del dispositivo di puntamento al (b) pulsante «CHIUDI» sullo schermo e al (c) codice denominato «chiudiProgramma», il quale chiude la finestra e termina il programma.

  1. Il codice che attende l'ingresso si chiama ciclo degli eventi («event loop»).

3.3   A proposito del ciclo degli eventi

In certi film americani, in ogni piccola città c'è una vecchietta che passa tutto il suo tempo a guardare dalla finestra. Ella osserva tutto ciò che accade nel vicinato. Quasi tutto quello che vede è di scarso interesse, ovviamente - solo gente che va e viene per strada. Ma qualcosa di interessante c'è - tipo una bella lite fra gli sposini al di là della strada. Quando qualcosa di interessante accade, la nostra vecchina da guardia va immediatamente a telefonare al giornale o alla polizia oppure ai vicini.

Il ciclo degli eventi assomiglia a questa vecchietta. Il ciclo degli eventi passa tutto il tempo a osservare gli eventi che vanno e vengono, e li vede tutti. Gran parte di essi sono privi di interesse, perciò quando li vede lui li ignora. Ma se vede qualcosa di interessante - un evento che lui sa essere interessante in quanto ad esso è stato collegato un gestore di eventi - allora il ciclo subito chiama a rapporto il gestore di eventi e gli notifica l'avvenimento.

4   Il primo programma UI

4.1   Funzionamento del programma

Il programma che segue introduce elementarmente alla programmazione UI mostrando in che modo i concetti fondamentali suesposti possano essere realizzati in maniera estremamente semplice. Il programma non usa Tkinter né alcun altro tipo di tecnica GUI. Semplicemente presenta un menù sulla console e preleva un ingresso elementare da tastiera. In ogni caso, come è evidente, esso realizza i quattro compiti fondamentali della programmazione UI.

4.2   Codice sorgente del programma

# ---- compito 2:  definizione del codice di gestione degli eventi
def gestisci_A():
  print "Sbagliato! Prova ancora!"

def gestisci_B():
  print "Bravissimo! Il ranuncolo è un fiore!"
         
def gestisci_C():
  print "Sbagliato! Prova ancora!"

# ---- compito 1: definizione dell'aspetto sullo schermo
print "\n"*100   # pulisci lo schermo
print "             UN QUIZ ESTREMAMENTE APPASSIONANTE"
print "==========================================================="
print "Premere il tasto corrispondente alla risposta, poi [Invio]."
print
print "    A.  Animale"
print "    B.  Vegetale"
print "    C.  Minerale"
print
print "    X.  Termina questo programma"
print
print "==========================================================="
print "Che cos'è un 'Ranuncolo'?"
print

# ---- compito 4: ciclo degli eventi. Ciclo infinito, in attesa di eventi
while 1:

  # Osserva il successivo ingresso
  risposta = raw_input().upper()

  # ---------------------------------------------------------
  # compito 3: associare eventi della tastiera 'interessanti'
  # con i gestori di eventi. Una semplice forma di
  # collegamento
  # ---------------------------------------------------------
  if risposta == "A":
    gestisci_A()
  if risposta == "B":
    gestisci_B()
  if risposta == "C":
    gestisci_C()
  if risposta == "X": 
    # pulisce lo schermo e termina il ciclo degli eventi
    print "\n"*100
    break

  # Si noti che tutti gli altri eventi sono giudicati non
  # interessanti, e percio` ignorati

5   Il più semplice programma Tkinter possibile: tre istruzioni!

Dei quattro compiti GUI fondamentali discussi in precedenza, il seguente programma ne realizza solo uno: il ciclo degli eventi.

  1. La prima istruzione importa il modulo Tkinter, affinché sia disponibile per l'uso. Si noti la particolare forma di importazione (from Tkinter import *) che implica la non necessità di qualificare ciò che si usa del modulo Tkinter mediante il prefisso Tkinter.

  2. La seconda istruzione crea una finestra principale («toplevel»). Tecnicamente l'istruzione crea un'istanza della classe Tkinter.Tk.

    Questa finestra principale è la componente GUI di massimo livello per qualsiasi applicazione Tkinter. Per convenzione la finestra principale è detta radice («root»),

  3. La terza istruzione esegue il ciclo principale (ossia il ciclo degli eventi) come metodo dell'oggetto radice. Quando il ciclo principale si esegue esso attende gli eventi che accadono nell'oggetto radice. Se avviene un evento allora esso viene gestito e il ciclo prosegue, continuando la sua attesa per il successivo evento. L'esecuzione del ciclo continua sinché nella finestra radice non si verifica un evento «destroy» (o distruggi). Un tale evento chiude la finestra. Quando la radice è distrutta la finestra si chiude e il ciclo degli eventi termina.

5.1   Funzionamento del programma

Quando si esegue il programma, grazie alla libreria Tk si vede la finestra principale automaticamente decorata con widget per minimizzare, massimizzare e chiudere la finestra. Possono essere provati per verificarne il funzionamento previsto:

[txs_01_010.png]

Attivando il widget di chiusura («close», rappresentato dalla «x» nel riquadro alla sinistra della barra del titolo) si genera un evento destroy. Tale evento termina il ciclo principale, e poiché non ci sono altre istruzione di seguito a radice.mainloop() il programma non ha altri compiti da eseguire e termina.

5.2   Codice sorgente del programma

from Tkinter import *   ### (1)

radice = Tk()           ### (2) 
radice.mainloop()       ### (3)

6   Specificare l'aspetto del GUI

Ora è necessario affrontare un altro dei quattro compiti fondamentali: specificare come appare il GUI.

Nel programma seguente vengono introdotti tre importanti concetti della programmazione Tkinter:

D'ora in poi si distingueranno i contenitori dai widget. Per chiarire i termini, per widget si intende una componente GUI visibile (di solito) e che esegue delle azioni. Un contenitore («container») invece è semplicemente un involucro - una specie di recipiente - in cui è possibile porre i widget.

Tkinter mette a disposizione una varietà di contenitori. La tela («canvas») ad esempio è un contenitore per applicazioni orientate al disegno. Il contenitore più usato è comunque il quadro («frame»).

I quadri vengono messi a disposizione da Tkinter mediante una classe chiamata Frame. Un'espressione del tipo:

Frame(mioGenitore)

crea un'istanza della classe Frame (ossia crea un quadro), e associa tale istanza con il suo genitore («parent»), mioGenitore. In altri termini: l'espressione aggiunge un quadro figlio alla componente mioGenitore.

Sicché nel programma l'istruzione (1):

mioContenitore1 = Frame(radice)

crea un quadro il cui genitore è radice e gli dà nome mioContenitore1. In sintesi: essa crea un contenitore in cui si può mettere dei widget. (Nel caso in esame non si metterà alcun widget nel contenitore. Ciò verrà fatto nei programmi seguenti.)

L'istruzione successiva (2) impacchetta («pack») mioContenitore1:

mioContenitore1.pack()

In parole povere, l'impacchettamento è un processo finalizzato a stabilire una relazione visuale fra una componente GUI e il sui genitore. Se non lo si impacchetta, è impossibile presentare un componente.

L'impacchettamento si realizza in Tkinter invocando il gestore di geometria «pack». Essenzialmente si tratta di una «API» («Interfaccia di programmazione per le applicazioni») - ossia un mezzo per dialogare con Tkinter - che ha lo scopo di indicare a Tkinter come si desidera che contenitori e widget siano presentati. Tkinter supporta tre gestori di geometria («geometry manager»): «pack», «grid» e «place». Pack e (in subordine) grid sono i più usati, essendo quelli di più semplice utilizzo. Tutti gli esempi del presente articolo usano il gestore pack.

Insomma, ecco uno schema di base per la programmazione Tkinter che si ripeterà durante questo articolo:

  1. si crea un'istanza (di un widget o di un contenitore) e la si associa al genitore;
  2. l'istanza viene impacchettata.

6.1   Funzionamento del programma

Eseguendo il programma, esso apparirà molto simile al precedente, a parte il fatto che si vede di meno. Questo accade in quanto...

6.2   I quadri sono «elastici»

Un quadro è in sostanza un contenitore. L'interno di un contenitore - lo «spazio» dentro il contenitore - si chiama cavità (si tratta di un termine tecnico mutuato dalla libreria Tk).

La cavità è «elastica», come la gomma. Se non si specifica una dimensione minima o massima per il quadro, la cavità si adatterà a ciò che nel quadro è contenuto.

Nel programma precedente, poiché non vi era stato messo nulla, la radice veniva presentata con le sue dimensioni predefinite.

Ma nel programma in esame è stato messo qualcosa nella cavità della radice - è stato inserito mioContenitore1. Sicché la cavità della radice si stringe per adattarsi alle dimensioni di mioContenitore1. Ma poiché non è stato messo alcun widget in mioContenitore1, e non avendo specificato una dimensione minima per mioContenitore1, la cavità della radice si stringe fino a sparire. Ecco il motivo per cui non si vede nulla sotto la barra del titolo.

Nei programmi successivi si porranno dei widget e altri contenitori in mioContenitore1, e si osserverà come mioContenitore1 si espande per adattarsi ad essi.

6.3   Codice sorgente del programma

from Tkinter import *

radice = Tk()

mioContenitore1 = Frame(radice)  ### (1)
mioContenitore1.pack()           ### (2)

radice.mainloop()       

7   Widget: esempio introduttivo

Nel programma seguente verrà per la prima volta creato un widget e verrà messo in mioContenitore1.

Il widget (1) sarà un pulsante - ossia sarà un'istanza della classe Tkinter chiamata Button. L'istruzione:

pulsante1 = Button(mioContenitore1)

crea un pulsante, lo nomina pulsante1 e lo accoppia al suo genitore, l'oggetto contenitore denominato mioContenitore1.

I widget hanno molti attributi (2) (3), i quali vengono conservati in un dizionario 5 - locale al widget - il quale costituisce lo spazio dei nomi 6 del widget.

Avvertenza

Esiste una possibilità di equivoco nell'uso della parola attributo in questo contesto.

Nella terminologia della programmazione a oggetti, come già accennato, gli attributi (attributi-dati) sono essenzialmente delle variabili che appartengono a un'istanza di un oggetto classe. L'accesso all'attributo si ottiene in questo caso mediante la cosiddetta notazione puntata, ad esempio:

....
unaIstanza = unaClasse()
....
if unaIstanza.unAttributo == "un valore":
  ....

Nella terminologia del modulo Tkinter, a volte si usa il termine attributo per indicare un'opzione di una componente GUI; tecnicamente, non si tratta di un attributo, ma di un elemento di un dizionario predefinito per la componente stessa, e l'accesso può avvenire utilizzando appunto la notazione tipica di tale struttura dati, come si vede negli esempi seguenti.

Comunque, dal contesto, si dovrebbe capire a quale significato ci si deve riferire di volta in volta.

Come tutti i widget anche i pulsanti posseggono attributi per controllarne le dimensioni, i colori di sfondo e primo piano, il testo che presentano, l'aspetto del bordo e così via. Nell'esempio vengono specificati solo due attributi di pulsante1: il colore di sfondo e il testo. A tal fine vengono assegnati dei valori al dizionario del pulsante mediante le chiavi "text" e "background":

pulsante1["text"] = "Ciao, Mondo!"
pulsante1["background"] = "green"

Ovviamente bisogna anche impacchettare pulsante1:

pulsante1.pack()

7.1   Alcuni utili termini tecnici

La relazione che intercorre fra un contenitore e i widget in esso contenuti è talvolta definita «genitore/figlio», altre volte «padrone/schiavo» («master/slave»).

7.2   Funzionamento del programma

Avviando il programma si osserva che Container1 ora contiene un pulsante verde con il testo «Ciao, Mondo!». Attivando il pulsante non accade nulla, poiché non è stato specificato cosa debba accadere in tal caso (verrà specificato in seguito):

[txs_01_020.png]

Per il momento si deve chiudere la finestra - come già visto - attivando l'icona di chiusura sulla barra del titolo.

Si noti altresì come mioContenitore1 abbia adattato le sue dimensioni a pulsante1.

7.3   Codice sorgente del programma

from Tkinter import *

radice = Tk()

mioContenitore1 = Frame(radice)
mioContenitore1.pack()

pulsante1 = Button(mioContenitore1)   ### (1)
pulsante1["text"] = "Ciao, Mondo!"    ### (2)
pulsante1["background"] = "green"     ### (3)
pulsante1.pack()                      ### (4)

radice.mainloop()

8   Introduzione all'uso delle classi

Nel programma seguente si mostrerà come strutturare un'applicazione Tkinter come un insieme di classi.

Nel programma viene aggiunta una classe denominata MiaApp e una parte del codice viene spostato all'interno del metodo costruttore __init__. 7 In questa versione ristrutturata del programma si realizzano tre cose:

  1. Si definisce una classe (MiaApp) la quale definisce l'aspetto del GUI e le cose che si desidera che il GUI faccia. Tutto il codice necessario si trova nel corpo del metodo costruttore (__init__) della classe (1a).

  2. All'esecuzione del programma, per prima cosa viene creata un'istanza della classe. L'istruzione che crea l'istanza è:

    miaApp = MiaApp(radice)
    

    Nota

    Si osservi l'uso delle lettere: MiaApp (con la maiuscola) è il nome della classe, mentre miaApp è il nome dell'istanza (iniziale minuscola).

    Si noti altresì che l'istruzione passa radice come parametro effettivo al metodo costruttore (__init__) di MiaApp. Il metodo costruttore riconosce radice come valore del parametro formale mioGenitore (1a).

  3. Infine, viene eseguito il ciclo principale su radice.

8.1   A che scopo strutturare un'applicazione come una classe?

Uno dei motivi per usare le classi in un programma è semplicemente allo scopo di controllare meglio il programma stesso. Un programma organizzato in classi probabilmente è più facile da capire - specie se il programma non è piccolo.

Ma la considerazione più importante è forse che l'uso delle classi consente di evitare le variabili globali. Durante la crescita di un programma in sviluppo a un certo punto occorre quasi sempre condividere informazioni fra diversi gestori di eventi. Ciò è possibile mediante l'uso di variabili globali, ma si tratta di una tecnica inelegante. Un modo migliore è quello di usare le istanze (mediante le variabili self. 12), e quindi risulta necessario attribuire all'applicazione una struttura di classe. 8

Avvertenza

Il motivo di questa precoce introduzione della struttura di classe è semplicemente quello di spiegare la cosa per poi procedere ad altro. Ma nella realtà produttiva è spesso opportuno scegliere un diverso modo di procedere.

Molto spesso un programma Tkinter inizia il suo sviluppo come uno script elementare, con tutto il codice in linea come nei precedenti esempi. Poi, man mano che si intuiscono aspetti nuovi del problema, il programma cresce. Dopo un po' ci si trova a gestire molto codice. Probabilmente si sono introdotte delle variabili globali, forse molte variabili globali. A questo punto il programma comincia ad essere difficile da comprendere o modificare. Quando questo accade è il momento di refattorizzare il programma, ristrutturandolo mediante l'uso delle classi.

Peraltro, è anche possibile che - se si ha confidenza con il concetto di classe e se si hanno le idee chiare fin dal principio sull'aspetto generale del programma - si scelga di strutturare il programma mediante classi fin dal principio.

Oppure può accadere che nelle fasi iniziali dello sviluppo non si abbia un'idea precisa di che tipo di classi utilizzare - poiché ancora non è chiaro il problema né la soluzione. In tal caso, l'uso precoce delle classi rischia d'introdurre struttura superflua che semplicemente confonde il codice, impedisce la comprensione e in fin dei conti richiede ulteriore refattorizzazione.

In conclusione si tratta di gusti individuali, esperienza e circostanze. È meglio procedere come meglio ci si sente. E soprattutto - aprescindere dal metodo prescelto - non si deve temere l'eventualità di procedere a sostanziale refattorizzazione in caso di necessità.

8.2   Funzionamento del programma

All'esecuzione il programma appare esattamente identico al precedente. Nessuna funzionalità è stata aggiunta - solo il codice è stato ristrutturato.

8.3   Codice sorgente del programma

from Tkinter import *

class MiaApp:                         ### (1)
  def __init__(self, mioGenitore):    ### (1a)
    self.mioContenitore1 = Frame(mioGenitore)
    self.mioContenitore1.pack()
                
    self.pulsante1 = Button(self.mioContenitore1) 
    self.pulsante1["text"] = "Ciao, Mondo!"     
    self.pulsante1["background"] = "green"      
    self.pulsante1.pack()                              
                

radice = Tk()
miaApp = MiaApp(radice)  ### (2)
radice.mainloop()        ### (3)

9   Ancòra widget: accesso alternativo agli attributi

Nel precedente programma è stato creato un oggetto pulsante, pulsante1, e il testo e il colore di sfondo sono stati fissati direttamente (1):

self.pulsante1["text"] = "Hello, World!"
self.pulsante1["background"] = "green"

Nel prossimo programma verranno aggiunti altri tre pulsanti a mioContenitore1 usando delle tecniche alternative.

Per pulsante2 il processo è essenzialmente lo stesso (2), ma invece di accedere al dizionario dell'oggetto viene usato il metodo predefinito configure.

Per pulsante3 si osservi che il metodo configure accetta una molteplicità di parametri effettivi con parole chiave (3), sicché è possibile fissare più opzioni in una singola istruzione.

Negli esempi precedenti la preparazione di un pulsante era un processo a due fasi: prima si crea il pulsante, poi se ne fissano le proprietà. Però è possibile specificare le proprietà del pulsante nello stesso momento della sua creazione (4). Il widget Button (come tutti i widget) si aspetta come suo primo argomento il suo genitore: ma non si tratta di un parametro inteso come parola chiave, bensì un «parametro posizionale»; dopo di esso, se si vuole, è possibile aggiungere uno o più argomenti parole chiave per specificare le proprietà del widget.

9.1   Funzionamento del programma

Eseguendo il programma si osserva che mioContenitore1 ora contiene - oltre al pulsante verde - altri tre pulsanti:

[txs_01_030.png]

Nota

Si noti che mioContenitore1 si è allargato per ospitare gli altri tre pulsanti.

Si noti altresì che i pulsanti sono impilati l'uno sull'altro. Nel programma successivo se ne capirà il motivo, oltre a imparare come sistemarli in modo diverso.

9.2   Codice sorgente del programma

from Tkinter import *

class MiaApp:
  def __init__(self, genitore):
    self.mioContenitore1 = Frame(genitore)
    self.mioContenitore1.pack()

    self.pulsante1 = Button(self.mioContenitore1)
    self.pulsante1["text"] = "Ciao, Mondo!"   ### (1)
    self.pulsante1["background"] = "green"    ### (1) 
    self.pulsante1.pack()       

    self.pulsante2 = Button(self.mioContenitore1)
    self.pulsante2.configure(text = "Andiamo a fare un giro!") ### (2)
    self.pulsante2.configure(background = "tan")               ### (2)
    self.pulsante2.pack()       


    self.pulsante3 = Button(self.mioContenitore1)
    self.pulsante3.configure(text = "Vieni con me?",
                             background = "cyan")  ### (3)
    self.pulsante3.pack()       

    self.pulsante4 = Button(self.mioContenitore1,
                            text = "Addio!",
                            background = "red")    ### (4)
    self.pulsante4.pack()       
        

radice = Tk()
miaApp = MiaApp(radice)
radice.mainloop()

10   Impacchettamenti differenti

Nel programma precedente c'erano dei pulsanti impilati l'uno sull'altro. Probabilmente era meglio averli affiancati: nel programma seguente verrà fatto così, e si approfondirà il comportamento di pack().

L'impacchettamento è una maniera di controllare le relazioni visive fra le componenti. Nel seguito verrà utilizzata l'opzione side dell'impacchettatore pack() allo scopo di affiancare i pulsanti invece di impilarli. Ad esempio:

self.pulsante1.pack(side=LEFT)

Nota

LEFT (come RIGHT, TOP e BOTTOM) sono costanti di convenienza definite in Tkinter. Ossia, LEFT è in realtà Tkinter.LEFT - ma in conseguenza del modo in cui il modulo è stato importato non è necessario specificare il prefisso Tkinter..

10.1   Perché i pulsanti impilati nel precedente programma?

Si ricordi che nel precedente programma i pulsanti erano stati impacchettati senza specificare l'opzione side, e di conseguenza essi risultavano impilati. Ciò avveniva a causa del fatto che per predefinizione side ha valore TOP.

Di conseguenza, impacchettando pulsante1, esso veniva impacchettato sopra la cavità interna a mioContenitore1. Ciò lasciava la cavità interna a mioContenitore1 posizionata sotto pulsante1. In seguito, impacchettando pulsante2, esso veniva impacchettato sopra la cavità, ossia immediatamente al di sotto di pulsante1, lasciando la cavità posizionata al di sotto di pulsante2.

Se i pulsanti fossero stati impacchettati in un diverso ordine - per esempio, prima pulsante2 e poi pulsante1 - le loro posizioni sarebbero state invertite, e pulsante2 sarebbe stato sopra.

Sicché risulta evidente che uno dei modi per controllare l'aspetto del GUI è attraverso l'ordine in cui si impacchettano i widget nei contenitori.

10.2   Terminologia tecnica: «orientazione»

Con «orientazione verticale» si intende i lati TOP e BOTTOM. Con «orientazione orizzontale» si intende i lati LEFT e RIGHT.

Durante l'impacchettamento è possibile mischiare le orientazioni. Ad esempio si sarebbe potuto impacchettare un pulsante in verticale (ad esempio TOP) e l'altro in orizzontale (ad esempio LEFT).

Ma mischiare così le orientazioni in un contenitore non è una buona idea. Usando orientazioni miste è difficile prevedere quale sarà il risultato visivo, con risultati particolarmente sorprendenti in caso di ridimensionamento della finesta.

Dunque è una buona pratica il non mischiare le orientazioni nel medesimo contenitore. Il modo corretto per gestire GUI complicati, in cui è effettivamente necessario avere orientazioni multiple, è attraverso l'annidamento dei contenitori. 9

10.3   Funzionamento del programma

Eseguendo il programma si noteranno i pulsanti affiancati:

[txs_01_040.png]

10.4   Codice sorgente del programma

from Tkinter import *

class MiaApp:
  def __init__(self, genitore):
    self.mioContenitore1 = Frame(genitore)
    self.mioContenitore1.pack(side = LEFT)

    self.pulsante1 = Button(self.mioContenitore1)
    self.pulsante1["text"] = "Ciao, Mondo!"   ### (1)
    self.pulsante1["background"] = "green"    ### (1) 
    self.pulsante1.pack(side = LEFT)       

    self.pulsante2 = Button(self.mioContenitore1)
    self.pulsante2.configure(text = "Andiamo a fare un giro!") ### (2)
    self.pulsante2.configure(background = "tan")               ### (2)
    self.pulsante2.pack(side = LEFT)       


    self.pulsante3 = Button(self.mioContenitore1)
    self.pulsante3.configure(text = "Vieni con me?",
                             background = "cyan")  ### (3)
    self.pulsante3.pack(side = LEFT)       

    self.pulsante4 = Button(self.mioContenitore1,
                            text = "Addio!",
                            background = "red")    ### (4)
    self.pulsante4.pack(side = LEFT)       

        
radice = Tk()
miaApp = MiaApp(radice)
radice.mainloop()

11   Gestire gli eventi

È giunta l'ora di far fare qualche cosa ai pulsanti. Si ricordino gli ultimi due compiti della programmazione GUI: scrivere codice di gestione degli eventi per realizzare il nucleo operativo del programma, e collegare i gestori degli eventi agli eventi.

Nel prossimo programma si torna a una situazione molto semplice: il GUI contiene solo due pulsanti: «Conferma» e «Annulla».

Si rammenti che col termine collegamento si intende un processo di definizione di una connessione usualmente fra i seguenti enti:

Un gestore di eventi è un metodo o altro segmento di codice il quale gestisce gli eventi al loro avvenire. 10

In Tkinter il collegamento si realizza attraverso il metodo bind(), comune a tutti i widget di Tkinter. La forma di utilizzo del metodo è:

widget.bind(nome_del_tipo_di_evento, nome_del_gestore_di_eventi)

Il collegamento così realizzato prende il nome di collegamento di evento (o «event binding»). 11

Avvertenza

Prima di procedere è il caso di specificare una possibile fonte di equivoco. Il termine «pulsante» può significare due cose ben diverse:

1. un widget - ossia una componente GUI presentata sullo schermo dell'elaboratore;

2. un tasto del dispositivo di puntamento - di solito attivato mediante pressione con un dito.

Per evitare confusione, si useranno i termini «pulsante» nel primo caso e «tasto» nel secondo.

Si collegano (1) gli eventi <Button-1> (pressioni del tasto sinistro del dispositivo di puntamento) sul widget pulsante1 al metodo self.pulsante1Premuto. Se si preme il pulsante pulsante1 con il tasto sinistro del dispositivo di puntamento, il metodo self.pulsante1Premuto() viene invocato a gestire l'evento.

Nota

Sebbene non sia specificato in fase di collegamento (1), al metodo self.pulsante1Premuto() vengono passati due parametri effettivi. Il primo, ovviamente, è self (che è sempre il primo parametro passato ai metodi in Python); il secondo è un oggetto evento, Questa tecnica di collegamento agli eventi (ossia, l'uso del metodo bind()) passa sempre un oggetto evento come parametro effettivo.

In Python/Tkinter, al verificarsi di un evento, esso assume la forma di un oggetto evento. Si tratta di qualcosa di estremamente utile, poiché esso comporta tutta una serie di utili informazioni e metodi. È possibile esaminare l'oggetto evento allo scopo di scoprire che tipo di evento si sia verificato, il widget coinvolto e altre utili informazioni.

Dunque, cosa accade premendo pulsante1? Nel caso in esame si fa in modo che accada qualcosa di estremamente semplice, ossia esso alterna il suo colore fra il verde e il giallo (4).

Invece pulsante2 (il pulsante con il testo «Annulla») fa qualcosa di più utile: esso determina la chiusura della finestra. Tale comportamento viene realizzato (2) collegando la pressione del tasto sinistro del dispositivo di puntamento in corrispondenza di pulsante2 al metodo pulsante2Premuto(); quest'ultimo metodo (6) distrugge la finestra radice, innescando così una reazione a catena che distrugge tutti i figli e i discendenti della radice. In breve viene distrutta ogni parte del GUI.

È evidente che a tal fine miaApp deve sapere di chi è figlia, quindi (7) si rende necessario aggiungere un'istruzione al codice del costruttore per far sì che miaApp si ricordi il suo genitore.

11.1   Funzionamento del programma

All'esecuzione si osservano due pulsanti:

[txs_01_050.png]

Premendo il pulsante «Conferma» se ne varia il colore:

[txs_01_060.png]

Premendo «Annulla» si termina l'applicazione.

Inoltre, se si preme il tasto [Tab] sulla tastiera. si può notare che il cosiddetto «fuoco» si alterna fra i due pulsanti. Tuttavia la pressione del tasto [Invio] non sortisce alcun effetto. Ciò accade in quanto si sono realizzati i collegamenti coi widget solo per le pressioni dei tasti del dispositivo di puntamento (e non per gli eventi relativi alla tastiera). Tra breve la lacuna sarà colmata.

Si noti anche che le dimensioni dei pulsanti seguono la larghezza del testo in essi contenuto, il che non è esteticamente soddisfacente. Anche in questo caso si provvederà nei successivi esempi.

11.2   Codice sorgente del programma

from Tkinter import *

class MiaApp:
  def __init__(self, genitore):
    self.mioGenitore = genitore  ### (7) ricorda: il genitore e` radice
    self.mioContenitore1 = Frame(genitore)
    self.mioContenitore1.pack()

    self.pulsante1 = Button(self.mioContenitore1)
    self.pulsante1.configure(text = "Conferma", background = "green")
    self.pulsante1.pack(side = LEFT)  
    self.pulsante1.bind("<Button-1>", self.pulsante1Premuto) ### (1)

    self.pulsante2 = Button(self.mioContenitore1)
    self.pulsante2.configure(text = "Annulla", background = "red")   
    self.pulsante2.pack(side = RIGHT)
    self.pulsante2.bind("<Button-1>", self.pulsante2Premuto) ### (2)
                
  def pulsante1Premuto(self, evento):           ### (3)
    if self.pulsante1["background"] == "green": ### (4)
      self.pulsante1["background"] = "yellow"
    else:
      self.pulsante1["background"] = "green"
        
  def pulsante2Premuto(self, evento):  ### (5)
    self.mioGenitore.destroy()         ### (6)

                
radice = Tk()
miaApp = MiaApp(radice)
radice.mainloop()

12   Fuoco ed eventi di tastiera

Nel precedente programma l'utente induceva i pulsanti all'azione mediante i tasti del dispositivo di puntamento, ma nulla accadeva se si premevano i tasti sulla tastiera. Nel programma che ora verrà introdotto, invece, si illustrerà come farli reagire anche agli eventi di tastiera.

Ma, per prima cosa, è necessario familiarizzare col concetto di fuoco (o «focus»).

12.1   Il concetto di fuoco

Chi conosce la mitologia greca saprà chi erano le Arpìe. Erano tre vecchie le quali avevano il controllo dei destini degli esseri umani. Ogni vita umana era un filo nelle loro mani, e quando esse lo tagliavano la vita finiva.

La particolarità delle Arpìe era che esse avevano un solo occhio in tre. Quella delle tre che aveva l'occhio vedeva, e comunicava alle altre due ciò che vedeva. L'occhio veniva passato dall'una all'altra, sicché esse vedevano a turno. Ovviamente se qualcuno riusciva a rubare l'occhio otteneva un consistente vantaggio per poter trattare con le Arpìe.

Il fuoco è ciò che consente ai widget del GUI di vedere gli eventi di tastiera. Il fuoco è per i widget ciò che l'occhio era per le Arpìe.

Solamente un widget alla volta può essere a fuoco; il widget che «è a fuoco» è quello che vede, e reagisce a, gli eventi di tastiera. «Mettere a fuoco» un widget è il processo di dare il fuoco al widget.

Nel programma in esame, ad esempio, il GUI ha due pulsanti: «Conferma» e «Annulla». Si supponga di premere [Invio]. Tale pressione corrisponderà ad un evento intercettato dal pulsante «Conferma», il che indicherà che l'utente ha accettato l'opzione? Oppure sarà inviato al pulsante «Annulla», il che indicherà che l'utente ha interrotto l'operazione? La risposta dipende da chi ha il fuoco, ossia da quale dei due pulsanti (eventualmente) è a fuoco.

Come l'occhio delle Arpìe, che passa dall'una all'altra, il fuoco può passare da un widget all'altro. Ci sono molte maniere di spostare il fuoco da un widget all'altro. Si può fare mediante il dispositivo di puntamento. È possibile mettere a fuoco un widget utilizzando il tasto sinistro del dispositivo di puntamento.

Nota

Per lo meno, il comportamento descritto (denominato «click to type») è quello comune negli ambienti Windows e Macintosh, per Tk e Tkinter. Altri sistemi possono utilizzare altre convenzioni come il cosiddetto «focus follows mouse» in cui il widget che si trova sotto il puntatore viene automaticamente messo a fuoco, senza che sia necessario premere alcun tasto. Eventualmente è possibile ottenere questo effetto in Tk mediante la procedura tk_focusFollowsMouse.

Un altra maniera di spostare il fuoco è mediante utilizzo della tastiera. L'insieme dei widget che sono in grado di ricevere il fuoco viene mantenuto in una lista circolare (detta «ordine di attraversamento») nella sequenza in cui i widget sono stati creati. Premendo il tasto [Tab] si sposta il fuoco dalla posizione attuale (eventualmente nessuna) al successivo widget della lista. Al termine della lista il fuoco ritorna al widget in testa alla lista. La combinazione [Maiuscole]+[Tab] sposta il fuoco in senso opposto all'interni della lista.

Quando un pulsante è e fuoco si nota una piccola cornice tratteggiata attorno al pulsante oppure al testo del pulsante. Per verificarlo si esegua il precedente programma. All'avvio nessuno dei pulsanti è a fuoco, perciò non si vede nessuna cornice tratteggiata. Se si preme [Tab] si osserva che appare la cornice attorno al pulsante a sinistra, il che indica che è stato messo a fuoco. Premendo ripetutamente [Tab] il fuoco passa al pulsante successivo, e quando raggiunge l'ultimo torna al primo. (In realtà il programma ha solamente due pulsanti, perciò il fuoco si alterna fra essi.)

Nel programma, per fare in modo che il pulsante «Conferma» sia a fuoco sin dall'inizio (0), si usa il metodo focus_force(), che forza il fuoco sul pulsante «Conferma». All'esecuzione del programma si osserverà che il pulsante «Conferma» è a fuoco dal momento in cui l'applicazione inizia a funzionare.

12.2   Eventi di tastiera

Nel programma precedente i pulsanti reagivano solo a un evento di tastiera - la pressione del tasto [Tab] - il quale provocava l'alternarsi del fuoco fra i due pulsanti. Ma la pressione del tasto [Invio] non provocava alcun effetto. Ciò avveniva perché erano stati collegati solamente eventi relativi al dispositivo di puntamento - e non eventi di tastiera - ai pulsanti.

Le istruzioni (1)(2) per collegare eventi di tastiera ai pulsanti sono molto semplici - hanno lo stesso formato celle istruzioni per collegare gli eventi relativi al dispositivo di puntamento. L'unica differenza è che il nome dell'evento è quello di un evento di tastiera (in questo caso <Return>) invece di un evento relativo al dispositivo di puntamento.

Volendo che la pressione del tasto [Invio] sulla tastiera abbia lo stesso effetto della pressione del tasto sinistro del dispositivo di puntamento sul widget, si deve collegare lo stesso gestore di eventi a entrambi i tipi di evento.

Il programma illustra come sia possibile collegare più tipi di evento allo stesso widget (ad esempio un pulsante). Inoltre è possibile collegare più coppie widget/evento al medesimo gestore di evento.

Avendo collegato i pulsanti a più tipi di evento, si noti altresì come sia possibile (3)(4) ricavare informazioni a proposito di un oggetto evento. È possibile (5) passare oggetti evento a una funzione descrivi_evento la quale scriverà (6) informazioni sull'evento; le informazioni sono ottenute dagli attributi dell'oggetto evento.

Nota

Per poter leggere le informazioni sulla console è necessario eseguire il programma dalla linea di comando, non tramite meccanismi che nascondono l'invocazione del programma (come ad esempio voci di menù o icone).

12.3   Funzionamento del programma

All'esecuzione del programma si osservano due pulsanti. Premendo il sinistro, oppure premendo il tasto [Invio] quando il pulsante è a fuoco, se ne cambia il colore. Premendo il destro, oppure premendo il tasto [Invio] quando il pulsante è a fuoco, si termina l'applicazione. In ciascuno dei casi si dovrebbe poter vedere in console un messaggio che riporta il tempo e il tipo dell'evento.

12.4   Codice sorgente del programma

from Tkinter import *

class MiaApp:
  def __init__(self, genitore):
    self.mioGenitore = genitore
    self.mioContenitore1 = Frame(genitore)
    self.mioContenitore1.pack()

    self.pulsante1 = Button(self.mioContenitore1)
    self.pulsante1.configure(text = "Conferma", background = "green")
    self.pulsante1.pack(side = LEFT)
    self.pulsante1.focus_force()  ### (0)
    self.pulsante1.bind("<Button-1>", self.pulsante1Premuto)
    self.pulsante1.bind("<Return>", self.pulsante1Premuto)  ### (1)

    self.pulsante2 = Button(self.mioContenitore1)
    self.pulsante2.configure(text = "Annulla", background = "red")
    self.pulsante2.pack(side = RIGHT)
    self.pulsante2.bind("<Button-1>", self.pulsante2Premuto)
    self.pulsante2.bind("<Return>", self.pulsante2Premuto)  ### (2)

  def pulsante1Premuto(self, evento):
    descrivi_evento(evento)  ### (3)
    if self.pulsante1["background"] == "green":
      self.pulsante1["background"] = "yellow"
    else:
      self.pulsante1["background"] = "green"

  def pulsante2Premuto(self, evento):
    descrivi_evento(evento)  ### (4)
    self.mioGenitore.destroy()

def descrivi_evento(evento):  ### (5)
  """Scrive una descrizione dell'evento, in base ai suoi attributi
  """
  nome_evento = {"2": "Pressione tasto tastiera",
                 "4": "Pressione pulsante GUI"}
  print "Tempo:", str(evento.time)  ### (6)
  print "Tipo di evento: " + str(evento.type),\
    "(" + nome_evento[str(evento.type)] + ")",\
    "-- Id. widget collegato a evento: " + str(evento.widget),\
    "-- Simbolo tasto collegato a evento: " + str(evento.keysym)


radice = Tk()
miaApp = MiaApp(radice)
radice.mainloop()

13   Collegamento di comando

In precedenza è stato introdotto il collegamento di evento («event binding»). Esiste anche un altro modo per collegare un gestore di eventi a un widget, detto collegamento di comando (o «command binding»), che verrà ora illustrato.


Si rammenti che nei programmi precedenti, l'evento <Button-1> relativo al dispositivo di puntamento era stato collegato al pulsante. Button è sinonimo di ButtonPress, e ButtonPress è un evento distinto da ButtonRelease. L'evento ButtonPress corrisponde all'atto di premere un tasto del dispositivo di puntamento, senza però rilasciarlo; l'evento ButtonRelease corrisponde all'atto di rilascio del tasto.

Poter distinguere i due tipi di evento è cruciale per consentire comportamenti come il cosiddetto «trascina e lascia cadere» (o «drag and drop»), mediante cui si può effettuare un ButtonPress su di un componente GUI, trascinare il componente altrove e poi «lasciarlo cadere» nella nuova posizione mediante rilascio del tasto del dispositivo di puntamento.

Ma i pulsanti non sono il tipo di componente che è idoneo al «drag and drop». Se un utente pensasse di poterlo fare con un pulsante, allora potrebbe effettuare un ButtonPress sul corrispondente widget, trascinare il puntatore altrove sullo schermo e infine rilasciare il tasto. Questo tipo di attività non corrisponde a ciò che generalmente si intende per una invocazione del widget pulsante. Normalmente, affinché il pulsante possa essere ritenuto premuto, si vuole che l'utente effettui un ButtonPress sul widget e poi - senza spostare il puntatore lontano dal widget - effettui un ButtonRelease. Questo si considera un'invocazione del pulsante.

Si tratta di una nozione più complessa rispetto ai precedenti programmi, in cui si collegava semplicemente un evento <Button-1> al pulsante mediante collegamento di evento.

Per fortuna esiste un altro tipo di collegamento che supporta questa forma di invocazione. È il cosiddetto collegamento di comando, che usa l'opzione command dei widget.


Nel programma seguente, si osservino le linee coi commenti (1) e (2) per capire come si realizza il collegamento di comando. Usando l'opzione command si collega pulsante1 al gestore di eventi self.pulsante1Premuto, e pulsante2 al gestore di eventi self.pulsante2Premuto.

Osservando la definizione dei gestori di eventi (3) (4), si noterà che - a differenza dei gestori dei precedenti programmi - essi non ricevono un oggetto evento come parametro effettivo. Ciò accade perché il collegamento di comando - a differenza del collegamento di evento - non passa automaticamente un oggetto evento come parametro effettivo.

Nota

Il comportamento suddetto è del tutto sensato. In effetti il collegamento di comando non collega un singolo evento a un gestore, bensì una molteplicità di eventi. Nel caso di un pulsante, ad esempio, esso collega una sequenza ButtonPress ButtonRelease al gestore; se dovesse passare un evento al gestore, quale dei due passerebbe: ButtonPress o ButtonRelease? Nessuno dei due sarebbe del tutto esatto, ed ecco il motivo per cui il collegamento di comando non passa al gestore un evento oggetto.

Nei prossimi programmi le questioni suddette verranno approfondite; nel frattempo si provi ad eseguire questo qui.

13.1   Funzionamento del programma

All'esecuzione, i pulsanti appaiono identici ai precedenti programmi... ma il comportamento è diverso.

Si confronti il comportamento relativo a un evento ButtonPress su uno dei pulsanti; ad esempio, si ponga il puntatore sopra il pulsante «Conferma», poi si prema il tasto sinistro del dispositivo di puntamento senza però rilasciarlo.

Nell'esempio precedente il gestore pulsante1Premuto sarebbe stato immediatamente innescato e si sarebbe visto un messaggio in console. Ma nel programma in esame nulla accade... fino al rilascio del tasto. Al rilascio del tasto si osserva un messaggio in console.

Un'ulteriore differenza si rileva confrontando il comportamento in relazione alla pressione di [Spazio] e [Invio]. Per esempio si usi [Tab] per mettere a fuoco il pulsante «Conferma», poi si prema [Spazio] oppure [Invio].

Nel programma precedente (in cui il pulsante «Conferma» era collegato all'evento <Return>) la pressione di [Spazio] non aveva effetto alcuno, mentre la pressione di [Invio] provocava la variazione cromatica del pulsante. Nel programma in esame, invece, il comportamento è opposto: la pressione di [Invio] provoca la variazione, mentre la pressione di [Invio] non ha effetto.

Nel seguito anche queste questioni saranno esaminate più approfonditamente.

13.2   Codice sorgente del programma

from Tkinter import *

class MiaApp:
  def __init__(self, genitore):
    self.MioGenitore = genitore
    self.MioContenitore1 = Frame(genitore)
    self.MioContenitore1.pack()

    self.pulsante1 = Button(self.MioContenitore1,
                            command=self.pulsante1Premuto)  ### (1)
    self.pulsante1.configure(text = "Conferma", background = "green")
    self.pulsante1.pack(side = LEFT)
    self.pulsante1.focus_force()

    self.pulsante2 = Button(self.MioContenitore1,
                            command=self.pulsante2Premuto)  ### (2)
    self.pulsante2.configure(text = "Annulla", background = "red")
    self.pulsante2.pack(side = RIGHT)

  def pulsante1Premuto(self):  ### (3)
    print "Gestore di eventi 'pulsante1Premuto'"
    if self.pulsante1["background"] == "green":
      self.pulsante1["background"] = "yellow"
    else:
      self.pulsante1["background"] = "green"

  def pulsante2Premuto(self):  ### (4)
    print "Gestore di eventi 'pulsante2Premuto'"
    self.MioGenitore.destroy()


radice = Tk()
miaApp = MiaApp(radice)
radice.mainloop()

14   Compresenza di diversi tipi di collegamento

Nel programma precedente è stato introdotto il collegamento di comando e se ne sono esaminate alcune differenze rispetto al collegamento di evento. Nel programma che ora sarà esaminato tali differenze saranno meglio esaminate.

14.1   A quali eventi è collegato command?

Nel programma precedente, se si usa [Tab] per mettere a fuoco il pulsante «Conferma», la pressione di [Spazio] determina una variazione cromatica nel pulsante, mentre [Invio] non ha effetto.

Ciò accade perché l'opzione command per un pulsante fornisce al widget sensibilità sia agli eventi relativi alla tastiera che a quelli relativi al dispositivo di puntamento. In effetti il pulsante rimane in ascolto di pressioni di [Spazio], non di [Invio]. Sicché il collegamento di comando comporta una variazione cromatica al pulsante «Conferma», mentre [Invio] non ha effetto.

Avvertenza

Il comportamento può apparire insolito, specialmente a chi è abituato ai sistemi Microsoft. È bene avere le idee chiare se si intende usare il collegamento di comando; ossia, è il caso di capire esattamente quali eventi - e relativamente a quale dispositivo - comportano l'invocazione di command.

Purtroppo non esistono attualmente fonti certe di informazione al riguardo, tranne il codice sorgente delle librerie Tk. Informazioni più accessibili possono essere alcuni libri su Tk 14 o su Tkinter. La documentazione su Tk è un po' disorganica, ma è disponibile in linea. 13

Si tenga presente, inoltre, che no tutti i widget hanno l'opzione command. I vari tipi di pulsanti (RadioButton, CheckButton, ecc.) ce l'hanno. Altri widget prevedono opzioni simili (ad esempio scrollcommand). In ogni caso è bene approfondire lo studio dei diversi tipi di widget per stabilire il relativo supporto del collegamento di comando. Conoscere a fondo l'opzione command dei widget usati permette di migliorare il funzionamento del GUI (e semplifica la vita del programmatore).

14.2   Usare il collegamento di evento assieme al collegamento di comando

Si è già fatto notare come il collegamento di comando, a differenza del collegamento di evento, non passa automaticamente un oggetto evento come parametro effettivo. Questa situazione rende le cose un po' complicate nel caso si desideri collegare un gestore di eventi a un widget utilizzando entrambi i tipi di collegamento.

Ad esempio, nel programma in esame vorrebbe che i pulsanti reagissero sia a [Invio] che a [Spazio]. Per far ciò è necessario collegare (1) l'evento <Return>, come nel penultimo programma.

Il fatto è che il collegamento di comando non prevede il passaggio di un oggetto evento come parametro effettivo, mentre il collegamento di evento lo prevede. E allora come si può scrivere il gestore di eventi?

Ci sono varie soluzioni possibili a questo problema, ma la più semplice è probabilmente quella di scrivere due gestori di eventi.

Il gestore vero (2) verrà utilizzato per il collegamento di comando, e non si aspetta il passaggio di alcun oggetto evento.

L'altro gestore (3) è solo un involucro (o «wrapper») attorno al gestore vero. Questo involucro si aspetta il passaggio di un oggetto evento come parametro effettivo, ma poi lo ignora, chiamando invece il vero gestore (senza oggetto evento). L'involucro avrà il medesimo nome del vero gestore di eventi, con l'aggiunta di un suffisso _a.

14.3   Funzionamento del programma

All'esecuzione, il comportamento sarà simile a quello del precedente programma, tranne per il fatto che ora i pulsanti reagiscono sia alla pressione di [Invio] che a quella di [Spazio].

14.4   Codice sorgente del programma

from Tkinter import *

class MiaApp:
  def __init__(self, genitore):
    self.mioGenitore = genitore
    self.mioContenitore1 = Frame(genitore)
    self.mioContenitore1.pack()

    self.pulsante1 = Button(self.mioContenitore1,
                            command = self.pulsante1Premuto)
    self.pulsante1.bind("<Return>", self.pulsante1Premuto_a)  ### (1)
    self.pulsante1.configure(text = "Conferma", background = "green")
    self.pulsante1.pack(side = LEFT)
    self.pulsante1.focus_force()

    self.pulsante2 = Button(self.mioContenitore1,
                            command = self.pulsante2Premuto)
    self.pulsante2.bind("<Return>", self.pulsante2Premuto_a)  ### (1)
    self.pulsante2.configure(text = "Annulla", background = "red")
    self.pulsante2.pack(side = RIGHT)

  def pulsante1Premuto(self):  ### (2)
    print "Gestore di eventi 'pulsante1Premuto'"
    if self.pulsante1["background"] == "green":
      self.pulsante1["background"] = "yellow"
    else:
      self.pulsante1["background"] = "green"

  def pulsante2Premuto(self):  ### (2)
    print "Gestore di eventi 'pulsante2Premuto'"
    self.mioGenitore.destroy()

  def pulsante1Premuto_a(self, evento):  ### (3)
    print "Gestore di eventi 'pulsante1Premuto_a' (un involucro)"
    self.pulsante1Premuto()

  def pulsante2Premuto_a(self, evento):  ### (3)
    print "Gestore di eventi 'pulsante2Premuto_a' (un involucro)"
    self.pulsante2Premuto()


radice = Tk()
miaApp = MiaApp(radice)
radice.mainloop()

15   Scambiare informazioni fra gestori di eventi

Negli ultimi programmi sono state esplorate diverse maniere di utilizzo dei gestori di eventi per compiere determinate azioni.

Ora verrà illustrato - sommariamente - come sia possibile la condivisione di informazioni fra i diversi gestori di eventi.


Esistono diverse situazioni in cui è desiderabile che un gestore di eventi esegua qualche compito per poi condividerne i risultati con altri gestori di eventi all'interno del programma.

Uno schema tipico è quello di un'applicazione con due insiemi di widget: un insieme prepara, o sceglie, alcune informazioni, l'altro le usa per qualche scopo.

Ad esempio, si consideri il caso di un widget che consenta all'utente la scelta di un nome di file da una lista, e un altro insieme di widget che gli offra diverse operazioni sul file scelto - apretura, cancellazione, copia, rinomina, e così via.

Oppure, si potrebbe avere un insieme di widget che imposti diverse opzioni di configurazione per l'applicazione, e un altro insieme (pulsanti con l'opzione «Salva» oppure «Annulla», per esempio) che consenta il salvataggio delle impostazioni su disco, oppure l'annullamento delle scelte operate.

Oppure ancora, si potrebbe avere un insieme di widget che imposti i parametri per un programma da eseguire, e un altro widget (probabilmente un pulsante denominato «Esegui», o simili) che avvii il programma con tali parametri impostati.

Oppure infine, potrebbe essere necessario che la funzione che realizza il gestore di eventi trasmetta delle informazioni fra due successive invocazioni di se stessa. Si consideri il caso di un gestore di eventi che semplicemente alterni fra due valori possibili una data variabile; per poterlo fare, il gestore deve sapere il valore che esso aveva assegnato alla variabile alla sua precedente esecuzione.

15.1   Il problema

Il problema che ci si pone è che ogni gestore di eventi è una funzione separata dalle altre. Ciascun gestore di eventi ha variabili locali sue proprie che esso non può condividere con altre funzioni di gestione degli eventi, e neppure con successive invocazioni di se stesso. Quindi ecco la questione: Come può una funzione gestore di eventi condividere dei dati con altri gestori, visto che non può condividere le sue variabili locali con essi?

La soluzione, ovviamente, è che le variabili da condividere non possono essere locali alla funzione di gestione degli eventi; esse devono per forza essere conservate all'esterno della funzione stessa.

15.2   Prima soluzione: variabili globali

Una tecnica per ottenere lo scopo è rendere globali le variabili da condividere. Per esempio, in ogni gestore che necessiti di accedere alle variabili miaVariabile1 e miaVariabile2, si possono inserire le istruzioni seguenti:

global miaVariabile1, miaVariabile2

Tuttavia, l'uso di variabili globali è potenzialmente pericoloso, nonché considerato in genere una tecnica di programmazione sciatta.

15.3   Seconda soluzione: variabili di istanza

Una tecnica più elegante prevede l'uso delle variabili di istanza (ossia, variabili precedute dal prefisso self.) al fine di conservare informazioni da condividere fra gestori di eventi. A tale scopo, ovviamente, l'applicazione dev'essere stata realizzata come una classe, e non come semplice codice in linea.

Nota

Questa era una delle ragioni per cui (già dai primi esempi) le applicazioni illustrate sono state racchiuse in una classe. Avendo fatto ciò precocemente, l'applicazione con cui adesso si ha a che fare possiede di già tutta l'infrastruttura che consente l'uso delle variabili di istanza.

Nel programma in esame, l'informazione da ricordare e condividere è molto semplice: il nome dell'ultimo pulsante invocato. L'informazione verrà conservata in una variabile di istanza denominata self.mioUltimoPulsanteInvocato (si vedano i commenti ### 1 nel sorgente).

Al fine di dimostrare che l'informazione viene realmente conservata, ogni volta che un gestore viene invocato l'informazione viene scritta in console (si vedano i commenti ### 2 nel sorgente).

15.4   Funzionamento del programma

Il programma mostra tre pulsanti:

[txs_01_070.png]

All'esecuzione, se se ne preme uno, verrà scritto il suo nome in console, oltre al nome di quello precedentemente premuto.

Si noti che nessuno dei pulsante termina l'applicazione, pertanto, se si desidera interromperla, si deve operare sul widget di chiusura (l'icona con la «X» in un quadratino, sul lato destro della barra del titolo).

15.5   Codice sorgente del programma

from Tkinter import *

class MiaApp:
  def __init__(self, genitore):

    ### 1 -- All'inizio, nessun gestore e` stato ancora invocato.
    self.mioUltimoPulsanteInvocato = None

    self.mioContenitore1 = Frame(genitore)
    self.mioContenitore1.pack()

    self.pulsanteGiallo = Button(self.mioContenitore1,
                                 command = self.pulsanteGialloPremuto)
    self.pulsanteGiallo.configure(text = "GIALLO",
                                  background = "yellow")
    self.pulsanteGiallo.pack(side = LEFT)

    self.pulsanteRosso = Button(self.mioContenitore1,
                                command = self.pulsanteRossoPremuto)
    self.pulsanteRosso.configure(text = "ROSSO", background = "red")
    self.pulsanteRosso.pack(side = LEFT)

    self.pulsanteBianco = Button(self.mioContenitore1,
                                 command = self.pulsanteBiancoPremuto)
    self.pulsanteBianco.configure(text = "BIANCO", background = "white")
    self.pulsanteBianco.pack(side = LEFT)

  def pulsanteRossoPremuto(self):
    print "Premuto pulsante  ROSSO. Il pulsante precedentemente \
invocato era", self.mioUltimoPulsanteInvocato  ### 2
    self.mioUltimoPulsanteInvocato = "ROSSO"   ### 1

  def pulsanteGialloPremuto(self):
    print "Premuto pulsante GIALLO. Il pulsante precedentemente \
invocato era", self.mioUltimoPulsanteInvocato  ### 2
    self.mioUltimoPulsanteInvocato = "GIALLO"  ### 1

  def pulsanteBiancoPremuto(self):
    print "Premuto pulsante BIANCO. Il pulsante precedentemente \
invocato era", self.mioUltimoPulsanteInvocato  ### 2
    self.mioUltimoPulsanteInvocato = "BIANCO"  ### 1


print "\n"*100  # un metodo rozzo ma efficace per pulire lo schermo ;-)
print "Programma avviato..."
radice = Tk()
miaApp = MiaApp(radice)
radice.mainloop()
print "... Fine!"

16   Controllo della disposizione

Nei vari programmi che precedono ci si è dedicati principalmente alle tecniche di collegamento fra gestori di eventi e widget.

Col programma ora in esame si ritorna all'argomento della creazione del GUI - l'impostazione dei widget e il controllo dell'aspetto e della posizione degli stessi.

16.1   Tre tecniche di controllo della disposizione di un GUI

Esistono tre tecniche per controllare la disposizione generale di un GUI:

  • attributi dei widget
  • opzioni di pack()
  • annidamento dei contenitori (quadri)

Nel programma in esame si considereranno le prime due.

Si lavorerà molto coi pulsanti e il quadro che li contiene. Nelle versioni precedenti del programma, il quadro si chiamava mioContenitore1. In questo caso, invece, si utilizzerà un nome un po' più descrittivo: quadro_pulsanti.

Nota

I numeri del seguente elenco si riferiscono ai corrispondenti commenti numerati nel codice sorgente.
  1. Per prima cosa, al fine di assicurarsi che i pulsanti abbiano tutti la stessa larghezza, si specifica un attributo width uguale per tutti, mediante la costante larghezza_pulsante. Si noti che l'attributo width è specifico per i widget di tipo Button - non tutti i widget hanno questo attributo. Si noti altresì che l'attributo width viene specificato in unità carattere (ossia, non in pixel, pollici o millimetri). Poiché il testo più lungo («Conferma») contiene 8 caratteri, la larghezza per i pulsanti viene così fissata.

  2. Poi si aggiunge dell'imbottitura ai pulsanti. L'imbottitura è spazio aggiuntivo attorno al testo, tra il testo e il bordo del pulsante. Si agisce tramite gli attributi padx e pady del pulsante (attraverso le costanti imb_pulsantex e imb_pulsantey). padx imbottisce lungo l'asse orizzontale, da sinistra a destra. padx imbottisce lungo l'asse verticale, dall'alto al basso.

    Si specifica l'imbottitura orizzontale a 2 millimetri (imb_pulsantex = "2m") e quella verticale a 1 millimetro (imb_pulsantey = "1m"). Si osservi che, a differenza dell'attributo width (che è numerico), i valori di questi attributi vengono racchiusi fra virgolette. Ciò accade in quanto le unità di imbottitura sono specificate usando il suffisso m, sicché è necessario specificare l'estensione delle imbottiture come stringhe e non come numeri.

  3. Infine, si aggiunge un po' di imbottitura al contenitore (quadro_pulsanti) che contiene i pulsanti. Per il contenitore è possibile specificare quattro attributi di imbottitura: padx e pady (attraverso le costanti imb_quadro_pulsantix e imb_quadro_pulsantix) specificano l'imbottitura attorno (all'esterno) al quadro. ipadx e ipady specificano l'imbottitura interna, ossia imbottitura che avvolge ciascun widget interno al contenitore.

    Nota

    Si noti che l'imbottitura non viene specificata, per il quadro, come un attributo, ma come opzioni passate al metodo impacchettatore (4). Si capisce che l'imbottitura è un concetto un po' confuso: i quadri hanno imbottitura interna, ma pulsanti e altri widget non ce l'hanno; in certi casi l'imbottitura è un attributo del widget, mentre in altri casi si specifica come opzione del metodo pack().

16.2   Funzionamento del programma

All'esecuzione si osservano due pulsanti:

[txs_01_080.png]

Però ora dovrebbero avere le stesse dimensioni. I bordi dei pulsanti non stringono più angustamente il testo contenuto, e i pulsanti sono circondati da una certa quantità di spazio libero attorno ad essi.

16.3   Codice sorgente del programma

from Tkinter import *

class MiaApp:
  def __init__(self, genitore):

    #------ costanti per il controllo della disposizione
    larghezza_pulsanti = 8  ### (1)

    imb_pulsantex = "2m"  ### (2)
    imb_pulsantey = "1m"  ### (2)

    imb_quadro_pulsantix = "3m"       ### (3)
    imb_quadro_pulsantiy = "2m"       ### (3)
    imb_int_quadro_pulsantix = "3m"   ### (3)
    imb_int_quadro_pulsantiy = "1m"   ### (3)
    #------------------ fine costanti -------------------

    self.mioGenitore = genitore
    self.quadro_pulsanti = Frame(genitore)

    self.quadro_pulsanti.pack(  ### (4)
      ipadx = imb_int_quadro_pulsantix,  ### (3)
      ipady = imb_int_quadro_pulsantiy,  ### (3)
      padx = imb_quadro_pulsantix,       ### (3)
      pady = imb_quadro_pulsantiy,       ### (3)
      )

    self.pulsante1 = Button(self.quadro_pulsanti,
                            command = self.pulsante1Premuto)
    self.pulsante1.configure(text = "Conferma", background = "green")
    self.pulsante1.focus_force()
    self.pulsante1.configure(
      width = larghezza_pulsanti,  ### (1)
      padx = imb_pulsantex,  ### (2)
      pady = imb_pulsantey   ### (2)
      )

    self.pulsante1.pack(side = LEFT)
    self.pulsante1.bind("<Return>", self.pulsante1Premuto_a)

    self.pulsante2 = Button(self.quadro_pulsanti,
                            command = self.pulsante2Premuto)
    self.pulsante2.configure(text = "Annulla", background = "red")
    self.pulsante2.configure(
      width = larghezza_pulsanti,  ### (1)
      padx = imb_pulsantex,  ### (2)
      pady = imb_pulsantey   ### (2)
      )

    self.pulsante2.pack(side = RIGHT)
    self.pulsante2.bind("<Return>", self.pulsante2Premuto_a)

  def pulsante1Premuto(self):
    if self.pulsante1["background"] == "green":
      self.pulsante1["background"] = "yellow"
    else:
      self.pulsante1["background"] = "green"

  def pulsante2Premuto(self):
    self.mioGenitore.destroy()

  def pulsante1Premuto_a(self, event):
    self.pulsante1Premuto()

  def pulsante2Premuto_a(self, event):
    self.pulsante2Premuto()


radice = Tk()
miaApp = MiaApp(radice)
radice.mainloop()

17   Quadri annidati

Nel programma in esame si utilizzerà il cosiddetto annidamento dei quadri («frame nesting»). Verranno creati una serie di quadri, l'uno incluso nell'altro: quadro_alto, quadro_basso, quadro_sinistra, quadro_destra.

Tali quadri saranno vuoti - ossia privi di widget. A causa dell'elasticità dei quadri, normalmente essi dovrebbero restringersi alle dimensioni minime; ma specificando gli attributi height e width si forniscono loro delle dimensioni iniziali.

Nota

Non per tutti i quadri si specificano tali attributi. Ad esempio mioContenitore1 non li prevede, mentre sono previsto per i suoi discendenti, quindi mioContenitore1 si adatterà automaticamente alle dimensioni complessive dei suoi discendenti.

Nei programmi successivi si esaminerà la procedura di inserimento di widget in questi quadri; per il momenti ci si limita a crearli, stabilirne le dimensioni, le posizioni e i colori di sfondo.

Inoltre, si pone un bordo in rilievo attorno ai tre quadri che saranno di particolare interesse per il seguito del presente articolo: quadro_basso, quadro_sinistra e quadro_destra. Gli altri quadri (ad esempio quadro_alto e quadro_pulsanti) non hanno bordo.

17.1   Funzionamento del programma

All'esecuzione del programma si osserveranno diversi quadri con differenti colori di sfondo:

[txs_01_090.png]

17.2   Codice sorgente del programma

from Tkinter import *

class MiaApp:
  def __init__(self, genitore):

    #--- costanti per il controllo della disposizione dei
    #--- pulsanti
    larghezza_pulsanti = 8
    imb_pulsantex = "2m"
    imb_pulsantey = "1m"
    imb_quadro_pulsantix = "3m"
    imb_quadro_pulsantiy = "2m"
    imb_int_quadro_pulsantix = "3m"
    imb_int_quadro_pulsantiy = "1m"
    #--------------------- fine costanti -----------------------

    self.mioGenitore = genitore

    ### Il quadro principale si chiama 'mioContenitore1'
    self.mioContenitore1 = Frame(genitore)
    self.mioContenitore1.pack()

    ### All'interno di 'mioContenitore1' viene utilizzata
    ### l'orientazione VERTICALE (dall'alto al basso).
    ### All'interno di 'mioContenitore1', per prima cosa
    ### si crea 'quadro_pulsanti'; successivamente si creano
    ### 'quadro_alto' e 'quadro_basso': essi sono i quadri
    ### dimostrativi.

    # Il quadro dei pulsanti
    self.quadro_pulsanti = Frame(self.mioContenitore1)
    self.quadro_pulsanti.pack(
      side = TOP,
      ipadx = imb_int_quadro_pulsantix,
      ipady = imb_int_quadro_pulsantiy,
      padx = imb_quadro_pulsantix,
      pady = imb_quadro_pulsantiy,
      )

    # quadro alto
    self.quadro_alto = Frame(self.mioContenitore1)
    self.quadro_alto.pack(side = TOP,
      fill = BOTH,
      expand = YES,
      )

    # quadro basso
    self.quadro_basso = Frame(self.mioContenitore1,
      borderwidth = 5,
      relief = RIDGE,
      height = 50,
      background = "white",
      )
    self.quadro_basso.pack(side = TOP,
      fill = BOTH,
      expand = YES,
      )

    ### Adesso vengono creati altri due quadri,
    ### 'quadro_sinistra' e 'quadro_destra', all'interno di
    ### 'quadro_alto'. Dentro 'quadro_alto' si utilizza
    ### l'orientazione ORIZZONTALE (da sinitra a destra).

    # quadro sinistra
    self.quadro_sinistra = Frame(self.quadro_alto,
      background = "red",
      borderwidth = 5,
      relief = RIDGE,
      height = 250,
      width = 50,
      )
    self.quadro_sinistra.pack(side = LEFT,
      fill = BOTH,
      expand = YES,
      )

    # quadro destra
    self.quadro_destra = Frame(self.quadro_alto,
      background = "tan",
      borderwidth = 5,
      relief = RIDGE,
      width = 250,
      )
    self.quadro_destra.pack(side = RIGHT,
      fill = BOTH,
      expand = YES,
      )

    # Vengono ora aggiunti i pulsanti a 'quadro_pulsanti'

    self.pulsante1 = Button(self.quadro_pulsanti,
                            command = self.pulsante1Premuto)
    self.pulsante1.configure(text = "Conferma", background = "green")
    self.pulsante1.focus_force()
    self.pulsante1.configure(
      width = larghezza_pulsanti,
      padx = imb_pulsantex,
      pady = imb_pulsantey
      )

    self.pulsante1.pack(side = LEFT)
    self.pulsante1.bind("<Return>", self.pulsante1Premuto_a)

    self.pulsante2 = Button(self.quadro_pulsanti,
                            command = self.pulsante2Premuto)
    self.pulsante2.configure(text = "Annulla", background = "red")
    self.pulsante2.configure(
      width = larghezza_pulsanti,
      padx = imb_pulsantex,
      pady = imb_pulsantey
      )

    self.pulsante2.pack(side = RIGHT)
    self.pulsante2.bind("<Return>", self.pulsante2Premuto_a)

  def pulsante1Premuto(self):
    if self.pulsante1["background"] == "green":
      self.pulsante1["background"] = "yellow"
    else:
      self.pulsante1["background"] = "green"

  def pulsante2Premuto(self):
    self.mioGenitore.destroy()

  def pulsante1Premuto_a(self, event):
    self.pulsante1Premuto()

  def pulsante2Premuto_a(self, event):
    self.pulsante2Premuto()


radice = Tk()
miaApp = MiaApp(radice)
radice.mainloop()

18   Controllo della geometria

La gestione delle dimensioni delle finestre può rivelarsi un'esperienza frustrante programmando con Tkinter.

Si consideri la seguente situazione.

Aderendo alla dottrina della programmazione per raffinamenti successivi, per prima cosa si stabiliscono le dimensioni del quadro da utilizzare. Si effettua una prova, si osserva che le cose funzionano e si procede ad aggiungere dei pulsanti al quadro. Alla prova successiva, sorprendentemente, Tkinter si comporta come se le specifiche dimensionali del quadro fossero assenti, e il quadro si adatta ai pulsanti inclusi.

Che accade?

Il fatto è che il comportamento del metodo impacchettatore è incosistente; o, meglio, il comportamento deipende da una serie di fattori contingenti. In sostanza l'impacchettatore rispetta le specifiche dimensionali per un contenitore se il contenitore è vuoto; altrimenti la sua natura elastica prende il sopravvento, le specifiche dimensionali vengono ignorate e il contenitore si adatta al massimo ai widget contenuti.

In definitiva è impossibile controllare le dimensioni di un contenitore non vuoto.

Ciò che si può fare è controllare le dimensioni iniziali dell'intera finestra principale (radice), mediante le opzioni «geometriche» del gestore di finestre («Window Manager»).

Nel programma in esame, si usa il metodo geometry (1) per costruire una finestra bella ampia attorno a un quadro più piccolo.

Si noti (2) che l'opzione title, usata anch'essa in questo programma, è un altro metodo del gestore di finestre, il quale controlla il testo della barra del titolo della finestra.

Si noti altresì che le opzioni del gestore di finestre possono opzionalmente essere fatte precedere dal prefisso wm_, ad esempio wm_geometry oppure wm_title. Nel programma, a titolo dimostrativo, si utilizzano geometry``e ``wm_title.

18.1   Funzionamento del programma

Il programma imposta quattro finestre in successione.

Si noti che ciascuna finestra va chiusa utilizzando il widget di chiusura - la «X» nel rettangolino a destra della barra del titolo.

Nel caso 1, si osserva l'aspetto del quadro se le dimensioni vengono specificate e se non contiene widget:

[txs_01_100.png]

Nel caso 2, si osserva il medesimo quadro con alcuni widget (in particolare, tre pulsanti) aggiunti. Si osservi che il quadro si è adattato ai tre pulsanti:

[txs_01_110.png]

Nel caso 3, si osserva nuovamente un quadro vuoto, ma con l'utilizzo delle opzioni di geometria che controllano le dimensioni complessive della finestra. È visibile lo sfondo azzurro del quadro all'interno dell'area grigia più ampia della finestra:

[txs_01_120.png]

Nel caso 4, si osserva nuovamente il quadro con tre pulsanti, ma questa volta con l'utilizzo delle opzioni di geometria che controllano le dimensioni complessive della finestra. Si osservi che le dimensioni della finestra sono identiche al caso 3, ma (analogamente al caso 2) il quadro si è adattato ai tre pulsanti, impedendo così la visione dello sfondo azzurro.

[txs_01_130.png]

18.2   Codice sorgente del programma

from Tkinter import *

class App:
  def __init__(self, radice, usa_geometria, mostra_pulsanti):
    qdr = Frame(radice, width = 300, height = 200, bg = "blue")
    qdr.pack(side = TOP, expand = NO, fill = NONE)

    if usa_geometria:
      radice.geometry("600x400")  ### (1) Si noti l'uso di
                                  ### un metodo del gestore
                                  ### di finestre

    if mostra_pulsanti:
      Button(qdr, text = "Button 1", width = 10).pack(side = LEFT)
      Button(qdr, text = "Button 2", width = 10).pack(side = LEFT)
      Button(qdr, text = "Button 3", width = 10).pack(side = LEFT)


caso = 0
for usa_geometria in (0, 1):
    for mostra_pulsanti in (0,1):
        caso = caso + 1
        radice = Tk()
        radice.wm_title("Caso " + str(caso))  ### (2) Si noti
                                              ### l'uso del metodo
                                              ### 'wm_title' del
                                              ### gestore di finestre
        app = App(radice, usa_geometria, mostra_pulsanti)
        
        radice.mainloop()

19   Approfondimento sull'impacchettatore

Nel programma che viene ora esaminato, si dimostreranno alcune opzioni del metodo pack() utili al controllo della disposizione in un quadro:

A differenza dei programmi precedenti, per capirne il funzionamento è necessario eseguirlo, più che leggerne il codice sorgente.

Lo scopo del programma è dimostrare l'effetto delle varie opzioni di impacchettamento. Eseguendo il programma è possibile impostare diverse opzioni per osservarne l'effetto combinato.

19.1   I concetti che stanno alla base delle opzioni di impacchettamento

Allo scopo di comprendere come sia possibile avere il controllo dell'aspetto dei widget all'interno di un contenitore (ossia, un quadro), è necessario rammentare che il gestore di geometria impacchettatore utilizza un modello di distribuzione a cavità. Ossia, ogni contenitore contiene una cavità, e i figli vengono impacchettati in essa.

Trattando del posizionamento e della presentazione delle componenti in un contenitore, sono utili i seguenti tre concetti:

  • spazio non richiesto (ossia, la cavità)
  • spazio richiesto ma libero
  • spazio richiesto e occupato

Quando si impacchetta un widget, ad esempio un pulsante, esso viene impacchettato sempre lungo uno dei quattro lati della cavità. L'opzione di impacchettamento side specifica quale lato usare. Per esempio, specificando side = LEFT, il widget verrà impacchettato (ossia posizionato) lungo il lato sinistro della cavità.

Quando un widget è impacchettato lungo un lato, tale widget richiede l'intero lato, anche se non necessariamente lo occupa interamente. Si supponga di impacchettare un piccolo pulsante «X» lungo il lato sinistro di una grande cavità, come nel seguente diagramma:

              -------------------
richiesto ma  |   |             |
libero -----> |   |   cavità    |
              |   | (spazio non |
              |   |  richiesto) |
richiesto e   |---|             |
occupato ---> | X |             |
              |---|             |
              |   |             |
richiesto ma  |   |             |
libero -----> |   |             |
              |   |             |
              -------------------

La cavità (lo spazio non richiesto) si trova ora alla destra del widget. Il widget «X» ha richiesto l'intero lato sinistro, in una striscia larga abbastanza da contenerlo. Però, poiché il widget «X» è piccolo, esso occupa effettivamente solo una parte dello spazio che ha richiesto, ossia solo la parte necessaria alla sua presentazione.

È evidente che il widget «X» ha richiesto solo lo spazio necessario alla sua presentazione. Se si specificasse l'opzione expand = YES, allora il widget richiederebbe tutto lo spazio disponibile. Non rimarrebbe nella cavità dello spazio non richiesto.

Nota

Ciò non significa che il widget «X» userebbe tutto lo spazio disponibile: esso userebbe ancora solo la piccola parte che gli serve effettivamente.

In tali situazioni, il widget «X» ha a sua disposizione un ampio spazio inutilizzato in cui vagare. Dove andrà a finire, all'interno di questo ampio spazio? Dipende dall'opzione di ancoraggio. Quando un widget richiede più spazio di quanto ne utilizzi, allora l'opzione anchor determina la posizione finale del widget all'interno dello spazio richiesto. I valori dell'opzione anchor corrispondono ai punti cardinali. N significa nord (ossia al centro della parte alta dello spazio richiesto); NE significa nordest (ossia nell'angolo in alto a destra dello spazio richiesto); e così via.

Se un widget richiede più spazio di quanto ne utilizzi, il widget può vagare all'interno dello spazio non utilizzato (a seconda dell'opzione anchor); oppure può «crescere» fino a riempire lo spazio non utilizzato, a seconda dell'opzione fill, la quale specifica se un widget possa o non possa crescere per utilizzare («riempire») lo spazio non utilizzato, e in quale direzione:

  • fill=NONE significa che non può crescere;
  • fill=X significa che può crescere lungo l'asse X (ossia, orizzontalmente);
  • fill=Y significa che può crescere lungo l'asse Y (ossia, verticalmente);
  • fill=BOTH significa che può crescere sia orizzontalmente che verticalmente;

19.2   Esecuzione del programma

SI provi dunque a eseguire il programma. Non è necessario leggere il codice sorgente, basta eseguire il programma e sperimentare con le varie opzioni di impacchettamento relative ai tre pulsanti dimostrativi:

[txs_01_140.png]

Il quadro del pulsante «A» gli fornisce una cavità orizzontale in cui spostarsi - il quadro non è più alto del pulsante.

Il quadro del pulsante «B» gli fornisce una cavità verticale in cui spostarsi - il quadro non è più largo del pulsante.

Infine, il quadro del pulsante «C» gli fornisce una cavità abbastanza ampia - molto più larga e più alta del pulsante stesso - in cui vagare.

E per concludere...

19.3   Un utile trucco per la «disinfestazione»

Si sarà notato che l'impacchettamento è una procedura complicata, a causa del fatto che il posizionamento di un widget rispatto agli altri precedentemente impacchettati dipende in parte da come questi erano stati a loro volta impacchettati. Concretamente, se gli altri widget erano stati impacchettati a sinistra, allora la cavità all'interno della quale il successivo widget potrà essere impacchettato si troverà alla loro sinistra; ma se erano stati impacchettati in alto nella cavità, allora la cavità all'interno della quale il successivo widget potrà essere si troverà al di sotto di quelli. Complessivamente, le cose si confondono con estrema facilità.

Ecco allora un utile trucco per la «disinfestazione». Se nello sviluppo della disposizione ci si imbatte in qualche problema - se le cose non avvengono nel modo previsto - allora può essere utile conferire a ciascun contenitore (quadro) un diverso colore di sfondo, per esempio:

bg = "red"

oppure:

bg = "cyan"

oppure:

bg = "tan"

... oppure yellow, o blue, o red, e così via.

Così facendo è possibile osservare come effettivamente i quadri si dispongono nella finestra; questo, spesso, fornisce indicazioni utili per capire il problema.

19.4   Codice sorgente del programma

from Tkinter import *

class MiaApp:
  def __init__(self, genitore):

    #--- costanti per il controllo della disposizione
    #--- dei pulsanti
    larghezza_pulsanti = 8
    imb_pulsantex = "2m"
    imb_pulsantey = "1m"
    imb_quadro_pulsantix = "3m"
    imb_quadro_pulsantiy = "2m"
    imb_int_quadro_pulsantix = "3m"
    imb_int_quadro_pulsantiy = "1m"
    #--------------------- fine costanti -----------------------

    # impostazione delle variabili di controllo Tkinter,
    # controllate dai pulsanti radio
    self.nome_pulsante = StringVar()
    self.nome_pulsante.set("C")

    self.opzione_lato = StringVar()
    self.opzione_lato.set(LEFT)

    self.opzione_riempimento = StringVar()
    self.opzione_riempimento.set(NONE)

    self.opzione_espansione = StringVar()
    self.opzione_espansione.set(YES)

    self.opzione_ancoraggio = StringVar()
    self.opzione_ancoraggio.set(CENTER)

    #---------------

    self.mioGenitore = genitore
    self.mioGenitore.geometry("640x400")

    ### Il quadro principale si chiama 'quadro_grande'
    self.quadro_grande = Frame(genitore) ###
    self.quadro_grande.pack(expand = YES, fill = BOTH)

    ### Viene usata l'orientazione ORIZZONTALE (da sinistra a
    ### destra) all'interno di 'quadro_grande'.
    ### Dentro 'quadro_grande' si creano 'quadro_controllo' e
    ### 'quadro_dimostrativo'.

    # 'quadro_controllo' - praticamente tutto tranne la
    # dimostrazione
    self.quadro_controllo = Frame(self.quadro_grande) ###
    self.quadro_controllo.pack(side = LEFT, expand = NO, padx = 10,
                               pady = 5, ipadx = 5, ipady = 5)

    # All'interno di 'quadro_controllo' si creano un'etichetta
    # per il titolo e un 'quadro_pulsanti'

    mioMessaggio = "Questa finestra illustra l'effetto \ndelle \
opzioni di impacchettamento \n 'expand', 'fill' e 'anchor'."
    Label(self.quadro_controllo,
      text = mioMessaggio,
      justify = LEFT).pack(side = TOP, anchor = W)

    # 'quadro_pulsanti'
    self.quadro_pulsanti = Frame(self.quadro_controllo)
    self.quadro_pulsanti.pack(side = TOP, expand = NO, fill = Y,
                              ipadx = 5, ipady = 5)

    # 'quadro_dimostrativo'
    self.quadro_dimostrativo = Frame(self.quadro_grande)
    self.quadro_dimostrativo.pack(side = RIGHT, expand = YES,
                                  fill = BOTH)

    ### Dentro 'quadro_dimostrativo' vengono creati
    ### 'quadro_alto' e 'quadro_basso'.
    ### Essi saranno i quadri della dimostrazione.
    # 'quadro_alto'
    self.quadro_alto = Frame(self.quadro_dimostrativo)
    self.quadro_alto.pack(side = TOP, expand = YES, fill = BOTH)

    # 'quadro_basso'
    self.quadro_basso = Frame(self.quadro_dimostrativo,
      borderwidth = 5,
      relief = RIDGE,
      height = 50,
      bg = "cyan",
      )
    self.quadro_basso.pack(side = TOP, fill = X)

    ### Vengono aggiunti altri due quadri, 'quadro_sx' e
    ### 'quadro_dx' all'interno di 'quadro_alto'. Si utilizza
    ### l'orientazione ORIZZONTALE (da sinistra a destra)
    ### dentro 'quadro_alto'.

    # 'quadro_sx'
    self.quadro_sx = Frame(self.quadro_alto,
      background = "red",
      borderwidth = 5,
      relief = RIDGE,
      width = 50
      ) 
    self.quadro_sx.pack(side = LEFT, expand = NO, fill = Y)

    # 'quadro_dx'
    self.quadro_dx = Frame(self.quadro_alto,
      background = "tan",
      borderwidth = 5,
      relief = RIDGE,
      width = 250
      )
    self.quadro_dx.pack(side = RIGHT, expand = YES, fill = BOTH)

    # Si pone un pulsante in ciascun quadro significativo
    nomi_pulsanti = ["A", "B", "C"]
    opzioni_lato = [LEFT, TOP, RIGHT, BOTTOM]
    opzioni_riempimento = [X, Y, BOTH, NONE]
    opzioni_espansione = [YES, NO]
    opzioni_ancoraggio = [NW, N, NE, E, SE, S, SW, W, CENTER]

    self.pulsanteA = Button(self.quadro_basso, text = "A")
    self.pulsanteA.pack()
    self.pulsanteB = Button(self.quadro_sx, text = "B")
    self.pulsanteB.pack()
    self.pulsanteC = Button(self.quadro_dx, text = "C")
    self.pulsanteC.pack()
    self.pulsante_con_nome = {"A": self.pulsanteA,
      "B": self.pulsanteB,
      "C": self.pulsanteC
      }

    # Si aggiungono alcuni sottoquadri a 'quadro_pulsanti'
    self.quadro_nomi_pulsanti = Frame(self.quadro_pulsanti,
                                      borderwidth = 5)
    self.quadro_opzioni_lato = Frame(self.quadro_pulsanti,
                                     borderwidth = 5)
    self.quadro_opzioni_riempimento = Frame(self.quadro_pulsanti,
                                            borderwidth = 5)
    self.quadro_opzioni_espansione = Frame(self.quadro_pulsanti,
                                           borderwidth = 5)
    self.quadro_opzioni_ancoraggio = Frame(self.quadro_pulsanti,
                                           borderwidth = 5)

    self.quadro_nomi_pulsanti.pack(side = LEFT, expand = YES,
                                   fill = Y, anchor = N)
    self.quadro_opzioni_lato.pack(side = LEFT, expand = YES,
                                  anchor = N)
    self.quadro_opzioni_riempimento.pack(side = LEFT, expand = YES,
                                         anchor = N)
    self.quadro_opzioni_espansione.pack(side = LEFT, expand = YES,
                                        anchor = N)
    self.quadro_opzioni_ancoraggio.pack(side = LEFT, expand = YES,
                                        anchor = N)

    Label(self.quadro_nomi_pulsanti,
      text = "\nPulsante").pack()
    Label(self.quadro_opzioni_lato,
      text = "Opzione\n'side'").pack()
    Label(self.quadro_opzioni_riempimento,
      text = "Opzione\n'fill'").pack()
    Label(self.quadro_opzioni_espansione,
      text = "Opzione\n'expand'").pack()
    Label(self.quadro_opzioni_ancoraggio,
      text = "Opzione\n'anchor'").pack()

    for opzione in nomi_pulsanti:
      pulsante = Radiobutton(self.quadro_nomi_pulsanti,
                             text = str(opzione),
                             indicatoron = 1,
                             value = opzione,
                             command = self.aggiorna_pulsante,
                             variable = self.nome_pulsante)
      pulsante["width"] = larghezza_pulsanti
      pulsante.pack(side = TOP)

    for opzione in opzioni_lato:
      pulsante = Radiobutton(self.quadro_opzioni_lato,
                             text = str(opzione),
                             indicatoron = 0, 
                             value = opzione, 
                             command = self.aggiorna_dimostrazione, 
                             variable = self.opzione_lato)
      pulsante["width"] = larghezza_pulsanti
      pulsante.pack(side = TOP)

    for opzione in opzioni_riempimento:
      pulsante = Radiobutton(self.quadro_opzioni_riempimento, 
                             text = str(opzione), 
                             indicatoron = 0, 
                             value = opzione, 
                             command = self.aggiorna_dimostrazione, 
                             variable = self.opzione_riempimento)
      pulsante["width"] = larghezza_pulsanti
      pulsante.pack(side = TOP)

    for opzione in opzioni_espansione:
      pulsante = Radiobutton(self.quadro_opzioni_espansione, 
                             text = str(opzione), 
                             indicatoron = 0, 
                             value = opzione, 
                             command = self.aggiorna_dimostrazione, 
                             variable = self.opzione_espansione)
      pulsante["width"] = larghezza_pulsanti
      pulsante.pack(side = TOP)

    for opzione in opzioni_ancoraggio:
      pulsante = Radiobutton(self.quadro_opzioni_ancoraggio, 
                             text = str(opzione), 
                             indicatoron = 0, 
                             value = opzione, 
                             command = self.aggiorna_dimostrazione, 
                             variable = self.opzione_ancoraggio)
      pulsante["width"] = larghezza_pulsanti
      pulsante.pack(side = TOP)

    self.quadroPulsanteAnnulla = Frame(self.quadro_nomi_pulsanti)
    self.quadroPulsanteAnnulla.pack(side = BOTTOM, 
                                    expand = YES, 
                                    anchor = SW)

    self.pulsanteAnnulla = Button(self.quadroPulsanteAnnulla,
                                  text = "Annulla", 
                                  background = "red", 
                                  width = larghezza_pulsanti, 
                                  padx = imb_pulsantex, 
                                  pady = imb_pulsantey)
    self.pulsanteAnnulla.pack(side = BOTTOM, anchor = S)

    self.pulsanteAnnulla.bind("<Button-1>", 
      self.pulsanteAnnullaPremuto)
    self.pulsanteAnnulla.bind("<Return>", 
      self.pulsanteAnnullaPremuto)

    # Si impostano i pulsanti nella posizione iniziale
    self.aggiorna_dimostrazione()

  def aggiorna_pulsante(self):
    pulsante = self.pulsante_con_nome[self.nome_pulsante.get()]
    proprieta = pulsante.pack_info()
    self.opzione_riempimento.set(proprieta["fill"])
    self.opzione_lato.set(proprieta["side"])
    self.opzione_espansione.set(proprieta["expand"])
    self.opzione_ancoraggio.set(proprieta["anchor"])

  def aggiorna_dimostrazione(self):
    pulsante = self.pulsante_con_nome[self.nome_pulsante.get()]
    pulsante.pack(fill = self.opzione_riempimento.get(), 
      side = self.opzione_lato.get(), 
      expand = self.opzione_espansione.get(), 
      anchor = self.opzione_ancoraggio.get()
      )

  def pulsanteAnnullaPremuto(self, evento):
      self.mioGenitore.destroy()


radice = Tk()
miaApp = MiaApp(radice)
radice.mainloop()

20   Appendice: GUI multipiattaforma

I programmi esemplificati nel presente aricolo sono stati verificati in un ambiente di programmazione GNU/Linux, ma è possibile installare l'interprete Python e la libreria Tk anche in altri ambienti 15.

L'astrazione fornita dalle Tk tramite il modulo Python Tkinter permette quindi di realizzare applicazioni GUI multipiattaforma, ossia eseguibili senza alcuna modifica in sistemi operativi diversi.

Ad esempio, ecco un immagine catturata durante l'esecuzione di uno dei programmi in ambiente GNU/Linux + Fvwm2:

[txs_01_150.jpg]

ed ecco lo stesso programma in esecuzione sotto Microsoft Windows 2000:

[txs_01_160.jpg]