Python.
Le librerie grafiche Tk

Tk e tkinter.

Le Tk sono delle librerie grafiche sviluppate originariamente per TCL. Oltre al TCL le Tk si possono utilizzare anche nei linguaggi Python, Ruby e Perl. Il sito ufficiale di TCL e di Tk è www.tcl.tk.

In Python le Tk si possono usare tramite il wrapper tkinter il cui sito ufficiale è wiki.python.org/moin/TkInter.

Un'ottima e moderna guida alle Tk e a tkinter è www.tkdocs.com, dove gli esempi mostrati sono disponibili per tutti e quattro i linguaggi di programmazione che supportano le Tk. Ecco altre due buone guide, solo per Python: effbot.org/tkinterbook/ e New Mexico Tech.

Le Tk sono già presenti in ogni distribuzione di Python. Quindi non dovete installare nulla. Ci sono però innumerevoli diverse estensioni che si sono stratificate nel tempo e che provarono a sopperire a diverse mancanze delle Tk. Attualmente molte di queste sono obsolete e le nuove caratteristiche che aggiungevano sono state oramai integrate nelle nuove versioni delle Tk. Per motivi di compatibilità però nelle Tk convivono sia queste nuove implementazioni degli elementi grafici (detti widget), sia quelle vecchie. In Python tkinter permette l'uso delle Tk ma di default usa i vecchi widget. Per usare quelli nuovi va caricato il sottomodulo ttk.

Per controllare che tutto sia funzionante basta lanciare una console Python ed eseguire queste righe

import tkinter
tkinter._test()

Le basi

Ecco un esempio minimo in forma procedurale.

# Carico il modulo tkinter e il sottomodulo ttk per usare i nuovi widget
from tkinter import *
from tkinter import ttk

# Creo l'oggetto "finestra principale" che chiamo root
root = Tk()

# Ci aggiungo un bottone (che non fa nulla) con una scritta
button = ttk.Button(root, text="Hello World").grid()

# Lancio l'event loop
root.mainloop()

I widget e le finestre

La lista dei widget a disposizione:

Frame
LabelFrame     (text)
PanedWindow    (text)
Notebook

Label          (text, textvariable)
Button         (text, command)
Checkbutton    (text, command, variable, onvalue, offvalue)
Radiobutton    (text, variable, value. Raggruppamento per variable) 
Entry          (textvariable)

Menubutton
Scale
Scrollbar
Combobox       (textvariable, values, readonly)
Progressbar
Separator
Sizegrip
Treeview

Il primo argomento del costruttore di tutti i widget è il parent object, che è tipicamente un Frame.

Ecco come creare delle nuove finestre o finestre di dialogo.

Toplevel

from tkinter import filedialog
filename = filedialog.askopenfilename()
filename = filedialog.asksaveasfilename()
dirname = filedialog.askdirectory()

from tkinter import colorchooser
colorchooser.askcolor(initialcolor='#ff0000')

from tkinter import messagebox

messagebox.showinfo(message='Have a good day')

messagebox.askyesno(
message='Are you sure you want to install SuperVirus?'
icon='question' title='Install')
# Icon to show: one of "info" (default), "error", "question" or "warning".

La grandezza e il posizionamento delle finestre

Ogni finestra ha il metodo geometry che permette di specificare l'angolo in alto a sinistra e lo shift in x e y rispetto all'angolo in alto a sinistra del monitor.

finestra.geometry('200x300+50+50')

Spesso si vuole centrare una finestra rispetto allo schermo o rispetto a un'altra finestra, per esempio quella principale del programma. Questo avviene per esempio quando si apre la finestra delle "Informazioni". Fate delle prove con alcuni programmi che conoscete e vedete di cosa sto parlando.

Per centrare la finestra ho creato una funzione apposita scopiazzandola da internet e modificandola leggermente. Ovviamente per funzionare dovete prima posizionare tutti i widget! Altrimenti quando la funzione vorrà sapere quando è grande la finestra per fare gli opportuni calcoli, otterrà un valore sbagliato. Come potete vedere questa funzione permette anche di specificare una larghezza e altezza desiderate e non quelle minime.

def _centeronscreen(self, window, w='', h=''):

        # Non so a cosa serva e l'ho commentato
        # parent = window.winfo_parent()
        # if type(parent) == types.StringType:
        #    parent = window._hull._nametowidget(parent)

        # Find size of window.
        window.update_idletasks()
        if w == '' or h == '':
            w = window.winfo_width()
            h = window.winfo_height()
            if w == 1 and h == 1:
                # If the window has not yet been displayed, its size is
                # reported as 1x1, so use requested size.
                w = window.winfo_reqwidth()
                h = window.winfo_reqheight()

        # Place in centre of screen:
        x = int((window.winfo_screenwidth() - w) / 2.0)
        y = int((window.winfo_screenheight() - h) / 2.0)
        if x < 0:
            x = 0
        if y < 0:
            y = 0
        geometry = str(w) + 'x' + str(h) + '+' + str(x) + '+' + str(y)
        window.geometry(geometry)
        return 0

Il posizionamento dei widget

Se si crea un oggetto dell'interfaccia grafica (un bottone, ecc...) ma non lo si posiziona, l'oggetto esiste ma non viene visualizzato.

Esistono tre metodi per posizionare gli oggetti: pack, grid e place. Pack è piuttosto flessibile e usato anche se molto noioso da usare; non permette la sovrapposizione dei widget. Alcuni lo considerano obsoleto e da abbandonare. Il metodo grid basa il posizionamento su una griglia dinamica di righe e colonne; due oggetti nella stessa casella (cioè stessa riga e colonna) saranno sempre sovrapposti. Molti lo considerano il metodo migliore anche se in alcuni casi si rivela troppo semplice. Place permette un posizionamento assoluto sulla finestra del programma; permette però anche un posizionamento relativo di un widget rispetto a un altro widget. Molti lo sconsigliano perché lascia troppi dettagli al programmatore; questo è vero ma è anche il metodo più potente di tutti ed è quello usato dai Gui builder come PAGE.

Il metodo grid

Il metodo grid presuppone una divisione in righe e colonne per ogni oggetto che ne può contenere altri. Per esempio la finestra principale del programma la si pensa divisa in un numero arbitrario di righe e colonne. Se dentro di questa si posiziona un Frame nella colonna 0 e riga 0 e un altro nella posizione colonna 1 riga 0, i due frame saranno affiancati.

---------------------
|         |         |
| Frame 1 | Frame 2 |
| C0 - R0 | C1 - R0 |
|         |         |
---------------------

A sua volta dobbiamo immaginare questi frame ognuno diviso come una scacchiera, e ognuno con la sua numerazione. Potremmo quindi posizionare due widget qualsiasi, dopo aver specificato il loro parent al momento della creazione, usando il metodo grid. La riga e la colonna che useremo si riferiranno alla griglia del loro parent.

Si può anche dire quante colonne o righe può occupare (di default può occupare una sola casella data dall'intersezione diuna riga con una colonna). Se due oggetti vengono messi nella stessa cella oppure si fa in modo che si sovrappongano, l'ordine in "z" corrisponde all'ordine di creazione dell'oggetto. Oggetti creati per ultimi saranno più in primo piano.

NOME_OGGETTO.grid(column=3, row=6, columnspan=3, rowspan=2)

Quando si posiziona un oggetto, va per forza posizionato anche il Frame padre, altrimenti l'oggetto non compare. Un'altra cosa da sapere è che non c'è bisogno di dare i comandi di posizionamento in un qualche ordine. Quando si esegue il mainloop tutte le informazioni sul posizionamento vengono raccolte e messe in pratica.

Un attributo interessante e molto utile è lo sticky. Obbliga il widget a non oltrepassare determinati lati di una cella indicati con i punti cardinali (NSOE). Infatti un widget se è troppo grande sborda dalla cella rimanendo centrato rispetto a essa.

NOME_OGGETTO.grid(sticky=(S,E))

Lo sticky serve anche per cambiare l'allineamento nella cella che di default è centrato. Mettere uno sticky a W (ovest) equivale ad allineare il widget a sinistra.

La barra dei menù

A meno di creare un programma veramente potente e innovativo (vedi Google Chrome o Skype), sotto Windows ogni programma che si rispetti deve avere una barra dei menu con almeno le voci "File" e "Help". A sua volta nel menù "File" deve comparire almeno la voce "Exit" mentre nel menù "Help" deve comparire almeno la voce "About..." che apre una finestra secondaria con le informazioni sull'autore e sulla licenza del programma in uso.

Ecco come si crea una barra dei menù minimale. Questo è un esempio preso dal costruttore di una classe per creare l'interfaccia grafica.

# Serve per "modernizzare" l'interfaccia
self.root.option_add('*tearOff', tk.FALSE)

# Creo la barra dei menù e la collego alla finestra principale
self.menubar = tk.Menu(self.root)
self.root.config(menu=self.menubar)

# Creo il menù File, lo collego alla barra dei menù e infine ci inserisco
# la voce Exit associata al metodo destroy che chiude il programma
self.menu_file = tk.Menu(self.menubar)
self.menubar.add_cascade(label='File', menu=self.menu_file)
self.menu_file.add_command(label='Exit', command=self.destroy)

# Creo il menù Help, lo collego alla barra dei menù e infine ci inserisco
# la voce About... associata al metodo AboutWindow che crea la finestra con
# le info sull'autore e la licenza
self.menu_help = tk.Menu(self.menubar)
self.menubar.add_cascade(label='Help', menu=self.menu_help)
self.menu_help.add_command(label='About', command=self.AboutWindow)


# Ecco un esempio di codice per la finestra About...
def AboutWindow(self):
  # Il comando Toplevel crea una finestra secondaria
  self.AboutWindow = tk.Toplevel(self.root)
  self.AboutWindow.title('About Lux')
  self.AuthorLabel = tk.Label(self.AboutWindow,
  text='Author: Francesco Biccari\n (c) 2011-2013')
  self.AuthorLabel.pack()
  self.LicenceLabel = tk.Label(self.AboutWindow, text='Licence: GPL v3')
  self.LicenceLabel.pack()
  # Rende la finestra delle informazioni "modale" cioè non permette di
  # toccare le altre finestre
  self.AboutWindow.grab_set()
  # Implementare la centratura della finestra Informazioni e renderla
  # più bella. Dimensioni più grandi ecc...

Dialog che si possono aprire

Questi tre metodi restituiscono rispettivamente il percorso completo del file da aprire, il percorso completo del file da salvare e il percorso completo della directory scelta.

file_da_aprire = filedialog.askopenfilename()
file_da_salvare = filedialog.asksaveasfilename()
dir_scelta = filedialog.askdirectory()

Poi ci sono i classici

messagebox.showinfo(message='Have a good day')
messagebox.askyesno(
	   message='Are you sure you want to install SuperVirus?'
	   icon='question' title='Install')
     
askokcancel, askquestion, askretrycancel, askyesno, askyesnocancel, showerror, showinfo, showwarning
Opzioni:
message, detail, title,	icon ("info" (default), "error", "question" or "warning"),
default	("ok" or "cancel" for a "okcancel" type dialog), parent

Strutturare un programma complesso con interfaccia grafica

Un programma che si rispetti deve essere strutturato in classi a meno che sia molto semplice.

Se il programma è abbastanza semplice si può usare un singolo file

# Moduli
# ...
from tkinter import *
# ...


# La classe dell'interfaccia grafica
class UI:
  def __init__(self,parent):
    self.UIParent = parent
    self.UIFrame = Frame(parent)
    # ...
    
  # metodi legati agli eventi dell'interfaccia
  

  
# Altre classi
# ...


# La funzione main del programma
def main():
  root = Tk()
  root.title("Titolo finestra")
  ui = UI(root)
  root.mainloop()
  
# Entry point del programma
main()

Matplotlib in Tk

Se si vuole inserire una finestra di Matplotlib (una delle librerie più famose in Python per fare grafici) in una interfaccia realizzata con le librerie Tk, bisogna usare il backend TkAgg (fa parte di matplotlib).

Un codice di esempio è presente sulla guida ufficiale di Python: user_interfaces example code: embedding_in_tk.py.

Sfortunatamente nella distribuzione Anaconda, matplotlib non è compilato per il supporto per tkinter. Quindi se provate a far funzionare il codice della guida ufficiale otterrete un crash. Quindi come spiegato a questo link, basta disinstallare pillow e matplotlib, scaricare delle versioni compilate per tkinter (per esempio da qui) e installarle. Da riga di comando:

conda uninstall pillow
conda uninstall matplotlib
pip install Pillow-...whl
pip install matplotlib-...whl

Spesso i plot sono usati per rappresentare in tempo reale dei dati. E solitamente questi vengono acquisiti in un for o un while. Per tutto il tempo dell'esecuzione del for o del while però, l'interfaccia risulterà irrimediabilmente bloccata. Per ovviare a questo inconveniente si possono usare i thread oppure dei bindings richiamati con il metodo after fornito da Tk. Vedremo quest'ultimo metodo perché per cose semplice è il più immediato.

Ecco un esempio di funzione lanciata alla pressione di un certo bottone nell'interfaccia grafica. Quello che fa è acquisire mille volte un certo dato con la funzione Acquire(), aggiungere il dato a una lista con tutti i dati e ogni volta plottare questa lista in un grafico. Così facendo però l'interfaccia non risponde più appena si prova a cliccarci sopra.

# Il costruttore dell'interfaccia grafica
def __init__():
    ...
    self.a = []
	...

# Funzione lanciata dalla pressione di un bottone
def PrendiDati(self, event=None):
    a = []
	i = 0
	while i<1000:
	    x = self.Acquire()
		a.append(x)
		self.axes.plot(a)
		self.canvas.draw()
		i = i+1

La soluzione che sfrutta il metodo after delle librerie Tk è questa: Il metodo lanciato dal bottone, lancia a sua volta un altro metodo che deve fare la serie di operazionoi che prima venivano svolte in un ciclio del while. Alla fine di questo metodo, va istruito Tk a rilanciare lo stesso metodo fra 100 ms, a meno che il numero delle acquisizioni non sia arrivato a 1000. In quei 100 ms di attesa tra un'esecuzione e l'altra del metodo AcquireNext() l'interfaccia diventa usabile e gli eventi vengono processati.

# costruttore dell'interfaccia grafica
def __init__():
    ...
    self.a = []
	self.n = 0
	...

# metodo da lanciare ogni 100 ms
def AcquireNext():
    x = self.Acquire()
	self.a.append(x)
	self.axes.plot(a)
	self.canvas.draw()
	self.n = self.n + 1
	if self.n < 1000:
	    self.root.after(100, AcquireNext)
	

# Funzione lanciata dalla pressione di un bottone
def PrendiDati(self, event=None):
    self.a = []
	self.n = 0
	self.AcquireNext()