I'm trying to create a simple custom calendar widget in python using tkinter. I want to create this as a drop down when selected. I have created the base of the entire code. I was wondering if there was a way to create my custom calendar as a drop down similar to an option menu. I want it to drop down when I select the Date and close automatically when I update the date.
Note: I know it will be easier to use something like tkcalendar but I don't like that interface and would prefer to create my own.
Edit: I'm now using grid forget to hide/show the calendar. Only issue is it moves all the widgets below it down. I want it to sort of overlay over the below widgets.
CODE:
import tkinter as tk
import datetime
from calendar import monthrange
class CalendarDatePicker:
def __init__(self, frame, font=("Times New Roman", 12), bg="white", fg="black", bg_selected="light gray", fg_selected="black"):
self.__font = font
self.__bg = bg
self.__fg = fg
self.__bg_selected = bg_selected
self.__fg_selected = fg_selected
self.__frame = tk.Frame(frame, bg=self.__bg)
self.__selected_date = None
self.__top_frame = tk.Frame(self.__frame, bg=self.__bg)
self.__date_frame = tk.Frame(self.__frame, bg=self.__bg)
self.__dates = {}
today = datetime.datetime.today()
self.__dates["year"] = int(today.year)
self.__dates["month"] = today.strftime("%B")
self.__dates["date"] = today.day
self.__dates["selected"] = today
self.__update()
def __toggle(self, show=False):
if show:
self.__top_frame.grid(row=1, column=0, pady=5, padx=2)
self.__date_frame.grid(row=2, column=0, pady=5, padx=2)
else:
self.__top_frame.grid_remove()
self.__date_frame.grid_remove()
self.__frame.update()
def __shift_year(self, shift):
self.__dates["year"] = int(self.__dates["year"]) + shift
self.__CurYear.destroy()
self.__update()
def __shift_month(self, shift):
mn = int(datetime.datetime.strptime(self.__dates["month"], "%B").month) + shift
if mn == 0:
mn = 12
self.__dates["year"] = int(self.__dates["year"])-1
self.__CurYear.destroy()
elif mn == 13:
mn = 1
self.__dates["year"] = int(self.__dates["year"])+1
self.__CurYear.destroy()
self.__dates["month"] = datetime.datetime(2000, mn, 1).strftime("%B")
self.__CurMonth.destroy()
self.__update()
def __set_date(self, date):
if date.strip() != '':
self.__dates["date"] = date
self.__dates["selected"] = datetime.datetime(int(self.__dates["year"]),int(datetime.datetime.strptime(self.__dates["month"], "%B").month),int(self.__dates["date"]))
self.__toggle(False)
self.__update()
def get(self, date_format="%Y-%m-%d"):
return self.__dates["selected"].strftime(date_format)
def create(self):
return self.__frame
def __update(self):
if self.__selected_date is not None:
self.__selected_date.destroy()
self.__selected_date = tk.Label(self.__frame, width=17, bg=self.__bg, fg=self.__fg, font=("Times New Roman", 16, "bold"), text=self.get("%d %B, %Y"), relief='solid', bd=1)
self.__selected_date.bind('<Button-1>', lambda event, show=True: self.__toggle(show))
self.__selected_date.grid(row=0, column=0)
today = self.__dates["selected"]
self.__YearShiftPrev = tk.Label(self.__top_frame, fg=self.__fg, text="<", relief="solid", bd=1, bg=self.__bg)
self.__YearShiftPrev.bind('<Button-1>', lambda event, x=-1: self.__shift_year(x))
self.__YearShiftPrev.grid(row=0, column=1, padx=2)
self.__CurYear = tk.Label(self.__top_frame, fg=self.__fg, width=6, text=self.__dates["year"], bg=self.__bg)
self.__CurYear.grid(row=0, column=2, padx=2)
self.__YearShiftNext = tk.Label(self.__top_frame, fg=self.__fg, text=">", relief="solid", bd=1, bg=self.__bg)
self.__YearShiftNext.bind('<Button-1>', lambda event, x=1: self.__shift_year(x))
self.__YearShiftNext.grid(row=0, column=3, padx=2)
self.__MonthShiftPrev = tk.Label(self.__top_frame, fg=self.__fg, text="<", relief="solid", bd=1, bg=self.__bg)
self.__MonthShiftPrev.bind('<Button-1>', lambda event, x=-1: self.__shift_month(x))
self.__MonthShiftPrev.grid(row=0, column=5, padx=2)
self.__CurMonth = tk.Label(self.__top_frame, fg=self.__fg, width=10, text=self.__dates["month"], bg=self.__bg)
self.__CurMonth.grid(row=0, column=6, padx=2)
self.__MonthShiftNext = tk.Label(self.__top_frame, fg=self.__fg, text=">", relief="solid", bd=1, bg=self.__bg)
self.__MonthShiftNext.bind('<Button-1>', lambda event, x=1: self.__shift_month(x))
self.__MonthShiftNext.grid(row=0, column=7, padx=2)
day, total_days = monthrange(int(self.__dates["year"]), int(datetime.datetime.strptime(self.__dates["month"], "%B").month))
for ind, text in enumerate(["M", "T", "W", "T", "F", "S", "S"]):
lbl = tk.Label(self.__date_frame, fg=self.__fg, text=text, width=3, bg=self.__bg, relief="solid", bd=1)
lbl.grid(row=0, column=ind%7, pady=1, padx=1)
date = 1
cur_month = today.year == self.__dates["year"] and today.strftime("%B") == self.__dates["month"]
for i in range(42):
row, col = 1+i//7, i%7
if col == day and date <= total_days:
day = (day+1)%7
text = str(date)
date += 1
else:
text = ' '
bg, fg = (self.__bg_selected, self.__fg_selected) if (text==str(self.__dates["date"]) and cur_month) else (self.__bg, self.__fg)
lbl = tk.Label(self.__date_frame, width=3, text=text, bg=bg, fg=fg, relief="solid", bd=1)
lbl.bind("<Button-1>", lambda event, date=text: self.__set_date(date))
lbl.grid(row=row, column=col, pady=1, padx=1)
self.__frame.update()
window = tk.Tk()
calendar = CalendarDatePicker(window, bg="black", fg="white", bg_selected="white", fg_selected="black")
calendar.create().pack()
for i in range(10):
tk.Label(window, text=i).pack()
Changes:
def __toggle(self, show=False):
if show:
self.__top_frame.grid(row=1, column=0, pady=5, padx=2)
self.__date_frame.grid(row=2, column=0, pady=5, padx=2)
else:
self.__top_frame.grid_remove()
self.__date_frame.grid_remove()
self.__frame.update()
self.__toggle(False)
is called at the end of __set_date
function.
self.__toggle(True)
id called on the first label under the __update
function. I've binded it to call the function there.
What I've basically done is added a method to hide/show the calendar part. However now when I toggle it, All the labels (1-9) are moved below. I would like it to instead overlay the widgets. or more accurately cover them and uncover them on the toggle method.
I've looked this up a bit more and one solution I found was to use .place
instead of .grid
. However in the actual program I want to use this with the grid
manager. Is there any way to accomplish this without using .pack
?