pythontkintertkcalendar

Restrict date selection from calendar to specific dates


I'd like to let the user pick one out of a list of dates from my calendar but prevent her from selecting any other date.

Based on this answer I managed to restrict the date range that can be selected but I couldn't exclude days in between.

import datetime as dt
import tkinter as tk
from tkinter import ttk
from tkcalendar import DateEntry

# fmt: off
dates = [
    "2024-04-08", "2024-04-10", "2024-04-11", "2024-04-12",
    "2024-04-15", "2024-04-16", "2024-04-17", "2024-04-18", "2024-04-19",
    "2024-04-22",
    "2024-05-21", "2024-05-22", "2024-05-23", "2024-05-24",
    "2024-05-27", "2024-05-28", "2024-05-29", "2024-05-30", "2024-05-31",
    "2024-06-03", "2024-06-04", "2024-06-05", "2024-06-07",
    "2024-06-10", "2024-06-11", "2024-06-12", "2024-06-13", "2024-06-14",
]
# fmt: on

root = tk.Tk()

date_entry_var = tk.StringVar()
date_entry = DateEntry(
    root,
    textvariable=date_entry_var,
    date_pattern="yyyy-mm-dd",
    mindate=dt.date.fromisoformat(dates[0]),
    maxdate=dt.date.fromisoformat(dates[-1]),
)
date_entry.pack()

root.mainloop()

Is there a way that makes it possible to select from a list of dates with a calendar widget?

tk is required because I'm working with tk in the entire app but answer do not need to use the tkcalendar module if there are alternatives.


Solution

  • I got original code for Calendar and I added variable allowed_dates which gets list of objects datetime and it uses it to disable other dates which are not on this list (in range allowed_dates[0], allowed_dates[-1].

    I tried to do similar code as for mindate,maxdate but it still may need changes in some functions

    Main part is

    # --- display
    def _display_calendar(self):
    
        # ... code ...
    
        allowed_dates = self['allowed_dates']
    
        if allowed_dates is not None:
    
            import datetime as dt
    
            one_day = dt.timedelta(days=1)
    
            date = allowed_dates[0]
            end_date = allowed_dates[-1]
    
            while date <= end_date:
                if date not in allowed_dates:
                    mi, mj = self._get_day_coords(date)
                    if mi is not None:
                        print(date, mi, mj, '!disabled')
                        self._calendar[mi][mj].state(['disabled'])
                date += one_day
    

    And this is how I use it - it needs to convert list of strings to list of objects datetime

    I also added buttons which add/remove allowed dates to Calendar or DateEntry (based on variable widget) and it also change mindate,maxdate.

    Strange is when I added 2024-06-06 to Calendar then it also added it to DateEntry but it could't remove it. It also did't add date in DateEntry when it is not in range mindate, maxdate.

    import datetime as dt
    import tkinter as tk
    #from tkinter import ttk
    #from tkcalendar import DateEntry
    
    from mycalendar import Calendar
    from mycalendar import DateEntry
    
    # fmt: off
    dates = [
        "2024-04-08", "2024-04-10", "2024-04-11", "2024-04-12",
        "2024-04-15", "2024-04-16", "2024-04-17", "2024-04-18", "2024-04-19",
        "2024-04-22", 
    
        "2024-05-21", "2024-05-22", "2024-05-23", "2024-05-24",
        "2024-05-27", "2024-05-28", "2024-05-29", "2024-05-30", "2024-05-31",
    
        "2024-06-03", "2024-06-04", "2024-06-05", "2024-06-07",
        "2024-06-10", "2024-06-11", "2024-06-12", "2024-06-13", "2024-06-14",
    ]
    # fmt: on
    
    root = tk.Tk()
    
    dt_dates = [ dt.date.fromisoformat(date) for date in dates ]
    
    # example mycalendar.Calendar
    tk.Label(root, text="Calendar").pack()
    cal = Calendar(
        root,
        date_pattern="yyyy-mm-dd",
        mindate=dt_dates[0],
        maxdate=dt_dates[-1],
        allowed_dates=dt_dates,
        locale="en_GB.utf-8",  # to show it in English instead of my native Polish
                                # to make screenshot
    )
    cal.pack()
    
    date_entry_var = tk.StringVar()
    
    # example mycalendar.DateEntry
    tk.Label(root, text="DateEntry").pack()
    date_entry = DateEntry(
        root,
        textvariable=date_entry_var,
        date_pattern="yyyy-mm-dd",
        mindate=dt_dates[0],
        maxdate=dt_dates[-1],
        allowed_dates=dt_dates,
        locale="en_GB.utf-8",  # to show it in English instead of my native Polish
                                # to make screenshot
    )
    date_entry.pack()
    
    # --- test buttons ---
    
    #widget = cal
    widget = date_entry
    
    # ---
    
    def show_allowed_dates():
        for date in widget['allowed_dates']:  # not `cal.allowed_dates`
            print('allowed:', date)
    
    button_show = tk.Button(root, text="Show Allowed Dates", command=show_allowed_dates)
    button_show.pack(fill='x')
    
    # ---
    
    def add_allowed_date(date):
        dt_date = dt.date.fromisoformat(date)
    
        if dt_date not in widget['allowed_dates']:
            print('add allowed:', date)
    
            widget['allowed_dates'].append(dt_date)
            # other methods
            #widget['allowed_dates'] += [append(dt.date.fromisoformat(date)]
            #widget['allowed_dates'].extend( [append(dt.date.fromisoformat(date)] )
    
            widget['allowed_dates'] = sorted(widget['allowed_dates'])
    
            # what if new date is not in `mindate`, `maxdate` ???
            if widget['allowed_dates'][0] < widget['mindate']:
                widget['mindate'] = widget['allowed_dates'][0]
    
            if widget['allowed_dates'][-1] > widget['maxdate']:
                widget['maxdate'] = widget['allowed_dates'][-1]
    
            # redraw it
            if widget == cal:
                widget._display_calendar()
    
    
    for date in ('2024-06-06', '2024-06-26', '2024-07-10'):
        button_add = tk.Button(root, text=f"Add Allowed Date: {date}", command=lambda x=date:add_allowed_date(x))
        button_add.pack(fill='x')
    
    # ---
    
    def remove_allowed_date(date):
        dt_date = dt.date.fromisoformat(date)
    
        if dt_date in widget['allowed_dates']:
            print('remove allowed:', date)
    
            widget['allowed_dates'].remove(dt_date)
    
            # what if removed date is `mindate` or  `maxdate` ???
            if widget['mindate'] < widget['allowed_dates'][0]:
                widget['mindate'] = widget['allowed_dates'][0]
    
            if  widget['maxdate'] > widget['allowed_dates'][-1]:
                widget['maxdate'] = widget['allowed_dates'][-1]
    
            # redraw it
            if widget == cal:
                widget._display_calendar()
    
    for date in ('2024-06-06', '2024-06-26', '2024-07-10'):
        button_remove = tk.Button(root, text=f"Remove Allowed Date: {date}", command=lambda x=date:remove_allowed_date(x))
        button_remove.pack(fill='x')
    
    # ---
    
    root.mainloop()
    

    example window with buttons

    If I put original DateEntry without from tkcalendar.calendar_ import Calendar in the same file mycalendar.pythen it uses my calendar.
    And it needs only to use mycalendar.DateEntry instead of original tkCalendar.DateEntry with allowed_dates


    Full code for Calendar is too long to show it here so I put it in GitHub

    python-examples/tkinter / tkcalendar - add allowed_dates