AkiRoss' H.o.E - Python + PyGTK + Glade

Alessandro "AkiRoss" Re -

Original text, in Italian - 20040901

Oggi sono extragasato, ho deciso quindi di rendere partecipi altri animi e spiegare cos'e' successo :)
E' stato al Webbit 2004 (il primo che ho seguito a Milano) che ho visto una introduzione allo sviluppo rapido di applicazioni usando Python e Glade, e quindi la libreria PyGTK.

Una breve introduzione e' dovuta.
Python e' un linguaggio Open Source che sta prendendo piede velocemente. Sostanzialmente

GTK sono le Gimp Tool Kit, le famosissime librerie utilizzate per costruire interfaccie grafiche eleganti e funzionali, nonche' (mio parere personale) veloci e multipiattaforma (Unix-like, Windows, Mac e suppongo altro).
In particolare con Python si usano le PyGTK, di cui tratteremo solo in minima parte.

Glade e' un'ottima invenzione: e' un programma che consente di creare facilmente interfaccie grafiche con pochi colpi di mouse e di associare immediatamente gli eventi degli oggetti alle funzioni di callback. Poi e' possibile esportare l'interfaccia in linguaggio C/C++ e avere direttamente le funzioni interessate e l'inizializzazione dell'interfaccia gia pronta per l'uso.
Questo pero' ha un piccolo difetto: ad ogni cambiamento dell'interfaccia e' necessario ricostruire il codice C, con catastrofica :D perdita di tempo
La grande invenzione e' stata libglade. Questa libreria e' in grado di leggere il file con cui glade salva le interfaccie grafiche, di effettuare un parsing dello stesso e di creare dinamicamente in run-time l'interfaccia e gli eventi da gestire. Questo ovviamente ci permette di risparmiare tempo: cambiando l'interfaccia, non e' necessario ricompilare tutto in C, visto che libglade lo fara' dinamicamente per noi.

Detto questo mi sembra evidente che per lavorare con queste belle cose e' necessario avere sul proprio computer:

Io che vivo nel mondo gentoo (e non mi stacchero' mai :) mi basta emergere python, pygtk, glade e libglade.
Una volta installato tutto, possiamo iniziare a fare qualcosa: un piccolo tutorial classico: la calcolatrice.

Iniziamo con lo sviluppo della calcolatrice. Nella prima fase costruitemo l'interfaccia con glade-2 e stabiliremo le callback utilizzate. In pagina 3 scriveremo il codice.

Passo 1 - Nuovo progetto

Avviamo glade-2, che si presenta in questo modo

Lo strumento e' composto principalmente da 3 finestre:
  1. La finestra pincipale, quella in alto
  2. La finestra dei widgets (palette), a sinistra
  3. La finestra delle propieta', a destra
La prima e' dove si trova il menu' di gestione dei progetti, di aiuto, delle opzioni. La casella bianca sotto il menu' inoltre conterra' l'elenco di tutte le finestre appartenenti al nostro progetto. Nel nostro caso una soltanto.
Premendo su New creiamo un progetto GTK. Per il nostro scopo sara' sufficiente il set di widget GTK. Sappiate pero' che un progetto GNOME mette a disposizione widget piu' avanzati e complessi.

Passo 2 - La finestra principale

A questo punto e' necessario dire a glade che vogliamo una finestra nel nostro progetto. Premiamo sul pulsante Window, nella palette . Apparira' una finestra con uno sfondo un po' particolare, ad indicare che non e' stato posizionato nulla su di essa.
Prima di procedere, cambiamo qualche impostazione nelle proprieta' della finestra: innanzi tutto il nome. Assegnamo un nome significativo, tipo winCalc. Poi diamo un titolo adeguato alla nostra finestra. Io ho messo banalmente PyCalc. Altre modifiche le rimando a dopo
A differenza di altri ToolKit (che usano un sistema a coordinate), GTK usa un sistema insolito, ma efficacie ed ordinato per disporre i widget sulla finestra: si divide la finestra in celle, e in ogni cella sara' possibile posizionare un widget.
Per la nostra calcolatrice dividiamo la finestra in 2, creando una divisione verticale con il pulsante Vertical Box

semplicemente scegliendo il tool e cliccando sulla finestra. Nella parte superiore posizioneremo il display, in quella inferiore i pulsanti della calcolatrice.
Ovviamente per posizionare i pulsanti sara' necessario dividere la parte inferiore con altre sottocelle, mediante il pulsante Table
. Clicchiamo sulla parte inferiore e scegliamo di mettere 3 righe e 4 colonne.
Fatto questo, otterremo una finestra simile alla seguente.

E' anche possibile cambiare i nomi ai riquadri che conterranno i nostri widget, ma visto che essi rimangono solitamente immutati e invisibili nel corso dell'applicazione, rinominarli e' superfluo.

Passo 3 - Posizionare i widget

Sempre con il solito "choose-'n-click" (scegli il widget e clicca nel riquadro dove posizionarlo), mettiamo sulla finestra anche gli altri widget necessari: 10 pulsanti per le cifre, 1 pulsante per cancellare il risultato, 1 pulsante per uscire e, ovviamente, un campo label per visualizzare il risultato. Usiamo quindi i pulsanti e e posizioniamoli come in figura.
L'aspetto della nostra finestra e' tutt'altro che invitante. Allora iniziamo prima di tutto a cambiare le Label (il testo scritto sul pulsante) e i nomi dei pulsanti. Io ho usato le cifre da 0 a 9, "OFF" e "CL" per le label, e nomi simili a: cmd0 cmd1 ... cmdOff e cmdCl. Inoltre, vediamo di disporre i pulsanti in modo un po' piu' decoroso sulla finestra. Selezionando dalla finestra principale di galde il widget winCalc,e premendo il tasto destro su di esso, e' possibile avere un elenco dei widget posizionati su di essi. Scegliamo vbox1 e selezioniamolo.
Nella finestra delle proprieta' abbiamo i valodi Border Width (spessore del bordo) e Spacing (spaziatura) che ci aiutano a posizionare i widget. Il primo ci indica quanto sono distanti le celle dal bordo, mentre il secondo quanto sono distanti le celle tra di loro. Io ho utilizzato un valore pari a 15 per il Border Width e di 20 per lo Spacing.
Poi scegliamo la tabella nella quale abbiamo posizionato i pulsanti. Impostiamo Row spacing e Col spacing su 20. Ora selezioniamo un pulsante alla volta, e nella finestra delle proprieta', sotto la voce Packing, impostiamo Expand e Fill su Yes, sia per X che per Y. In questo modo i pulsanti andranno ad occupare la casella completamente.
Ora selezionare la tabella dove abbiamo posizionato i pulsanti, nella tabella Common delle proprieta', cambiare le dimensioni Width ed Height della tabella a circa 300x200 (almeno cosi' ho messo io), cosi' che la sua dimensione sia fissa, e la finestra appaia come vogliamo. Poi allo stesso modo cambiamo la dimensione del display. Infine selezionate la finestra e disabilitate Resizable. La finestra apparira' come la vedete, e rimarra' tale.
Si noti che il font della capion non viene impostato direttamente da glade, ma viene impostato dal programma (ma a me per ora non interessa quindi lo lascio cosi :)

Dio mio, ho dimenticato di mettere i 4 pulsanti per le operazioni aritmetiche! Niente paura ;) selezioniamo la tabella, e nella finestra proprieta' impostiamo a 4 le righe. Ora aggiungiamo i pulsanti per le operazioni, cambiamo loro nome e proprieta' come abbiamo fatto per gli altri. Infine cambiamo le proprieta' della label in modo che il testo sia centrato e la label estesa a tutta l'ampiezza della cella. Io l'ho chiamata lblRes, mentre ho chiamato cmdP, cmdM, cmdR e cmdD i pulsanti per le operazioni + - * /.
Cosa? Si, manca anche il pulsante con l'uguale sopra, ma ho un metodo per risparmiare spazio che vedremo dopo...
Il risultato finale dovrebbe essere simile al seguente.

Passo 4 - Assegnazione delle callback

L'ultimo compito di glade e' quello di associare un evento ad una funzione. Ad esempio, noi vogliamo che al premere del pulsante CL la calcolatrice venga ripulita.
Per fare cio', selezioniamo il pulsante CL e nella finestra delle proprieta' andiamo sul tab Signals (segnali). Ora scegliamo il segnale GtkButton->clicked. Glade in automatico dara' il nome all'handler, che possiamo cambiare ma a me piace, quindi lo lascio cosi': on_cmdCl_clicked. Premiamo il pulsante Add per aggiungere il segnale. Cosi' diciamo al nostro programma (quando sara' completo :) che alla pressione del pulsante CL verra' chiamato l'handler on_cmdCl_clicked, che non sara' altro che una funzione.
Ora con lo stesso procedimento facciamo in modo che: A questo punto abbiamo finito di usare glade-2. Salviamo il nostro progetto da qualche parte e passiamo alla fase successiva: la stesura del codice.

Passo 5 - Scrittura del codice

A questo punto e' necessario scrivere il codice. Ovviamente e' necessario avere una minima conoscenza di Python o di un altro linguaggio, almeno per capire cosa si sta facendo :D
In ogni caso noterete (se avete esperienze passate con GTK ma non con libglade) di come sia breve la parte di inizializzazione della GUI.
In questo esempio si creera' una classe, contente tutte le routine necessarie per il programma. Vorrei ricordare che questo e' un esempio: non ha la presunzione di essere un buon pezzo di codice (anche perche' non e' da molto che uso python), quindi non prendetelo come "modello perfetto".
E come al solito, il programma e' sotto la licenza GNU/GPL v2 o successiva.

Bene, iniziamo a codificare.
Andiamo nella directory dove abbiamo salvato il progetto fatto con glade. Dovrebbero esserci 2 file, ma quello che ci interessa ha estensione .glade. In questa directory creiamo un file chiamato pycalc.py e diamogli permessi di esecuzione.

chmod 755 pycalc.py

prendiamo il nostro editor di testi preferito (GVIM per quanto mi riguarda) e iniziamo con il codice. Per gestire l'applicazione creiamo una classe, iniziamo a definire le funzioni e nel passo successivo le implementiamo. Sostanzialmente utilizzeremo questi metodi:


Here we go!

#!/usr/bin/python # eventualmente cambiate il path per il vostro sistema # FILE: pycalc.py # TITLE: PyCalc - Semplice calcolatrice in Python e PyGTK + Glade-2 # Copyright Alessandro <AkiRoss> Re # Licenza GNU/GPL v2 # Modules we need from gtk import *; from gtk.glade import *; # Application class PyCalc: def __init__(self): """ Just initialize the app, load the glade file and connect the signals """ def PerformNumber(self, obj): """ This function add a cipher to the display """ def PerformOp(self, obj): """ This function perform the operation between the current and next number """ def PerformClear(self, obj): """ This function reset the display and the memory """ def PerformOnOff(self, obj): """ This function set visibile/invisibile the display, change the caption for the cmdOff button and reset the screen on power on """ def CloseApp(self, obj): """ Term the apps mainloop """; mainquit(); def Main(self): """ Start the apps mainloop """; mainloop(); # Start! App = PyCalc(); App.Main();

Ecco la prima versione del programma. Assolutamente vuota, non fa nulla, ma e' la base della nostra applicazione. Come prima cosa, direi che e' opportuno inizializzare il tutto: carichiamo il file salvato da glade, connettiamo le callback alle funzioni e visualizziamo la nostra calcolatrice.
Quello che dobbiamo fare e' semplicissimo: modifichiamo la funzione __init__ aggiungendo due sole righe di codice:

self.xml = XML("pycalc.glade"); # Parse the XML file self.xml.signal_autoconnect({"on_winCalc_destroy" : self.CloseApp});

La prima effettua un parsing del file specificato, la seconda connette la funzione PyCalc::CloseApp all'handler on_winCalc_destory. In sostanza: quando arrivera' il segnale winCalc_destroy, il programma chiamera' la funzione CloseApp, che provvedera' a terminare il main loop e quindi uscire.
Provate ad eseguire il programma e a chiudere la finestra. Come vedrete il programma termina correttamente: la finestra scompare e ritorna la shell. Se commentate con un # la riga del signal_autoconnect, avviate l'applicazione e provate a chiuderla, noterete che la finestra sparira', ma il programma sara' ancora in esecuzione. A questo punto e' necessario terminarlo con un Ctrl-C o una kill.
Bene, capito cosa succede, ci conviene associare ogni altra chiamata ad una funzione. Sempre in __init__ mettiamo queste righe:

self.xml.signal_autoconnect({"on_cmdCl_clicked" : self.PerformClear}); self.xml.signal_autoconnect({"on_cmdNumber_clicked" : self.PerformNumber}); self.xml.signal_autoconnect({"on_cmdOp_clicked" : self.PerformOp}); self.xml.signal_autoconnect({"on_cmdoff_clicked" : self.PerformOnOff});

E' facile intuire a cosa servono. Ma se volete verificare cosa fanno, vi basta aggiungere una riga del tipo

print "Questa funzione, funziona!";

nelle funzioni Perform*, e testate pure il programma premendo qualche pulsante.
Ecco il codice (con qualche ritocco personale) fino ad ora

class PyCalc: def __init__(self): """ Just initialize the app, load the glade file and connect the signals """ self.xml = XML("pycalc.glade"); # Parse the XML file self.xml.signal_autoconnect({"on_winCalc_destroy" : self.CloseApp}); self.xml.signal_autoconnect({"on_cmdCl_clicked" : self.PerformClear}); self.xml.signal_autoconnect({"on_cmdNumber_clicked" : self.PerformNumber}); self.xml.signal_autoconnect({"on_cmdOp_clicked" : self.PerformOp}); self.xml.signal_autoconnect({"on_cmdOff_clicked" : self.PerformOnOff}); def PerformNumber(self, obj): """ This function add a cipher to the display """ print "Questa funzione (Number) funziona!"; def PerformOp(self, obj): """ This function perform the operation between the current and next number """ print "Questa funzione (Operation) funziona!"; def PerformClear(self, obj): """ This function reset the display and the memory """ print "Questa funzione (Clear) funziona!"; def PerformOnOff(self, obj): """ This function set visibile/invisibile the display, change the caption for the cmdOff button and reset the screen on power on """ print "Questa funzione (On/Off) funziona!"; def CloseApp(self, obj): """ Term the apps mainloop """; print "Chiusura di PyCalc. Grazie per avermi usato :D"; mainquit(); def Main(self): """ Start the apps mainloop """; print "PyCalc - by AkiRoss"; print "Semplice calcolatrice in Python e GTK"; print "http://akiross.hopto.org"; mainloop();

Io ho provato tutti i tasti e fanno quello che devono.
Ora e' necessario far fare qualcosa a questi bellissimi widget.
La prima cosa che ci interessa e' capire come facciamo a usare i metodi dei widget. Prima di tutto apriamo con Mozillone (si vabe, un browser decente insomma, anche Opera, Netscape, Konqueror, Camino o Safari va bene ;) la reference di PyGTK . Sotto la voce "PyGTK Class Hierarchy" troviamo tutti i widget messi a disposizione da PyGTK, in gerarchia.
Questo ci sara' molto utile se vogliamo cercare una proprieta' di un certo widget: lo cerchiamo nell'elenco, guardiamo il widget e se non troviamo quello che cerchiamo nell'elenco, risaliamo nell'albero fino a che non lo troviamo. Se proprio non c'e', magari abbiamo le idee un po' confuse ;)
A questo punto e' necessario codificare: come faccio ad ottenere il riferimento (creato dinamicamente da libglade) ad un certo pulsante? Semplice: se il pulsante che ci interessa e' quello che e' appena stato premuto, il suo riferimento e' contenuto nella variabile obj, che e' il secondo parametro delle funzioni di callback. Quindi ci bastera' fare obj.quello_che_ci_pare per accedere ai suoi metodi o alle sue proprieta'.
Un esempio e' piu' esplicativo:

print "Questa funzione (Number) funziona! ", print "Ed e' stato premuto il pulsante numero " + obj.get_label();

Provate e guardate i risultati se premete qualche numero. Incredibile, tutto questo in cosi' poche righe di codice. Grandi orizzonti si dovrebbero aprire, a questo punto, davanti ai vostri occhi. Per me e' stato cosi', quasi lo stesso sentimento che ho provato con il mio primo "Hello, World!" in C. Transeat (passiamo oltre).
Se avete una mente geniale come la mia, dovreste aver gia capito perche' io ho fatto una callback on_cmdNumber_clicked anziche' 10 callback on_cmd*_clicked: prima di tutto perche' risparmiamo qualche riga di codice (il che non e' mai male), e in secondo luogo perche', da quello che abbiamo visto, e' possibile reperire il numero del pulsante direttamente all'atto della sua pressione. Se siete piu' geniali (avete un idea ancora migliore, rivoluzionaria :) per favore scrivetemi perche' vorrei trovarmi davvero con un genio davanti. Fino ad ora ho visto qualche persona al mio livello, molte piu' stupide (o almeno cosi' mi e' apparso) e poche meglio di me, ma ancora nessuna che possa considerare genio (a parte Coda, ovviamente ;). Transeat (2).
Ora che abbiamo gia' intuito come funzionera' il programma, la nostra mente geniale gia pensa ad un piccolo problema: "Ok, ma lo schermo come lo cancello?? C'e' scritto "Risultato" adesso, non sarebbe meglio cancellarlo con CL o addirittura all'accensione della calcolatrice?".
Si, si puo' fare, e per farlo dobbiamo usare una funzione che, in base al nome del widget, ritorna un riferimento ad esso e poi, sempre con il nostro albero dei widget sottomano, lo usiamo come ci pare.

def __init__(self): ... self.display = self.xml.get_widget("lblRes");

in questo modo, abbiamo un riferimento sempre presente al display, in qualsiasi momento vogliamo accedervi ci bastera' usare self.display. Nella riga seguente mettiamo infine

self.display.set_label("0");

In questo modo, all'avvio del programma il display verra' cancellato. Provare per credere.
E' stato in questo momento, in cui ho capito di avere un basilare controllo sui widget, su glade e con il widget tree sottomano, che ho capito che niente in PyGTK mi avrebbe fermato. Ho percepito per 1 nanosecondo l'onnipossenza, ma poi sono tornato sulla terra e ho deciso di scrivere questo tutorial :D Spero possa farvi la stessa impressione e spero che questi miei scorci di vita possa in qualche modo tenervi su il morale durante questa lungo tutorial palloso. Transeat (3).

Bene, appurato questo ora dobbiamo solo dedicarci ad un po' di normalissima programmazione, visto che sappiamo ormai tutto (o quasi) su come controllare i nostri widget:

  • Alla pressione di un tasto numerico viene accodato nel display la cifra indicata
  • Alla pressione di un tasto di operazione
  • Se non era ancora stato premuto (variabile tmpOp vuota)
    1. Letta la stringa sul display
    2. Convertita in numero intero
    3. Letto il genere di operazione da effettuare e salvataggio in una variabile temporanea tmpOp
    4. Cancellato il display
    5. Tutti i tasti di operazione diventano degli '='
    Altrimenti (se era stato premuto, tmpOp settata con una delle 4 operazioni)
    1. Lettura del display e conversione in numero
    2. Lettura di tmpOp ed effettuazione del calcolo
    3. I tasti delle operazioni tornano normali
    4. Visualizzazione del risultato su display
    5. tmpOp viene svuotata
  • Alla pressione del tasto Off/On
  • Tutti i pulsanti vengono messi in stato di partenza Il display viene azzerato Se il tasto e' Off
    1. Viene settato su On
    2. Il display viene reso visibile
    Altrimenti (se era On)
    1. Viene settato su Off
    2. Il display viene reso invisibile
  • Alla pressione di Cl viene ripulito tutto: tutti i pulsanti in stato di partenza, display azzerato.
  • Prego, a voi il codice :D
    Una volta finito, o in caso di dubbio/errori, guardate pure il sorgente finito e ultimato che segue NOTA: La funzione Cl effettua il reset dello stato dei pulsanti e azzeramento del display, viene chiamata da __init__ e da On/Off per praticita'.

    #!/usr/bin/python # eventualmente cambiate il path per il vostro sistema # FILE: pycalc.py # TITLE: PyCalc - Semplice calcolatrice in Python e PyGTK + Glade-2 # Copyright Alessandro <AkiRoss> Re # Licenza GNU/GPL v2 # Modules we need from gtk import *; from gtk.glade import *; # Application class PyCalc: def __init__(self): """ Just initialize the app, load the glade file and connect the signals """ self.xml = XML("pycalc.glade"); # Parse the XML file self.xml.signal_autoconnect({"on_winCalc_destroy" : self.CloseApp}); self.xml.signal_autoconnect({"on_cmdCl_clicked" : self.PerformClear}); self.xml.signal_autoconnect({"on_cmdNumber_clicked" : self.PerformNumber}); self.xml.signal_autoconnect({"on_cmdOp_clicked" : self.PerformOp}); self.xml.signal_autoconnect({"on_cmdOff_clicked" : self.PerformOnOff}); self.tmpOp = "="; # get reference for each widget self.display = self.xml.get_widget("lblRes"); self.c0 = self.xml.get_widget("cmd0"); self.c1 = self.xml.get_widget("cmd1"); self.c2 = self.xml.get_widget("cmd2"); self.c3 = self.xml.get_widget("cmd3"); self.c4 = self.xml.get_widget("cmd4"); self.c5 = self.xml.get_widget("cmd5"); self.c6 = self.xml.get_widget("cmd6"); self.c7 = self.xml.get_widget("cmd7"); self.c8 = self.xml.get_widget("cmd8"); self.c9 = self.xml.get_widget("cmd9"); self.cP = self.xml.get_widget("cmdP"); self.cM = self.xml.get_widget("cmdM"); self.cR = self.xml.get_widget("cmdR"); self.cD = self.xml.get_widget("cmdD"); self.cCl = self.xml.get_widget("cmdCl"); self.cOff = self.xml.get_widget("cmdOff"); # reset self.PerformClear(0); def PerformNumber(self, obj): """ This function add a cipher to the display """ val = int(self.display.get_label()); val *= 10; val += int(obj.get_label()); self.display.set_label(str(val)); def PerformOp(self, obj): """ This function perform the operation between the current and next number """ if (self.tmpOp == "="): # save the value in memory self.memory = int(self.display.get_label()); # get operation self.tmpOp = obj.get_label(); self.display.set_label("0"); self.cP.set_label("="); self.cM.set_label("="); self.cR.set_label("="); self.cD.set_label("="); else: self.memory2 = int(self.display.get_label()); # perform the operation if (self.tmpOp == "+"): self.memory += self.memory2; elif (self.tmpOp == "-"): self.memory -= self.memory2; elif (self.tmpOp == "/"): self.memory /= self.memory2; else: self.memory *= self.memory2; self.PerformClear(0); self.display.set_label(str(self.memory)); self.tmpOp = "="; def PerformClear(self, obj): """ This function reset the display and the memory """ self.display.set_label("0"); self.c0.set_label("0"); self.c1.set_label("1"); self.c2.set_label("2"); self.c3.set_label("3"); self.c4.set_label("4"); self.c5.set_label("5"); self.c6.set_label("6"); self.c7.set_label("7"); self.c8.set_label("8"); self.c9.set_label("9"); self.cP.set_label("+"); self.cM.set_label("-"); self.cR.set_label("*"); self.cD.set_label("/"); self.cOff.set_label("Off"); def PerformOnOff(self, obj): """ This function set visibile/invisibile the display, change the caption for the cmdOff button and reset the screen on power on """ # save button status (clear will erase it!) stat = obj.get_label(); # reset all the table self.PerformClear(0); # check if (stat == "Off"): obj.set_label("On"); self.display.hide(); # make it visibile else: # Clear has already set the label as off self.display.show(); # make it visibile def CloseApp(self, obj): """ Term the apps mainloop """; print "Chiusura di PyCalc. Grazie per avermi usato :D"; mainquit(); def Main(self): """ Start the apps mainloop """; print "PyCalc - by AkiRoss"; print "Semplice calcolatrice in Python e GTK"; print "http://akiross.hopto.org"; mainloop(); # Start! App = PyCalc(); App.Main();

    Umm mi sembra di aver fatto tutto e non aver tralasciato niente, in caso una segnalazione per favore.
    A me sembra che funzioni (senza controllo della lunghezza del display). Come avrete notato, nascondendo la label i pulsanti si spostano. Questo perche' nella creazione della GUI abbiamo scelto di espanderli (in effetti basterebbe settare la label del display a "" anziche' nasconderlo del tutto). Basta una modifica all'interfaccia e voila'! Sparisce il display e tutto rimane fermo, a riprova del fatto che glade e' uno strumento molto utile.

    Spero che questo tutorial semplice (ma non breve ;) sia servito a voi. Sicuramente a me e' servito.