Saturday, February 28, 2015

Widget Calendario con Tkinter - Tkinter Calendar Widget part 2

Siamo arrivati alla parte 'intelligente' del codice, cioè la gestione della data conseguente all'uso del mouse sulle spinbox o sui button del giorno.

In un post precedente ho parlato delle Variable Classes, cioè di quelle particolari variabili che sono collegate ai widget e determinano, in relazione a specifici eventi, di chiamare delle funzioni di callback.

Allego di seguito un'altra sezione del codice:

        # some Variable Classes, when updated a callback function is activated
        self.year = IntVar(self.Calendar)
        self.year.set(self.dtime.year)
        self.year.trace('w', self.check_and_reconfigure)
        self.month = IntVar(self.Calendar)
        self.month.set(self.dtime.month)
        self.month.trace('w', self.check_and_reconfigure)
        self.day = IntVar(self.Calendar)
        self.day.set(self.dtime.day)
        self.day.trace('w', self.check_and_reconfigure)
        self.hour = IntVar(self.Calendar)
        self.hour.set(self.dtime.hour)
        self.hour.trace('w', self.check_and_reconfigure)
        self.minute = IntVar(self.Calendar)
        self.minute.set(self.dtime.minute)
        self.minute.trace('w', self.check_and_reconfigure)
        # bottom Frame and call to functions
        self.month_length = 0
        self.spin_year.config(textvariable = self.year)
        self.spin_month.config(textvariable = self.month)
        self.spin_hour.config(textvariable = self.hour, justify = RIGHT)
        self.spin_minute.config(textvariable = self.minute, justify = RIGHT)
        self.jd = self.cal2jul(self.year.get(),
                                                 self.month.get(),
                                                 self.day.get(),
                                                 self.hour.get(),
                                                 self.minute.get())
        self.time_lbl.config(text = "{0}:{1}     JD {2}".format(
                                    str(self.hour.get()).zfill(2),
                                    str(self.minute.get()).zfill(2),
                                    self.jd))                   
        self.check_and_reconfigure()

Le variable classes riproducono i tipi fondamentali di variabili elementari di Python: boolean, string, integer, float. I tipi corrispondenti sono BooleanVar, StringVar, IntVar e DoubleVar. Si creano come istanze, assegnandole ad una nuova variabile, e ricevono un valore dato dal metodo .set della variable class. Per leggere il contenuto si usa invece il metodo .get(). L'assegnazione al widget comporta l'iscrizione della nuova variabile tra i parametri del widget stesso, attraverso l'istruzione 'textvariable', nelle modalità che abbiamo già incontrato:

label = Label(self.master, textvariable = <class variable>)
oppure con il metodo config: label.config(textvariable = <class variable>
oppure ancora con la selezione diretta della key: label['textvariable'] = <class variable> 

Il comportamento dinamico di queste variabili è fornito dal metodo .trace: <class variable>.trace('w', <funzione di callback> quando la variabile viene modificata, per esempio agendo sulle frecce delle spinbox, oppure inserendo direttamente il valore in un widget di tipo entry. Esiste anche l'attivazione del callback qualora si legga il contenuto della variabile, in tal caso il trace si fa con il parametro 'r' anziche 'w'.

Nel codice sopra riportato la funzione di callback è self.check_and_reconfigure, che vediamo immediatamente:

    def check_and_reconfigure(self, *args):
        year = self.year.get()
        month = self.month.get()
        day = self.day.get()
        hour = self.hour.get()
        minute = self.minute.get()
        self.act_time=""
        # recalculate month's length in days
        if month in (1,3,5,7,8,10,12):
            self.max_days = 31
        elif month in (4,6,9,11):
            self.max_days = 30
        elif month == 2:
            if year < 1582:
                if year%4 == 0:
                    self.max_days = 29
                else:
                    max_day = 28
            else:
                if (year%4 == 0) and (year%400 == 0):
                    self.max_days = 29
                else:
                    self.max_days = 28
        self.month_length = self.max_days
        # fill days' buttons
        for i in self.table:
            i.config(text = '')
            i.config(bg   = 'grey')
            i.config(state = DISABLED)
        for day in range (1, self.month_length+1):
            dotw = self.day_of_the_week(self.year.get(), self.month.get(), day)
            posx = dotw
            if day == 1:
                lag = posx
            posy = int((day+lag-1)/7)
            self.table[posx + posy * 7].config(text = str(day))
            self.table[posx + posy * 7].config(state = NORMAL)
            if day == self.day.get():
                self.table[posx + posy * 7].config(bg='white')
                self.act_time = "{0}:{1}     JD {2}".format(
                        str(self.hour.get()).zfill(2), str(self.minute.get()).zfill(2),
                        self.cal2jul(self.year.get(), self.month.get(), self.day.get(),
                        self.hour.get(), self.minute.get()))
        self.time_lbl['text']=self.act_time

Nell'ordine vengono eseguite le seguenti operazioni:

  1. Si leggono, nell'ordine, i valori della variabile self.dtime riferita all'anno, al mese, all'ora e al minuto
  2. con un semplice algoritmo si determina la lunghezza, in giorni, del mese selezionato, sia per le date gregoriane che per quelle più antiche (qui ci sarà un problema, il calendario gregoriano non è stato adottato da tutti i paesi nel 1582, bisognerà tenerne conto)
  3. si ripulisce la table dei giorni, disabilitando, temporaneamente, la possibilità di cliccare sui button
  4. con un altro semplice algoritmo si calcola, per i giorni da 1 alla lunghezza del mese, la posizione del giorno nella table in riga e colonna e si ripristina la possibilità di interazione per il mouse per i soli button selezionati
  5. si crea la stringa che compare in fondo al widget con l'ora e minuto e la data giuliana

Rimane la parte finale del codice, che segue immediatamente:

    def change_day(self, button):
        self.day.set(button.cget('text'))
        
        
    def cal2jul(self, year, month, day,
                hour=0, minute =0):
        month2 = month
        year2 = year
        if month2 <= 2:
            year2 -= 1
            month2 += 12
        else:
            pass
        if (year*10000 + month*100 + day) > 15821015:
            a = int(year2/100)
            b = 2 - a + int(a/4)
        else:
            a = 0
            b = 0
        if year < 0:
            c = int((365.25 * year2)-0.75)
        else:
            c = int(365.25 * year2)
        d = int(30.6001 *(month2 + 1))
        return b + c + d + day + hour / 24.0 + minute / 1440.0 + 1720994.5

   
    def day_of_the_week(self, year, month, day):
                
        jd = self.cal2jul(year, month, day)
        a = (jd+1.5)/7
        f = int((a % 1)*7 +.5)
        return f        

Chiudiamo con alcune funzioni helper:

self.change_day(self) banalmente effettua l'aggiornamento della variabile self.day, abbiamo visto come la funzione lambda consenta di aggiornare il valore della variabile associata ad uno specifico button

cal2jul è una della funzioni che abbiamo usato nel calcolo di posizione dei pianeti. Ho preferito integrarla anzichè richiamarla da un modulo esterno per dare indipendenza al nuovo widget

infine day_of_the_week serve per dare un valore numerico ad ogni giorno del mese, è una funzione che utilizza la data giuliana.

Con questo ho finito, appuntamento ad un prossimo post

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...