# 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 calendar CTRL/COMMAND + SPACE: Show current selection 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 calendar CTRL/COMMAND + SPACE: Show 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 calendar self.bind("<Control-space>", lambda event: self.show_date_on_calendar()) # CTRL/COMMAND + SPACE: Show 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()