@daniilS, I'm working on a themed date chooser which has 3-components....
- a
DateChooser
class that creates the TopLevel
window.
- an
ask_date
function that opens the DateChooser
and returns a datetime
object.
- a
DateEntry
class, that works like an entry field with a date chooser button attached.
I expect all of these to be used independently based on the needs of the situation.
A few more things I still need to work out...
- When using the
DateEntry
, the date chooser should use the date in the entry field as the default, so that if the button is invoked again, the calendar in the popup reflects the new date, not the current date as it does now.
- I need to implement the themes for the various components so that they are changeable. Currently they are fixed to the primary theme color, and not inside of the class. Possibly I can subclass the ttk style and have something unique to the calendar widget, but I'll think about it.
Below is the proto-type code for this widget. It's not clean by any stretch, but it works.
import calendar
from datetime import datetime
from tkinter import IntVar, Toplevel, StringVar
from tkinter import ttk
from tkinter.ttk import Frame, Entry
from ttkbootstrap import Style
def ask_date(parent):
outvar = StringVar()
dp = DateChooserPopup(parent, outvar)
if outvar.get():
return datetime.strptime(outvar.get(), '%Y-%m-%d')
class DateEntry(Frame):
def __init__(self, parent=None, dateformat='%Y-%m-%d', **kw):
super().__init__(master=parent, **kw)
self.parent = parent
self.dateformat = dateformat
self.entry = Entry(self)
self.entry.pack(side='left', fill='x', expand='yes')
self.button = ttk.Button(self, text='📅', command=self.on_date_select)
self.button.pack(side='left')
# insert default value
self.entry.insert('end', datetime.today().strftime(dateformat))
def on_date_select(self):
date = ask_date(self.entry)
self.entry.delete('0', 'end')
self.entry.insert('end', date.strftime(self.dateformat))
class DateChooserPopup(Toplevel):
def __init__(self, parent, variable=None, **kw):
super().__init__()
self.withdraw()
self.transient(parent)
self.parent = parent
self.update_idletasks() # actualize geometry
x = parent.winfo_rootx() + parent.winfo_width()
y = parent.winfo_rooty() + parent.winfo_height()
self.overrideredirect(True)
self.resizable(False, False)
self.today = datetime.today()
self.date = datetime.today()
self.calendar = calendar.Calendar()
self.cframe = ttk.Frame(self, padding=10, borderwidth=1, relief='raised')
self.tframe = ttk.Frame(self.cframe)
self.dframe = None
self.titlevar = StringVar(value=f'{self.date.strftime("%B %Y")}')
self.datevar = IntVar()
self.variable = variable or StringVar()
self.setup()
self.geometry(f'+{x}+{y}')
self.wait_window()
def next_month(self):
year, month = calendar._nextmonth(self.date.year, self.date.month)
self.date = datetime(year=year, month=month, day=1)
self.dframe.destroy()
self.draw_calendar()
def prev_month(self):
year, month = calendar._prevmonth(self.date.year, self.date.month)
self.date = datetime(year=year, month=month, day=1)
self.dframe.destroy()
self.draw_calendar()
def setup(self):
"""Setup the calendar widget"""
parent = self.parent
self.cframe.pack(fill='both')
self.tframe.pack(fill='x')
self.calendar.setfirstweekday(calendar.SUNDAY)
self.draw_titlebar()
self.draw_calendar()
self.deiconify()
self.focus_set()
def date_select(self, index):
row, col = index
date = self.monthdates[row][col].strftime('%Y-%m-%d')
self.variable.set(date)
self.after(10, self.destroy)
def draw_titlebar(self):
"""Create the title bar"""
# previous month button
self.btn_prev = ttk.Button(self.tframe, text='«', style='primary.Link.TButton', command=self.prev_month)
self.btn_prev.pack(side='left')
# month and year title
self.title_label = ttk.Label(self.tframe, textvariable=self.titlevar, anchor='center')
self.title_label.pack(side='left', fill='x', expand='yes')
# next month button
self.btn_next = ttk.Button(self.tframe, text='»', style='primary.Link.TButton', command=self.next_month)
self.btn_next.pack(side='left')
def draw_calendar(self):
self.titlevar.set(f'{self.date.strftime("%B %Y")}')
self.monthdays = self.calendar.monthdayscalendar(self.date.year, self.date.month)
self.monthdates = self.calendar.monthdatescalendar(self.date.year, self.date.month)
self.dframe = ttk.Frame(self.cframe)
self.dframe.pack(fill='both', expand='yes')
# days of the week header
for i, wd in enumerate(['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']):
wd_lbl = ttk.Label(self.dframe, text=wd, anchor='center', padding=(0, 5, 0, 10), width=4)
wd_lbl.grid(row=0, column=i, sticky='nswe')
# calendar days
for row, wk in enumerate(self.monthdays):
self.dframe.rowconfigure(row, weight=1)
for col, day in enumerate(wk):
self.dframe.columnconfigure(col, weight=1)
if day == 0:
ttk.Label(self.dframe, text=self.monthdates[row][col].day, anchor='center',
style='secondary.TLabel',
padding=(0, 0, 0, 10)).grid(row=row + 1, column=col, sticky='nswe')
else:
if all([
day == self.today.day,
self.date.month == self.today.month,
self.date.year == self.today.year]):
day_style = 'success.Toolbutton'
else:
day_style = 'calendar.primary.Outline.Toolbutton'
rb = ttk.Radiobutton(self.dframe, variable=self.datevar, value=day, text=day, style=day_style,
padding=(0, 0, 0, 10), command=lambda x=row, y=col: self.date_select([x, y]))
rb.grid(row=row + 1, column=col, sticky='nswe')
def define_style(self):
pass
if __name__ == '__main__':
# TODO setup the styling in the __init__ file, and setup the class so that it can be easilily modified.
# TODO add documentation to all classes and methods.
# TODO make sure the date chooser defaults to the entry field value instead of the current date.
style = Style()
style.configure('calendar.primary.Outline.Toolbutton', lightcolor='#fff', darkcolor='#fff', bordercolor='#fff')
style.map('calendar.primary.Outline.Toolbutton',
darkcolor=[
('disabled', '#fff'),
('pressed', '!disabled', '#273747'),
('selected', '!disabled', '#273747'),
('hover', '!disabled', '#2c3e50')],
bordercolor=[
('disabled', '#fff'),
('pressed', '!disabled', '#273747'),
('selected', '!disabled', '#273747'),
('hover', '!disabled', '#2c3e50')],
lightcolor=[
('disabled', '#fff'),
('pressed', '!disabled', '#273747'),
('selected', '!disabled', '#273747'),
('hover', '!disabled', '#2c3e50')])
root = style.master
picker = DateEntry(root, dateformat='%B %d, %Y', padding=20)
picker.pack()
root.mainloop()
enhancement