Creazione di un widget personalizzato e convalida dell'input
Introduzione
In questa lezione vedremo come preparare un proprio widget personalizzato mettendo insieme alcuni widget base. Approfitteremo dell'esempio preparato anche per vedere come effettuare una convalida dei dati.
Immaginiamo di voler creare un banalissimo programma per la semplificazione di frazioni, in cui però decidiamo di voler preparare un widget personalizzato per l'input del numeratore e del denominatore della frazione.
Il risultato che vogliamo ottenere è il seguente:
Programma principale
Vogliamo fare in modo di poter aggiungere la frazione nella finestra principale come se fosse un widget normale, scrivendo qualcosa tipo:
self.parent = parent
self.Fraction=FractionWidget(self.parent, background='red')
self.Fraction['borderwidth']=10
self.Fraction['relief']='ridge'
self.Fraction['numerator']=12
self.Fraction['denominator']=24
self.Fraction.pack({'side':'top', 'padx':10, 'pady':10})
Come si vede, il nostro widget, chiamato FractionWidget
, si comporta a tutti gli effetti come un widget di base (il costruttore riceve dei parametri che vengono impostati, le proprietà possono essere modificate con il metodo dell'accesso tramite parentesi quadre. ecc.). Inoltre, è possibile impostare i valori per il numeratore e il denominatore.
La classe FractionWidget
Per il nostro widget personalizzato definiamo una classe, in cui il costruttore prepara un frame in cui sono presenti due widget Entry separati da un widget Label con i trattini (si può fare di meglio). Per il numeratore e denominatore impostiamo la proprietà justify a right.
def __init__(self, parent, height=10, width=10, background=None):
self.parent=parent
self.height=height
self.width=width
self.NumeratorValue=IntVar()
self.NumeratorValue.set(1)
self.DenominatorValue=IntVar()
self.DenominatorValue.set(1)
self.MainFrame=Frame(self.parent, width=self.width, height=self.height)
self.Numerator=Entry(self.MainFrame,
textvariable=self.NumeratorValue,
width=self.width,
justify="right")
self.FractionLine=Label(self.MainFrame, text="------")
self.Denominator=Entry(self.MainFrame,
textvariable=self.DenominatorValue,
width=self.width,
justify="right")
self.Numerator.pack(side="top", padx=5, pady=5)
self.FractionLine.pack(side="top")
self.Denominator.pack(side="top", padx=5, pady=5)
if background:
self.MainFrame['background']=background
Pack, grid e place
Per fare in modo che sia possibile usare i metodi pack, grid e place per il nostro widget, essi andranno ridefiniti nella classe, eseguendoli con la stessa lista di parametri sul frame che abbiamo creato:
def pack(self, *args, **kwargs):
self.MainFrame.pack(*args, **kwargs)
def grid(self, *args, **kwargs):
self.MainFrame.grid(*args, **kwargs)
def place(self, *args, **kwargs):
self.MainFrame.place(*args, **kwargs)
Convalida dell'input
Se vogliamo effettuare una convalida dell'input durante la digitazione (per evitare che vengano inseriti caratteri non numerici, nel nostro caso) bisogna adottare una tecnica un po' particolare, che deriva dal fatto che si è costretti a richiamare funzionalità di basso livello di Tk.
Si inizia definendo una tupla contenente il comando di validazione, composto da una funzione e da una serie di parametri che si possono tenere in considerazione per la validazione stessa:
vcmd=(self.parent.register(self.doValidation), '%P')
Poi si impostano per il widget in cui fare la validazione i parametri validate (quando effettuare la validazione) e validatecommand (la tupla impostata prima):
self.Numerator=Entry(self.MainFrame,
....
validate="key",
validatecommand=vcmd)
Per validate
i valori validi sono key
(pressione di un tasto), focus
, focusin
, focusout
e all
.
I parametri che si possono impostare per la validazione sono i seguenti:
- %d = tipo di azione (1=inserimento, 0=cancellazione, -1 altro)
- %i = indice del carattere da inserire/cancellare, oppure -1
- %P = valore della casella di input, se la modifica è consentita
- %s = valore della casella prima della modifica
- %S = stringa di testo che dovrebbe essere inserita o cancellata, se esiste
- %v = tipo di validazione correntemente impostata
- %V = tipo di validazione che ha fatto scattare la funzione
- %W = nome Tk del widget
Ridefinizione metodi degli operatori per l'accesso agli attributi
Se si vuole fare in modo che sia possibile cambiare le proprietà del widget mediante la sintassi delle parentesi quadrate, si devono ridefinire gli operatori mediante le funzioni __setitem__
e __getitem__
:
def __setitem__(self, key, value):
if key in ('background', 'bg', 'borderwidth', 'bd', 'relief'):
self.MainFrame[key]=value
if key=='numerator':
self.setNumerator(value)
if key=='denominator':
self.setDenominator(value)
def __getitem__(self, key):
if key in ('background', 'bg', 'borderwidth', 'bd', 'relief'):
return self.MainFrame[key]
if key=='numerator':
return self.getNumerator()
if key=='denominator':
return self.getDenominator()
Barra di progressione
Una barra di progressione può essere creata basandosi su un canvas in cui si disegna un rettangolo colorato, come nel seguente esempio:
class ProgressBar(object):
def __init__(self, parent, height=10, width=300, background=None, foreground='black', progress=0):
# adapted from "Python 2.1 Bible" (by Dave Brueck and Stephen Tanner)
self._parent=parent
self._height=height
self._width=width
self._background=background
self._foreground=foreground
self.barcanvas=Canvas(
self._parent,
width=self._width,
heigh=self._height,
background=self._background,
borderwidth=1,
relief=SUNKEN
)
self.barcanvas.pack(padx=5, pady=2)
self.rectangleValue=self.barcanvas.create_rectangle(0,0,0, self._height)
self.barcanvas.itemconfigure(
self.rectangleValue,
fill=self._foreground
)
self.setProgressPercent(progress)
def getProgressPercent(self):
return self._progress
def setProgressPercent(self, level):
self._progress = min(100, max(0, level))
self.DrawProgress()
def DrawProgress(self):
pixels=round(self._width * self.getProgressPercent()/100.0)
self.barcanvas.coords(
self.rectangleValue,
0,
0,
pixels,
self._height
)
def pack(self, *args, **kwargs):
self.barcanvas.pack(*args, **kwargs)
def grid(self, *args, **kwargs):
self.barcanvas.grid(*args, **kwargs)
def place(self, *args, **kwargs):
self.barcanvas.place(*args, **kwargs)
Esecuzione di un programma esterno
Può capitare di dover eseguire un programma esterno per verificare un valore. Ad esempio, si immagini di volere che facendo clic su un pulsante venga avviato un programma in cui l'utente gioca a qualcosa. Il programma chiamante deve venire informato dell'esito del programma chiamato.
Per ottenere questo risultato, il programma chiamato deve uscire con un esito, che nel nostro esempio può essere 0 (il giocatore ha vinto), 1 (il giocatore ha perso) oppure 2 (il giocatore ha chiuso la finestra).
def main():
root = Tk()
myapp = Application(root)
root.mainloop()
return myapp.returnValue
if __name__=='__main__':
sys.exit(main())
Il programma chiamante, d'altro canto, deve essere in grado di avviare un altro programma e capire qual ne è stato l'esito. Per farlo, useremo il modulo subprocess:
try:
subprocess.check_output([interpretername(), 'thegame.pyw'])
self.statusbar['text']='You won!'
except:
self.statusbar['text']='You lose!'
Il nome dell'interprete varia da sistema a sistema, per cui possiamo definire una funzione interpretername() che ce lo restituisca a seconda di alcune considerazioni (file di configurazione, sistema operativo usato, o altro).
Si noti che questo metodo consente di far interagire programmi completamente indipendenti (anche scritti in altri linguaggi di programmazione).