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.