Saturday, February 28, 2015

Widget Calendario con Tkinter - Tkinter Calendar Widget

Ho abbandonato Tkinter per quasi un anno, è venuto il momento di riprenderne l'uso e di scrivere qualche applicazione utile per il futuro utilizzo nel software astrologico autoprodotto.

Se curiosate tra i widget di Tkinter, anche nella ricca collezione dell'estensione ttk, non trovate un widget calendario. Qualcosa si trova sul web, ma ho pensato che, a questo punto, mi conviene scrivermene uno da solo.

In github trovate il codice di quello fatto da me, è un vero calendario perpetuo, tarato sull'intervallo di anni -3000 : 3000, utile per i nostri calcoli astrologici, facilmente incorporabile all'interno di altri programmi grafici in Python. Di seguito allego un'immagine del widget:

Per spiegarne l'utilizzo dividero' il sorgente in alcuni blocchi che commentero' brevemente. Nei post del 2014 trovate molte utili spiegazioni ulteriori, che possono aiutare a capire meglio come si lavora graficamente con il linguaggio python.

Iniziamo con le prime e le ultime righe di codice:

#/usr/bin/env python
# -*- coding: utf-8 -*-
from Tkinter import *


class tkCalendar:
    import datetime

.............................

if __name__ == '__main__':

    root = Tk()
    cal  = tkCalendar(root)

    root.mainloop()

Iniziamo importando la libreria Tkinter. In fondo al modulo, per testare il widget, avviamo un'istanza della classe tkCalendar e lanciamo il metodo mainloop sull'istanza, che serve a mettere il widget in ciclo continuo, in attesa di eventi.

Subito sotto la dichiarazione di classe, iniziamo a costruire i widget che compongono il calendario.

    def __init__(self, master):
        "this section initializes the graphic objects"
        # main window
        self.Calendar = master
        # top frame (date)
        self.date_labels = Frame(self.Calendar)
        self.date_labels.pack(fill = BOTH, expand = 1)
        for i in ('Year', 'Month', 'Hour', 'Minute'):
            label = Label(self.date_labels, text=i, bg = 'tomato')
            label.pack(side = LEFT, fill = X, expand = 1) 
        
        self.dtime = self.datetime.datetime.now()
        self.date = Frame(self.Calendar)
        self.date.pack(side = 'top', fill = X, expand = 1)

Come abbiamo visto nei post dedicati a Tkinter, la creazione dei widget è abbastanza semplice: si definiscono delle variabili, cui si associa la creazione degli oggetti grafici secondo lo schema:

<variabile> = <Widget>(self.<Nome dell'istanza>, <parametro1>=<valore1>, <parametro2>=<valore2>, ...) 

Dopo la dichiarazione di classe e l'importazione delle librerie che servono, si apre il costruttore con

     def __init__(self, master)

master è un termine convenzionale (potete usare quello che preferite) quindi si crea una finestra contenitore, cui si dà un nome preceduto da self, in questo caso ho usato self.Calendar, che acquisisce il riferimento all'istanza dal parametro master.

Subito dopo creo un frame, cioè un contenitore o, se preferite, una cornice, in cui inserisco quattro label che contengono le etichette in chiaro dell'anno, del mese, dell'ora e del minuto. Per i giorni uso un altro sistema, che vediamo nel seguito. Da notare che perchè i widget siano visibili, devono chiamare il metodo pack() - o grid() o place(). integrati nel pack() ci sono i parametri fill e expand, che determinano il comportamento delle label in caso di modifica della grandezza della finestra generale. Se preferite modificare la direzione di allungamento o di espansione, o i colori che ho preimpostato, potete liberamente intervenire sul codice.

Le ultime due righe del codice sopra riportato creano un frame che conterrà dei widget particolari, chiamati Spinbox.

        # spinbox for year
        self.spin_year = Spinbox(self.date, from_ = -3000, to = 3000, width = 3)
        self.spin_year.pack(side = 'left', fill = X, expand = 1)
        self.spin_year.config(state = 'readonly')
        # spinbox for month
        self.spin_month = Spinbox(self.date,
                                  from_ = 1, to = 12, 
                                  width = 3)
        self.spin_month.pack(side = 'left', fill = X, expand = 1)
        self.spin_month.config(state = 'readonly')# spinbox for hour
        self.spin_hour = Spinbox(self.date,
                                 from_ = 0, to = 23,
                                 width = 3)
        self.spin_hour.pack(side = LEFT, fill = X, expand = 1)
        self.spin_hour.config(state = 'readonly')
        # spinbox for minute
        self.spin_minute = Spinbox(self.date,
                                 from_ = 0, to = 59,
                                 width = 3)
        self.spin_minute.pack(side = LEFT, fill = X, expand = 1)
        self.spin_minute.config(state = 'readonly')

Gli spinbox sono widget molto comodi per selezionare un range ben definito di valori, in questo caso per l'anno fra 3000 e 3000, il mese tra 1 e 12, l'ora tra 0 e 23, il minuto tra 0 e 59. La scelta si fa con le frecce in alto e in basso. Questo puo' risultare scomodo per la selezione di anni estremi nell'intervallo, ma ho preferito disabilitare l'accesso diretto con tastiera per evitare errori grossolani nella gestione del dato.

Arriviamo alla gestione del giorno: i normali calendari di windows o linux consentono la selezione del giorno direttamente sul calendario, con un click del mouse. Ho cercato di realizzare questo comportamento, riproducendo una tabella di valori con i giorni del mese. Il codice seguente, dopo avere creato un frame contenente le labels per i sette giorni della settimana, partendo dalla domenica, realizza appunto una table in cui per ogni giorno del mese, in corripondenza della label indicante il giorno della settimana, si puo' selezionare uno specifico giorno con il click sinistro del mouse.

        # frame for days of the week labels
        self.dotw_labels= Frame(self.Calendar)
        self.dotw_labels.pack(fill = BOTH, expand = 1)
        for i in ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'):
            label = Label(self.dotw_labels, text=i, bg = 'light green',
                          relief = GROOVE)
            label.pack(side = LEFT, fill = BOTH, expand = 1) 
        # table (list of buttons) for days
        self.table = []
        for row in range(0,6):
            # here 6 horizontal frames are created
            # each one receives a block of 7 buttons
            self.row = Frame(self.Calendar)
            self.row.pack(side = TOP, fill = BOTH, expand = 1)
            for col in range(0,7):
                button = Button(self.row,
                                width = 2,
                                bg = 'grey',
                                text = ''
                                )
                # just a little trick: because of lambda usage the callback function
                # receives an updated parameter, referred to the specific button
                # widget, not the one available during
                # initialization
                button.config(command = lambda button = button: self.change_day(button))
                button.pack(side = LEFT, fill = BOTH, expand = 1)
                self.table.append(button)
        self.time_lbl = Label(self.Calendar, text='')
        self.time_lbl.pack()

Per realizzare la tabella dei giorni ho nuovamente usato il trucco che avevo già sperimentato in precedenza: la variabile self.table è una lista, cui associo 6 frame in verticale, abbastanza per contenere i 31 giorni del mese più lungo quando il mese inizia per sabato. Ogni frame viene riempito con sette button, allineati in modo tale da corrispondere alle etichette dei giorni della settimana.

Faccio notare solo il trucchetto usato per recuperare i 'quanti' del giorno direttamente dal testo del button: l'uso della funzione inline lambda consente di lanciare una funzione di callback (self.change_day) con il valore attuale del button selezionato, anzichè quello (vuoto) disponibile al momento della creazione del widget stesso. Per i dettagli sull'uso di queste funzioni potete consultare l'eccellente volume di Mark Lutz "Programming in Python - 4th edition" che dedica ampi capitoli a Tkinter, oppure scrivetemi o inserite un commento a questo post.

Siccome il post è diventato lunghissimo, continuo il commento al codice nel post successivo.

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