My date picker class code:
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)))
As you can see I’ve created child window in function __init__() as an instance of class tk.Toplevel. Then I blocked widgets in the parent window until date picker window closes (grab_set() function). The next lines describe the position of date picker window relative to the parent window.
self.c = calendar
self.cal = self.c.Calendar(self.c.firstweekday())
self.dp_frame = None
self.create_cal_ui()
Than I’ve made an instance of Calendar class. Next, dp_frame variable which stores an instance of tk.Frame class was set up. Then the method create_cal_ui() arranges widgets in window.
create_cal_ui() method:
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
Method create_cal_ui() takes year and month as arguments – default: current year and current month. Then monthdayscalendar() function from Calendar class gets the list of days in month. Each week is a separate list, so you have a list of lists. Days not belonging to the current month are marked as 0 (zero). The month_txt represents the month string and self.month holds integer value of a month.
if self.dp_frame is not None:
self.dp_frame.destroy()
It checks if month is changed. If so, the current frame is destroyed.
Then I create prev_btn and next_btn buttons to change month and date_lbl label to show selected month. In order to change a month I had to bind a method to a button. Event argument does’t have a sender name information. So I’ve created lambda function to pass additional string argument to set_date() method.
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'))
Next I put week names abbreviations:
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)
Then the day buttons are shown. When pressed on a button get_date() method runs. I used partial function from functools module. Lambda function doesn’t work in this case due to late binding.
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
Code of set_date() method:
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)
Code of get_date() method:
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()
I’ve made datetime object instance with formatting. Then I’ve cleared date_entry and added new content. Finally when date is chosen the date picker is destroyed.