Thursday, February 6, 2014

La prima applicazione, un pezzo alla volta

Interfaccia grafica puo' voler dire molte cose, ma la cosa più semplice è una finestra con qualcosa da scrivere dentro o da cliccare col mouse o da visualizzare all'interno.
Cominciamo proprio da qui, dalla finestra.
E cominciamo dal programma del precedente post, scomposto in modo da rendere facile capire come è costruito.
Ricopio parti del programma che siano comunque funzionanti e mostro un'immagine dell'eseguito, così possiamo capirne il significato.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#/usr/bin/env python
# -*- coding: utf-8 -*-

import swisseph
import Tkinter

class Applicazione:
    def __init__(self, myProva):

        self.myProva=myProva
        self.myProva.title('Piccola prova di Tkinter nel calcolo astronomico')


root = Tkinter.Tk()
app = Applicazione(root)
root.mainloop()
Ok, partiamo dalla finestra. Dopo l'importazione delle librerie, la prima cosa che facciamo è creare una classe, e poi crearne un'istanza. Non è difficile come appare, la classe è un insieme ordinato di variabili (proprietà) e funzioni (metodi) racchiusi dentro un contenitore che ha un nome. Pensatelo come il prototipo o il progetto o lo stampo di una serie di oggetti standard che si chiamano istanze, un po' come le auto che escono da una linea di produzione, una uguale all'altra. In Python trovate spesso un termine, self, che rappresenta l'identificativo dell'istanza in una classe, un po' come un badge personale, identico nella forma a tutti gli altri, ma associato ad una sola persona.
La classe che creiamo non deve necessariamente saper fare qualcosa, basta che esista, almeno all'inizio. Estraggo una parte del programma per essere più chiaro, lo eseguo in IDLE (l'editor presente di default nel pacchetto Python, sia in Windows, dove è integrato nell'installer, sia in Ubuntu Linux. dove si installa facilmente con Software Center, Synaptic o da riga di comando con il solito apt-get install), e inserisco più giù nel testo un'immagine del risultato.

Ecco, compare una finestra vuota, il titolo è anche incompleto, pero', per quello che le abbiamo chiesto di fare, funziona.
Il codice, procedendo verso il basso, si spiega così:
  • importo le librerie Tkinter e swisseph
  • definisco la classe Applicazione
  • inizializzo la classe con l'apposito costruttore def __init__(self, myProva), dove il self, come dicevo, rappresenta l'istanza che poi viene creata più avanti
  • dei due parametri che stanno dentro le parentesi, self è trasmesso in automatico, quindi non va in alcun modo richiamato durante la creazione dell'istanza. l'altro è una variabile generica che teoricamente puo' ricevere qualsiasi tipo - in Python ogni variabile, quando viene creata, non ha un tipo predefinito e assume quello della prima assegnazione, rispetto a C e Java è molto più comodo. Si parla di tipizzazione dinamica (non si preassegna il tipo in fase di creazione della variabile) e tipizzazione forte (una volta creato il tipo non si cambia più, salvo che non si rigeneri la variabile).
  • si copia il valore della variabile passata durante la creazione dell'istanza, qualunque sia il suo tipo, ad una variabile di istanza, legata cioè a quello specifico oggetto che riceverà un nome durante l'istanziazione. Tutte le variabili di istanza iniziano con self. La successiva istruzione assegna una stringa di testo al metodo title della nostra istanza, infatti è l'unica cosa che vediamo.

La parte finale del codice è quella che mette in moto il tutto. root è il nome dell'istanza della classe Tk che è presente nel modulo Tkinter. Una volta generata l'istanza root, questa viene passata all'atto della creazione dell'istanza di Applicazione che ho chiamato app, la riga finale mantiene indefinitamente il ciclo di root. Per il momento dimentichiamoci di questa parte, la riprenderemo alla fine del progetto.

Aggiungo qualche altra linea di codice:



1
2
3
4
        self.frame = Tkinter.Frame(myProva)
        self.frame.grid(row=0, column=0)
        self.titolo = Tkinter.Label(self.frame, text = "Titolo", bg = 'green', fg = 'black')
        self.titolo.grid(row=0, column=0, columnspan=6, sticky = 'we')
Se lancio in esecuzione il codice compare una cosa che magari non mi aspettavo:

Le istruzioni che ho aggiunto di fatto modificano la mia classe aggiungendo uno specifico componente, che è il Frame. Potete considerarlo il prototipo del contenitore, di cui self.frame è un'istanza. Il self anche in questo caso contrassegna l'istanza di Applicazione che abbiamo creato fuori della classe e rende visibile l'oggetto ovunque dentro la classe stessa.

il frame è molto elastico e si adatta al suo contenuto, che viene creato nella riga successiva. Notate che mentre quando si crea il Frame il primo parametro é myProva (che sarebbe il root passato durante la creazione di app), nel caso dell'oggetto self.titolo, istanza di Label, il primo parametro è self.frame, cioè la cornice contenitore. Il testo è stupido e infatti quando il Frame si chiude intorno praticamente la finestra scompare quasi. Ma non ci preoccupiamo e andiamo avanti. Prima pero' vi accenno alla geometry, che è la caratteristica che descrive come gli oggetti visuali si dispongono nello spazio. Qui ho scelto la grid, la griglia, che mi permette di inserire i widget in una griglia con righe e colonne numerate. Per altre applicazioni si puo' usare il metodo pack, che si arrangia da solo a disporre gli oggetti, ma lo trovo più difficile e scomodo da usare. Quando indichiamo il numero di riga e di colonna in cui sistemare il widget, cioè l'oggetto visuale, possiamo anche indicare un numero di colonne o di righe superiore, creando così una disposizione più fluida degli oggetti, utile quando vogliamo raggrupparli intorno ad una serie di oggetti di piccole dimensioni.

Aggiungiamo qualche altra riga, una serie di label, indicative di contenuti che andremo a inserire manualmente in una serie di caselle di testo. Aggiungo le relative righe di codice.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        self.lb_anno = Tkinter.Label(self.frame, text='Anno')
        self.lb_anno.grid(row=1, column=0)
        self.lb_mese = Tkinter.Label(self.frame, text='Mese')
        self.lb_mese.grid(row=1, column=1)
        self.lb_giorno = Tkinter.Label(self.frame, text = 'Giorno')
        self.lb_giorno.grid(row=1, column=2 )
        self.lb_ora = Tkinter.Label(self.frame, text='Ora')
        self.lb_ora.grid(row=1, column =3)
        self.lb_minuto = Tkinter.Label(self.frame, text='Minuto')
        self.lb_minuto.grid(row=1, column=4)

Le cose cominciano ad andare un po' meglio: le etichette (label) che ho aggiunto hanno modificato un po' la geometria della nostra finestra che comincia ad avere un aspetto un po' più interessante.

A parte il testo di ogni etichetta, la cui corrispondenza con l'immagine mi sembra chiara, e la specifica disposizione in righe e colonne, riga 1 e colonne da 0 a 4, vi prego di notare che il primo parametro durante l'istanziazione di ogni label è sempre self.frame, la nostra cornice iniziale. Non sarà sempre così, si possono usare più frame, volendo. Questa applicazione ne usa solo una.

Aggiungiamo altro codice, stavolta per creare le caselle di input che ci servono.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        self.en_anno = Tkinter.Entry(self.frame, width='14', justify='center')
        self.en_anno.grid(row=2, column=0)
        self.en_mese = Tkinter.Entry(self.frame, width='14', justify='center')
        self.en_mese.grid(row=2, column=1)
        self.en_giorno = Tkinter.Entry(self.frame, width='14', justify='center')
        self.en_giorno.grid(row=2, column=2)
        self.en_ora = Tkinter.Entry(self.frame, width='14', justify='center')
        self.en_ora.grid(row=2, column=3)
        self.en_minuto = Tkinter.Entry(self.frame, width='14', justify='center')
        self.en_minuto.grid(row=2, column=4 )

Osserviamo il risultato:

A parte la sparizione del testo delle etichette (riapparirà, non temete), va notato come, dando una larghezza fissa ad ogni casella, la cornice si sia finalmente distesa e il titolo appaia in tutto il suo splendore

La parte puramente grafica è quasi finita, ancora un piccola sforzo.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
        self.labelRes1=[]
        labelRes2=[]
        for i in range(4,16):
            for j in range (0,5):
                labelRes2.append(Tkinter.Label(self.frame,
                                               relief='sunken',
                                               text='--------',
                                               anchor='e',
                                               width=15)
                                 )
                labelRes2[-1].grid(row=i,column=j)
            self.labelRes1.append(labelRes2)
            
            labelRes2=[]

E questo è il risultato:

Non vi nascondo che ci ho dovuto pensare un po', non esistendo una tabella come widget predefinito.

Poi ho pensato che le caselle possono benissimo essere delle comunissime label, visto che servono solo a rappresentare i risultati, l'importante è sapere dove si trovano. Il metodo che ho scelto, non so se sia già stato pensato in precedenza, è creare una lista di liste, in cui ogni elemento della lista più esterna è composto da una lista di cinque label. In fondo nulla e nessuno vieta di creare liste con oggetti non convenzionali, la nostra fantasia non deve limitarsi a variabili int o float. Sapendo come rintracciare ogni elemento in base alla riga e alla colonna, dalla riga quattro alla sedici e dalla colonna 0 alla cinque, non devo più preoccuparmi di dove si trovano, lo sa la lista, in questo caso self.labelRes1. Nulla di particolare da notare se non che ho preferito un aspetto infossato (relief='sunken') e una disposizione allineata a destra per il testo, inizializzato ad una serie di trattini.


Ultimo sforzo: un pulsante per lanciare la routine di calcolo delle longitudini planetarie:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        self.bt_calcola = Tkinter.Button(self.frame, command=self.calcola,
                                         text='Calcola', bg='red', fg='blue')
        self.bt_calcola.grid(row=1, column=5, rowspan=14)


        self.lista_pianeti=['Sole', 'Luna', 'Mercurio', 'Venere', 'Marte',
                            'Giove', 'Saturno', 'Urano', 'Nettuno',
                            'Plutone', 'Nodo Lunare medio', 'Nodo Lunare vero']
        self.lista_segni=['Ariete','Toro','Gemelli','Cancro','Leone',
                          'Vergine','Bilancia','Scorpione','Sagittario',
                          'Capricorno', 'Acquario', 'Pesci']

ed ecco la foto (ho inserito in anticipo la funzione calcola solo per non fare sollevare un errore):

Il pulsante rosso a destra è quello che lancia la routine. Se osservate la riga di istanziazione, trovate un'istruzione command, che accetta il nome della funzione self.calcola, che aggiungeremo immediatamente dopo. Inutile dire che se lanciate il programma in questa fase va in errore, perchè la funzione non c'è ancora, io ne ho creata una fittizia.

Siccome in seguito mi serviranno i nomi dei pianeti e dei segni zodiacali ho creato due liste di appoggio.

Ok, ultimo pezzo, la routine di calcolo e stampa, con molta fantasia l'ho chiamata def calcola(self).


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    def calcola(self):
        giorno_giuliano=swisseph._julday(int(self.en_anno.get()),
                                         int(self.en_mese.get()),
                                         int(self.en_giorno.get()),
                                         int(self.en_ora.get()),
                                         int(self.en_minuto.get())
                                         )
        long_planet = [swisseph.calc(giorno_giuliano, x) for x in xrange(0,12)]

        for i in range(0,12):
            long_ecl = swisseph._degsplit(long_planet[i][0])
            self.labelRes1[i][0].configure(text=self.lista_pianeti[i])
            self.labelRes1[i][1].configure(text=str(long_ecl[0])+" °")
            self.labelRes1[i][2].configure(text=self.lista_segni[long_ecl[1]])
            self.labelRes1[i][3].configure(text=str(long_ecl[2])+" '")
            self.labelRes1[i][4].configure(text=str(long_ecl[3])+" \"")

E ora il programma puo' essere lanciato, basta inserire in ogni casella entry il dato corrispondente, indicato nella riga sopra, anno, mese, giorno, ora, minuto e schiacciare il pulsante rosso. Magicamente i risultati compariranno in ognuna delle label appositamente create per questo scopo.

Vediamo l'immagine finale:

Come vedete, l'ora in cui sto finendo il post è quella della foto, 2.46 e sono un po' cotto.

La routine di calcolo richiamata da swisseph fa quello che in poche righe faceva qualche post indietro, né più, né meno, ma l'aspetto è molto più ordinato e i dati sono più comprensibili. Per inciso, la funzione _degsplit di swisseph mi risolve il problema della rappresentazione del dato, perchè trasforma una longitudine compresa tra 0 e 360 gradi in grado e segno zodiacale, minuto e secondo, di lettura molto più agevole.

Penso che posso chiudere qui, se avete domande non esitate a commentare questo post. Buona notte a tutti, e alla prossima

No comments:

Post a Comment

How to create a virtual linux machine with qemu under Debian or Ubuntu with near native graphics performance

It's been a long time since my latest post. I know, I'm lazy. But every now and then I like to publish something that other people c...