Date picker for tkinter

date picker

It is a utility class for the project described in this post. You can use it freely in your project (MIT license). Project files you can find here.

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

Leave a Reply

Your email address will not be published. Required fields are marked *