1

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?

Samay Gupta
  • 437
  • 3
  • 8
  • ***"I want it to drop down"***: Which part do you want to **dropdown**? ***"when I select the Date"***: [`bind(''`](http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm) to `selected_date = tk.Label`. – stovfl Jan 25 '20 at 19:27
  • I want it to be such that when I click on the Date, It shows the whole date picker. And again when I select the date it should hide the date picker and update the Label. – Samay Gupta Jan 25 '20 at 19:30
  • 1
    Read up on [`grid_forget()`](http://effbot.org/tkinterbook/grid.htm#Tkinter.Grid.grid_forget-method) – stovfl Jan 25 '20 at 19:34
  • Okay I looked it up. Its pretty useful. Only issue is now when I use it, It moves all the widgets below it below the frame. I want the calendar to sort of overlay on the below widgets. I've updated the code using grid_forget – Samay Gupta Jan 25 '20 at 19:40
  • ***"It moves all the widgets below it "***: Verified your example and it works OK. [Edit] your question and show how do you do `.bind(...` and where do you call `toggle(False)` – stovfl Jan 25 '20 at 19:55
  • Can you explain a bit more more by what you mean by how do I do bind? Ive edited to add a reference to the position of the toggles – Samay Gupta Jan 25 '20 at 20:00
  • Now i see your problem, you call `update(...` multiple times, this leads to stacking widgets. You have layout your widgets **only once** and you have to only change the values inside `def update(...`. ***how do I do bind?***: E.g. `selected_date.bind('', self.__toggle, True)` – stovfl Jan 25 '20 at 20:06
  • Yea. The issue comes with widgets outside of the Calendar widget rn. Like how all the number labels below the custom widget move to accommodate the extended Calendar, which I don't want. – Samay Gupta Jan 25 '20 at 20:10
  • Does this answer your question? [How do I create a date picker in tkinter?](https://stackoverflow.com/questions/4443786/how-do-i-create-a-date-picker-in-tkinter) – SF12 Study Jan 26 '20 at 04:58

1 Answers1

0

When I had a similar problem with the dropdown pushing all the widgets down instead of overlapping them, I also tried using place() since it's what everyone was saying to do if you want to overlap widgets. However using place() for anything complex is a whole new set of problems. I solved this problem by using a Toplevel window for the dropdown, same as you would for a tooltip. I created my own full-featured combobox this way, since I wanted features that I wasn't able to add to ttk.Combobox.

Luther
  • 514
  • 4
  • 17