# encoding: utf-8
# Author: Miguel Martínez López
#
# Uncomment the next line to see my email
# print "Author's email: ", "61706c69636163696f6e616d656469646140676d61696c2e636f6d".decode("hex")
__doc__ = """
BINDINGS
========
Click button 1: Show calendar
Escape: Hide calendar and lost focus
PAGE UP: Move to the previous month.
PAGE DOWN: Move to the next month.
CTRL + PAGE UP: Move to the previous year.
CTRL + PAGE DOWN: Move to the next year.
CTRL/COMMAND + LEFT: Move to the previous day.
CTRL/COMMAND + RIGHT: Move to the next day.
CTRL/COMMAND + UP: Move to the previous week.
CTRL/COMMAND + DOWN: Move to the next week.
CTRL/COMMAND + END: Close the datepicker and erase the date.
CTRL/COMMAND + HOME: Move to the current month.
CTRL/COMMAND + SPACE: Show date on calendar
CTRL/COMMAND + DOWN: Move to the next week.
CTRL/COMMAND + END: Close the datepicker and erase the date.
CTRL/COMMAND + HOME: Move to the current month.
CTRL/COMMAND + SPACE: Show date on calendar
CTRL/COMMAND + Return: Set to entry current selection
"""
import calendar
import datetime
try:
import Tkinter
import tkFont
import ttk
from Tkconstants import CENTER, LEFT, N, E, W, S
from Tkinter import StringVar
except ImportError: # py3k
import tkinter as Tkinter
import tkinter.font as tkFont
import tkinter.ttk as ttk
from tkinter.constants import CENTER, LEFT, N, E, W, S
from tkinter import StringVar
def get_calendar(locale, fwday):
# instantiate proper calendar class
if locale is None:
return calendar.TextCalendar(fwday)
else:
return calendar.LocaleTextCalendar(fwday, locale)
class Calendar(ttk.Frame):
datetime = calendar.datetime.datetime
timedelta = calendar.datetime.timedelta
def __init__(self, master=None, year=None, month=None, firstweekday=calendar.MONDAY, locale=None, activebackground='#b1dcfb', activeforeground='black', selectbackground='#003eff', selectforeground='white', command=None, borderwidth=1, relief="solid", on_click_month_control=None):
"""
WIDGET OPTIONS
locale, firstweekday, year, month, selectbackground,
selectforeground, activebackground, activeforeground,
command=None, borderwidth, relief, on_click_month_control
"""
if year is None:
year = self.datetime.now().year
if month is None:
month = self.datetime.now().month
self._selected_date = None
self._sel_bg = selectbackground
self._sel_fg = selectforeground
self._act_bg = activebackground
self._act_fg = activeforeground
self.on_click_month_control = on_click_month_control
self._selection_is_visible = False
self._command = command
ttk.Frame.__init__(self, master, borderwidth=borderwidth, relief=relief)
self.bind("<FocusIn>", lambda event:self.event_generate('<<DatePickerFocusIn>>'))
self.bind("<FocusOut>", lambda event:self.event_generate('<<DatePickerFocusOut>>'))
self._cal = get_calendar(locale, firstweekday)
# custom ttk styles
style = ttk.Style()
style.layout('L.TButton', (
[('Button.focus', {'children': [('Button.leftarrow', None)]})]
))
style.layout('R.TButton', (
[('Button.focus', {'children': [('Button.rightarrow', None)]})]
))
self._font = tkFont.Font()
self._header_var = StringVar()
# header frame and its widgets
hframe = ttk.Frame(self)
lbtn = ttk.Button(hframe, style='L.TButton', command=self._on_press_left_button)
lbtn.pack(side=LEFT)
self._header = ttk.Label(hframe, width=15, anchor=CENTER, textvariable=self._header_var)
self._header.pack(side=LEFT, padx=12)
rbtn = ttk.Button(hframe, style='R.TButton', command=self._on_press_right_button)
rbtn.pack(side=LEFT)
hframe.grid(columnspan=7, pady=4)
self._day_labels = {}
days_of_the_week = self._cal.formatweekheader(3).split()
for i, day_of_the_week in enumerate(days_of_the_week):
Tkinter.Label(self, text=day_of_the_week, background='grey90').grid(row=1, column=i, sticky=N+E+W+S)
for i in range(6):
for j in range(7):
self._day_labels[i,j] = label = Tkinter.Label(self, background = "white")
label.grid(row=i+2, column=j, sticky=N+E+W+S)
label.bind("<Enter>", lambda event: event.widget.configure(background=self._act_bg, foreground=self._act_fg))
label.bind("<Leave>", lambda event: event.widget.configure(background="white"))
label.bind("<1>", self._pressed)
# adjust its columns width
font = tkFont.Font()
maxwidth = max(font.measure(text) for text in days_of_the_week)
for i in range(7):
self.grid_columnconfigure(i, minsize=maxwidth, weight=1)
self._year = None
self._month = None
# insert dates in the currently empty calendar
self._build_calendar(year, month)
def _build_calendar(self, year, month):
if not( self._year == year and self._month == month):
self._year = year
self._month = month
# update header text (Month, YEAR)
header = self._cal.formatmonthname(year, month, 0)
self._header_var.set(header.title())
# update calendar shown dates
cal = self._cal.monthdayscalendar(year, month)
for i in range(len(cal)):
week = cal[i]
fmt_week = [('%02d' % day) if day else '' for day in week]
for j, day_number in enumerate(fmt_week):
self._day_labels[i,j]["text"] = day_number
if len(cal) < 6:
for j in range(7):
self._day_labels[5,j]["text"] = ""
if self._selected_date is not None and self._selected_date.year == self._year and self._selected_date.month == self._month:
self._show_selection()
def _find_calendar_position(self, date):
first_weekday_of_the_month = (date.weekday() - date.day) % 7
return divmod((first_weekday_of_the_month - self._cal.firstweekday)%7 + date.day, 7)
def _show_selection(self):
"""Show a new selection."""
i,j = self._find_calendar_position(self._selected_date)
label = self._day_labels[i,j]
label.configure(background=self._sel_bg, foreground=self._sel_fg)
label.unbind("<Enter>")
label.unbind("<Leave>")
self._selection_is_visible = True
def _clear_selection(self):
"""Show a new selection."""
i,j = self._find_calendar_position(self._selected_date)
label = self._day_labels[i,j]
label.configure(background= "white", foreground="black")
label.bind("<Enter>", lambda event: event.widget.configure(background=self._act_bg, foreground=self._act_fg))
label.bind("<Leave>", lambda event: event.widget.configure(background="white"))
self._selection_is_visible = False
# Callback
def _pressed(self, evt):
"""Clicked somewhere in the calendar."""
text = evt.widget["text"]
if text == "":
return
day_number = int(text)
new_selected_date = datetime.datetime(self._year, self._month, day_number)
if self._selected_date != new_selected_date:
if self._selected_date is not None:
self._clear_selection()
self._selected_date = new_selected_date
self._show_selection()
if self._command:
self._command(self._selected_date)
def _on_press_left_button(self):
self.prev_month()
if self.on_click_month_control is not None:
self.on_click_month_control()
def _on_press_right_button(self):
self.next_month()
if self.on_click_month_control is not None:
self.on_click_month_control()
def select_prev_day(self):
"""Updated calendar to show the previous day."""
if self._selected_date is None:
self._selected_date = datetime.datetime(self._year, self._month, 1)
else:
self._clear_selection()
self._selected_date = self._selected_date - self.timedelta(days=1)
self._build_calendar(self._selected_date.year, self._selected_date.month) # reconstruct calendar
def select_next_day(self):
"""Update calendar to show the next day."""
if self._selected_date is None:
self._selected_date = datetime.datetime(self._year, self._month, 1)
else:
self._clear_selection()
self._selected_date = self._selected_date + self.timedelta(days=1)
self._build_calendar(self._selected_date.year, self._selected_date.month) # reconstruct calendar
def select_prev_week_day(self):
"""Updated calendar to show the previous week."""
if self._selected_date is None:
self._selected_date = datetime.datetime(self._year, self._month, 1)
else:
self._clear_selection()
self._selected_date = self._selected_date - self.timedelta(days=7)
self._build_calendar(self._selected_date.year, self._selected_date.month) # reconstruct calendar
def select_next_week_day(self):
"""Update calendar to show the next week."""
if self._selected_date is None:
self._selected_date = datetime.datetime(self._year, self._month, 1)
else:
self._clear_selection()
self._selected_date = self._selected_date + self.timedelta(days=7)
self._build_calendar(self._selected_date.year, self._selected_date.month) # reconstruct calendar
def select_current_date(self):
"""Update calendar to current date."""
if self._selection_is_visible: self._clear_selection()
self._selected_date = datetime.datetime.now()
self._build_calendar(self._selected_date.year, self._selected_date.month)
def prev_month(self):
"""Updated calendar to show the previous week."""
if self._selection_is_visible: self._clear_selection()
date = self.datetime(self._year, self._month, 1) - self.timedelta(days=1)
self._build_calendar(date.year, date.month) # reconstuct calendar
def next_month(self):
"""Update calendar to show the next month."""
if self._selection_is_visible: self._clear_selection()
date = self.datetime(self._year, self._month, 1) + \
self.timedelta(days=calendar.monthrange(self._year, self._month)[1] + 1)
self._build_calendar(date.year, date.month) # reconstuct calendar
def prev_year(self):
"""Updated calendar to show the previous year."""
if self._selection_is_visible: self._clear_selection()
self._build_calendar(self._year-1, self._month) # reconstruct calendar
def next_year(self):
"""Update calendar to show the next year."""
if self._selection_is_visible: self._clear_selection()
self._build_calendar(self._year+1, self._month) # reconstruct calendar
def get_selection(self):
"""Return a datetime representing the current selected date."""
return self._selected_date
selection = get_selection
def set_selection(self, date):
"""Set the selected date."""
if self._selected_date is not None and self._selected_date != date:
self._clear_selection()
self._selected_date = date
self._build_calendar(date.year, date.month) # reconstruct calendar
# see this URL for date format information:
# https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
class Datepicker(ttk.Entry):
def __init__(self, master, width=None, date_format="%Y-%m-%d", on_select=None, firstweekday=calendar.MONDAY, locale=None, activebackground='#b1dcfb', activeforeground='black', selectbackground='#003eff', selectforeground='white', borderwidth=1, relief="solid"):
self.date_var = Tkinter.StringVar()
entry_config = {}
if width is not None:
entry_config["width"] = width
ttk.Entry.__init__(self, master, textvariable=self.date_var, **entry_config)
self.date_format = date_format
self.on_select = on_select
self._is_calendar_visible = False
self.calendar_frame = Calendar(firstweekday=firstweekday, locale=locale, activebackground=activebackground, activeforeground=activeforeground, selectbackground=selectbackground, selectforeground=selectforeground, command=self._on_date_selection, on_click_month_control=lambda: self.focus())
self.bind("<1>", lambda event: self.show_date_on_calendar())
self.bind("<FocusOut>", lambda event: self._on_entry_focus_out())
self.bind("<Escape>", lambda event: master.focus())
self.calendar_frame.bind("<<DatePickerFocusOut>>", lambda event: self._on_calendar_focus_out())
# CTRL + PAGE UP: Move to the previous month.
self.bind("<Control-Prior>", lambda event: self.calendar_frame.prev_month())
# CTRL + PAGE DOWN: Move to the next month.
self.bind("<Control-Next>", lambda event: self.calendar_frame.next_month())
# CTRL + SHIFT + PAGE UP: Move to the previous year.
self.bind("<Control-Shift-Prior>", lambda event: self.calendar_frame.prev_year())
# CTRL + SHIFT + PAGE DOWN: Move to the next year.
self.bind("<Control-Shift-Next>", lambda event: self.calendar_frame.next_year())
# CTRL/COMMAND + LEFT: Move to the previous day.
self.bind("<Control-Left>", lambda event: self.calendar_frame.select_prev_day())
# CTRL/COMMAND + RIGHT: Move to the next day.
self.bind("<Control-Right>", lambda event: self.calendar_frame.select_next_day())
# CTRL/COMMAND + UP: Move to the previous week.
self.bind("<Control-Up>", lambda event: self.calendar_frame.select_prev_week_day())
# CTRL/COMMAND + DOWN: Move to the next week.
self.bind("<Control-Down>", lambda event: self.calendar_frame.select_next_week_day())
# CTRL/COMMAND + END: Close the datepicker and erase the date.
self.bind("<Control-End>", lambda event: self.erase())
# CTRL/COMMAND + HOME: Move to the current month.
self.bind("<Control-Home>", lambda event: self.calendar_frame.select_current_date())
# CTRL/COMMAND + SPACE: Show date on calendar
self.bind("<Control-space>", lambda event: self.show_date_on_calendar())
# CTRL/COMMAND + Return: Set to entry current selection
self.bind("<Control-Return>", lambda event: self.entry_selected_date())
def entry_selected_date(self):
selected_date = self.calendar_frame.selection()
if selected_date is not None:
self.date_var.set(selected_date.strftime(self.date_format))
self.hide_calendar()
def show_date_on_calendar(self):
try:
date = datetime.datetime.strptime(self.date_var.get(), self.date_format)
self.calendar_frame.set_selection(date)
except ValueError:
pass
self.show_calendar()
def show_calendar(self):
if not self._is_calendar_visible:
self.calendar_frame.place(in_=self, relx=0, rely=1)
self.calendar_frame.lift()
self._is_calendar_visible = True
def hide_calendar(self):
if self._is_calendar_visible:
self.calendar_frame.place_forget()
self._is_calendar_visible = False
def erase(self):
self.hide_calendar()
self.date_var.set("")
def _on_entry_focus_out(self):
if not str(self.focus_get()).startswith(str(self.calendar_frame)):
self.hide_calendar()
def _on_calendar_focus_out(self):
if self.focus_get() != self:
self.hide_calendar()
def _on_date_selection(self, date):
self.date_var.set(date.strftime(self.date_format))
self.hide_calendar()
@property
def is_calendar_visible(self):
return self._is_calendar_visible
if __name__ == "__main__":
from Tkinter import Tk, Frame
import sys
root = Tk()
root.geometry("500x500")
main =Frame(root, pady =10)
main.pack(expand=True, fill="both")
Datepicker(main).pack()
if 'win' not in sys.platform:
style = ttk.Style()
style.theme_use('clam')
root.mainloop()
Diff to Previous Revision
--- revision 1 2016-12-04 00:08:01
+++ revision 2 2016-12-04 00:13:40
@@ -31,9 +31,7 @@
CTRL/COMMAND + HOME: Move to the current month.
- CTRL/COMMAND + SPACE: Show calendar
-
- CTRL/COMMAND + SPACE: Show current selection
+ CTRL/COMMAND + SPACE: Show date on calendar
CTRL/COMMAND + DOWN: Move to the next week.
@@ -41,9 +39,9 @@
CTRL/COMMAND + HOME: Move to the current month.
- CTRL/COMMAND + SPACE: Show calendar
-
- CTRL/COMMAND + SPACE: Show current selection
+ CTRL/COMMAND + SPACE: Show date on calendar
+
+ CTRL/COMMAND + Return: Set to entry current selection
"""
import calendar
@@ -412,10 +410,10 @@
# CTRL/COMMAND + HOME: Move to the current month.
self.bind("<Control-Home>", lambda event: self.calendar_frame.select_current_date())
- # CTRL/COMMAND + SPACE: Show calendar
+ # CTRL/COMMAND + SPACE: Show date on calendar
self.bind("<Control-space>", lambda event: self.show_date_on_calendar())
- # CTRL/COMMAND + SPACE: Show current selection
+ # CTRL/COMMAND + Return: Set to entry current selection
self.bind("<Control-Return>", lambda event: self.entry_selected_date())