DatePicker w bibliotece tkinter

date picker

Pliki projektu można pobrać tutaj.

Kod mojej klasy DatePicker wygląda następująco:

import tkinter as tk
import calendar
from datetime import datetime
from functools import partial


class DatePicker():
    def __init__(self, root, date_entry, date_strf):
        self.root = root
        self.date_entry = date_entry
        self.date_strf = date_strf
        self.top_level = tk.Toplevel(self.root)
        self.top_level.grab_set()
        self.top_level.title('Date picker')
        x = self.root.winfo_rootx()
        y = self.root.winfo_rooty()
        width = self.root.winfo_width()
        height = self.root.winfo_height()
        self.top_level.geometry(
            '+{}+{}'.format(x + int(width / 2), y + int(height / 2)))

W metodzie __init__() tworzone jest okno potomne jako instancja klasy tk.Toplevel. Nastepnie blokowana jest możliwość aktywowania elementów interfejsu okna rodzica do momentu zamknięcia okna date picker. W kolejnych wierszach definiuję ustawienie okna date picker względem okna rodzica.

        self.c = calendar
        self.cal = self.c.Calendar(self.c.firstweekday())
        self.dp_frame = None
        self.create_cal_ui()

Następnie tworzę egzemplarz klasy Calendar, deklaruję zmienną dp_frame, która będzie przechowywać instancję klasy tk.Frame, a następnie aktywuję metodę create_cal_ui(), która rozmieszcza widżety w utworzonym oknie.

Kod metody create_cal_ui() wygląda następująco:

    def create_cal_ui(self, year=datetime.today().year, month=datetime.today().month):

        mc = self.cal.monthdayscalendar(year, month)
        self.month = month
        self.year = year
        month_txt = self.c.month_name[month]
        self.date_str = f'{month_txt} {year}'

        if self.dp_frame is not None:
            self.dp_frame.destroy()

        self.dp_frame = tk. Frame(self.top_level)
        self.dp_frame.grid(column=0, row=0)

        self.prev_img = tk.PhotoImage(file='Resources/prev.gif')
        self.next_img = tk.PhotoImage(file='Resources/next.gif')
        prev_btn = tk.Button(self.dp_frame, image=self.prev_img, relief='flat')
        prev_btn.bind(
            '<Button-1>', lambda event: self.set_date(event, 'prev_btn'))
        prev_btn.grid(row=0, column=0)
        next_btn = tk.Button(self.dp_frame, image=self.next_img, relief='flat')
        next_btn.bind(
            '<Button-1>', lambda event: self.set_date(event, 'next_btn'))
        next_btn.grid(row=0, column=6)
        self.date_lbl = tk.Label(self.dp_frame, text=self.date_str,
                                 font=12)
        self.date_lbl.grid(row=0, column=1, columnspan=5, sticky='WE')

        week_names = self.c.day_abbr
        for i, name in enumerate(week_names):
            label = tk.Label(self.dp_frame, text=name).grid(column=i, row=1)

        col = 0
        row = 2
        for week in mc:
            for day in week:
                state = 'normal'
                if day == 0:
                    state = 'disabled'
                    day = ''
                day = str(day)
                button = tk.Button(self.dp_frame, text=day,
                                   relief='flat', state=state, command=partial(self.get_date, day))
                button.grid(column=col, row=row)
                col += 1
            row += 1
            col = 0

Metoda create_cal_ui() pobiera jako argumenty rok oraz miesiąc, dla którego ma być wyświetlony kalendarz. Domyślnie wyświetlany jest kalendarz dla bieżącego miesiąca i bieżącego roku. Na wstępie za pomocą metody monthdayscalendar() klasy Calendar pobierana jest lista dni w miesiącu – każdy tydzień jest opisywany jako osobna lista tzn. metoda zwraca listę list. Dni nie należące do wybranego miesiąca są oznaczane jako 0. W zmiennej month_txt przechowywana jest tekstowa nazwa miesiąca, którego wartość liczbową przechowuje zmienna self.month.

 if self.dp_frame is not None:
            self.dp_frame.destroy()

Powyższa instrukcja warunkowa sprawdza, czy nie dokonano wyboru kolejnego miesiąca. W takim przypadku bieżąca ramka przechowująca elementy dla wcześniej utworzonego miesiąca zostaje usunięta.

Następnie w ramce dp_frame tworzone są przyciski do zmiany miesiąca prev_btn, next_btn, etykieta wyświetlająca aktualnie wybrany miesiąc i rok date_lbl oraz przyciski reprezentujące poszczególne dni w miesiącu.

Zmiana miesiąca na poprzedni lub kolejny dokonywana jest dokonywana za pomocą dowiązania do przycisku – metoda bind. Zdarzenie (event) przesyłane do metody po aktywowaniu przycisku w tkinterze nie zawiera danych np. o nazwie wciśniętego przycisku. Aby zidentyfikować, jaki przycisk aktywował daną metodę musiałem utworzyć anonimową funkcję, która przesyła dodatkowy argument do metody obsługującej aktywowanie przycisku tzn.

prev_btn.bind(
            '<Button-1>', lambda event: self.set_date(event, 'prev_btn'))
next_btn.bind(
            '<Button-1>', lambda event: self.set_date(event, 'next_btn'))

Powyższy kod umożliwia uruchomienie metody set_date() po kliknięciu na odpowiedni przycisk, przy czym do metody set_date() jest przesyłany również argument typu String, identyfikujący który przycisk wywołał metodę.

Poniższy kod wyświetla nagłówek dni tygodnia w wyświetlanym kalendarzu. Nazwy dni wyświetlane są w formie skróconej:

    week_names = self.c.day_abbr
    for i, name in enumerate(week_names):
        label = tk.Label(self.dp_frame, text=name).grid(column=i, row=1)

col = 0
        row = 2
        for week in mc:
            for day in week:
                state = 'normal'
                if day == 0:
                    state = 'disabled'
                    day = ''
                day = str(day)
                button = tk.Button(self.dp_frame, text=day,
                                   relief='flat', state=state, command=partial(self.get_date, day))
                button.grid(column=col, row=row)
                col += 1
            row += 1
            col = 0

Powyższy kod wyświetla przyciski dni dla wybranego miesiąca. Jeśli tydzień zawiera dni z poprzedniego lub następnego miesiąca, to przyciski dla tych dni są dezaktywowane. Po naciśnięciu na wybrany przycisk uruchamiana jest metoda get_date() za pomocą funkcji partial modułu functools. Lambda w tym przypadku nie zadziała tak, jak można by oczekiwać z powodu tzn. late binding.

Kod metody set_date() uruchamiany po naciśnięciu przycisków: next_btn oraz prev_btn:

    def set_date(self, event, sender):
        if sender == 'prev_btn':
            self.month -= 1
            if self.month < 1:
                self.month = 12
                self.year -= 1
        if sender == 'next_btn':
            self.month += 1
            if self.month > 12:
                self.month = 1
                self.year += 1
        self.create_cal_ui(self.year, self.month)

Powyższa metoda jako argument sender pobiera nazwę przycisku, który ją aktywował. Niestety argument event nie udostępnia takiej informacji. Po kliknięciu następuje uaktualnienie widżetów poprzez wywołanie metody create_cal_ui().

Kod metody get_date() uruchamianej po wybraniu konkretnej daty z date pickera wygląda następująco:

def get_date(self, day):
    day = int(day)
    self.date_entry.delete(0, tk.END)
    d = datetime(self.year, self.month, day)
    self.date_entry.insert(0, d.strftime(self.date_strf))
    self.top_level.destroy()

W powyższej metodzie wybrany dzień – argument day jest rzutowany na obiekt int, dzięki czemu można utworzyć egzemplarz klasy datetime na podstawie wybranego dnia, miesiąca i roku. Następnie kasowane jest pole date_entry w oknie rodzica i wypełniane tekstem reprezentującym wybraną datę na podstawie wybranego formatowania. Po aktualizacji widżetu z okna rodzica, okno date pickera zostaje usunięte.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *