Guilherme Polo | a7d2797 | 2009-01-28 16:06:51 +0000 | [diff] [blame] | 1 | """ |
| 2 | Simple calendar using ttk Treeview together with calendar and datetime |
| 3 | classes. |
| 4 | """ |
| 5 | import calendar |
| 6 | import tkinter |
| 7 | import tkinter.font |
| 8 | from tkinter import ttk |
| 9 | |
| 10 | def get_calendar(locale, fwday): |
| 11 | # instantiate proper calendar class |
| 12 | if locale is None: |
| 13 | return calendar.TextCalendar(fwday) |
| 14 | else: |
| 15 | return calendar.LocaleTextCalendar(fwday, locale) |
| 16 | |
| 17 | class Calendar(ttk.Frame): |
| 18 | # XXX ToDo: cget and configure |
| 19 | |
| 20 | datetime = calendar.datetime.datetime |
| 21 | timedelta = calendar.datetime.timedelta |
| 22 | |
| 23 | def __init__(self, master=None, **kw): |
| 24 | """ |
| 25 | WIDGET-SPECIFIC OPTIONS |
| 26 | |
| 27 | locale, firstweekday, year, month, selectbackground, |
| 28 | selectforeground |
| 29 | """ |
| 30 | # remove custom options from kw before initializating ttk.Frame |
| 31 | fwday = kw.pop('firstweekday', calendar.MONDAY) |
| 32 | year = kw.pop('year', self.datetime.now().year) |
| 33 | month = kw.pop('month', self.datetime.now().month) |
| 34 | locale = kw.pop('locale', None) |
| 35 | sel_bg = kw.pop('selectbackground', '#ecffc4') |
| 36 | sel_fg = kw.pop('selectforeground', '#05640e') |
| 37 | |
| 38 | self._date = self.datetime(year, month, 1) |
| 39 | self._selection = None # no date selected |
| 40 | |
| 41 | ttk.Frame.__init__(self, master, **kw) |
| 42 | |
| 43 | self._cal = get_calendar(locale, fwday) |
| 44 | |
| 45 | self.__setup_styles() # creates custom styles |
| 46 | self.__place_widgets() # pack/grid used widgets |
| 47 | self.__config_calendar() # adjust calendar columns and setup tags |
| 48 | # configure a canvas, and proper bindings, for selecting dates |
| 49 | self.__setup_selection(sel_bg, sel_fg) |
| 50 | |
| 51 | # store items ids, used for insertion later |
| 52 | self._items = [self._calendar.insert('', 'end', values='') |
| 53 | for _ in range(6)] |
| 54 | # insert dates in the currently empty calendar |
| 55 | self._build_calendar() |
| 56 | |
| 57 | # set the minimal size for the widget |
| 58 | self._calendar.bind('<Map>', self.__minsize) |
| 59 | |
| 60 | def __setitem__(self, item, value): |
| 61 | if item in ('year', 'month'): |
| 62 | raise AttributeError("attribute '%s' is not writeable" % item) |
| 63 | elif item == 'selectbackground': |
| 64 | self._canvas['background'] = value |
| 65 | elif item == 'selectforeground': |
| 66 | self._canvas.itemconfigure(self._canvas.text, item=value) |
| 67 | else: |
| 68 | ttk.Frame.__setitem__(self, item, value) |
| 69 | |
| 70 | def __getitem__(self, item): |
| 71 | if item in ('year', 'month'): |
| 72 | return getattr(self._date, item) |
| 73 | elif item == 'selectbackground': |
| 74 | return self._canvas['background'] |
| 75 | elif item == 'selectforeground': |
| 76 | return self._canvas.itemcget(self._canvas.text, 'fill') |
| 77 | else: |
| 78 | r = ttk.tclobjs_to_py({item: ttk.Frame.__getitem__(self, item)}) |
| 79 | return r[item] |
| 80 | |
| 81 | def __setup_styles(self): |
| 82 | # custom ttk styles |
| 83 | style = ttk.Style(self.master) |
| 84 | arrow_layout = lambda dir: ( |
| 85 | [('Button.focus', {'children': [('Button.%sarrow' % dir, None)]})] |
| 86 | ) |
| 87 | style.layout('L.TButton', arrow_layout('left')) |
| 88 | style.layout('R.TButton', arrow_layout('right')) |
| 89 | |
| 90 | def __place_widgets(self): |
| 91 | # header frame and its widgets |
| 92 | hframe = ttk.Frame(self) |
| 93 | lbtn = ttk.Button(hframe, style='L.TButton', command=self._prev_month) |
| 94 | rbtn = ttk.Button(hframe, style='R.TButton', command=self._next_month) |
| 95 | self._header = ttk.Label(hframe, width=15, anchor='center') |
| 96 | # the calendar |
| 97 | self._calendar = ttk.Treeview(show='', selectmode='none', height=7) |
| 98 | |
| 99 | # pack the widgets |
| 100 | hframe.pack(in_=self, side='top', pady=4, anchor='center') |
| 101 | lbtn.grid(in_=hframe) |
| 102 | self._header.grid(in_=hframe, column=1, row=0, padx=12) |
| 103 | rbtn.grid(in_=hframe, column=2, row=0) |
| 104 | self._calendar.pack(in_=self, expand=1, fill='both', side='bottom') |
| 105 | |
| 106 | def __config_calendar(self): |
| 107 | cols = self._cal.formatweekheader(3).split() |
| 108 | self._calendar['columns'] = cols |
| 109 | self._calendar.tag_configure('header', background='grey90') |
| 110 | self._calendar.insert('', 'end', values=cols, tag='header') |
| 111 | # adjust its columns width |
| 112 | font = tkinter.font.Font() |
| 113 | maxwidth = max(font.measure(col) for col in cols) |
| 114 | for col in cols: |
| 115 | self._calendar.column(col, width=maxwidth, minwidth=maxwidth, |
| 116 | anchor='e') |
| 117 | |
| 118 | def __setup_selection(self, sel_bg, sel_fg): |
| 119 | self._font = tkinter.font.Font() |
| 120 | self._canvas = canvas = tkinter.Canvas(self._calendar, |
| 121 | background=sel_bg, borderwidth=0, highlightthickness=0) |
| 122 | canvas.text = canvas.create_text(0, 0, fill=sel_fg, anchor='w') |
| 123 | |
| 124 | canvas.bind('<ButtonPress-1>', lambda evt: canvas.place_forget()) |
| 125 | self._calendar.bind('<Configure>', lambda evt: canvas.place_forget()) |
| 126 | self._calendar.bind('<ButtonPress-1>', self._pressed) |
| 127 | |
| 128 | def __minsize(self, evt): |
| 129 | width, height = self._calendar.master.geometry().split('x') |
| 130 | height = height[:height.index('+')] |
| 131 | self._calendar.master.minsize(width, height) |
| 132 | |
| 133 | def _build_calendar(self): |
| 134 | year, month = self._date.year, self._date.month |
| 135 | |
| 136 | # update header text (Month, YEAR) |
| 137 | header = self._cal.formatmonthname(year, month, 0) |
| 138 | self._header['text'] = header.title() |
| 139 | |
| 140 | # update calendar shown dates |
| 141 | cal = self._cal.monthdayscalendar(year, month) |
| 142 | for indx, item in enumerate(self._items): |
| 143 | week = cal[indx] if indx < len(cal) else [] |
| 144 | fmt_week = [('%02d' % day) if day else '' for day in week] |
| 145 | self._calendar.item(item, values=fmt_week) |
| 146 | |
| 147 | def _show_selection(self, text, bbox): |
| 148 | """Configure canvas for a new selection.""" |
| 149 | x, y, width, height = bbox |
| 150 | |
| 151 | textw = self._font.measure(text) |
| 152 | |
| 153 | canvas = self._canvas |
| 154 | canvas.configure(width=width, height=height) |
| 155 | canvas.coords(canvas.text, width - textw, height / 2 - 1) |
| 156 | canvas.itemconfigure(canvas.text, text=text) |
| 157 | canvas.place(in_=self._calendar, x=x, y=y) |
| 158 | |
| 159 | # Callbacks |
| 160 | |
| 161 | def _pressed(self, evt): |
| 162 | """Clicked somewhere in the calendar.""" |
| 163 | x, y, widget = evt.x, evt.y, evt.widget |
| 164 | item = widget.identify_row(y) |
| 165 | column = widget.identify_column(x) |
| 166 | |
| 167 | if not column or not item in self._items: |
| 168 | # clicked in the weekdays row or just outside the columns |
| 169 | return |
| 170 | |
| 171 | item_values = widget.item(item)['values'] |
| 172 | if not len(item_values): # row is empty for this month |
| 173 | return |
| 174 | |
| 175 | text = item_values[int(column[1]) - 1] |
| 176 | if not text: # date is empty |
| 177 | return |
| 178 | |
| 179 | bbox = widget.bbox(item, column) |
| 180 | if not bbox: # calendar not visible yet |
| 181 | return |
| 182 | |
| 183 | # update and then show selection |
| 184 | text = '%02d' % text |
| 185 | self._selection = (text, item, column) |
| 186 | self._show_selection(text, bbox) |
| 187 | |
| 188 | def _prev_month(self): |
| 189 | """Updated calendar to show the previous month.""" |
| 190 | self._canvas.place_forget() |
| 191 | |
| 192 | self._date = self._date - self.timedelta(days=1) |
| 193 | self._date = self.datetime(self._date.year, self._date.month, 1) |
| 194 | self._build_calendar() # reconstuct calendar |
| 195 | |
| 196 | def _next_month(self): |
| 197 | """Update calendar to show the next month.""" |
| 198 | self._canvas.place_forget() |
| 199 | |
| 200 | year, month = self._date.year, self._date.month |
| 201 | self._date = self._date + self.timedelta( |
| 202 | days=calendar.monthrange(year, month)[1] + 1) |
| 203 | self._date = self.datetime(self._date.year, self._date.month, 1) |
| 204 | self._build_calendar() # reconstruct calendar |
| 205 | |
| 206 | # Properties |
| 207 | |
| 208 | @property |
| 209 | def selection(self): |
| 210 | """Return a datetime representing the current selected date.""" |
| 211 | if not self._selection: |
| 212 | return None |
| 213 | |
| 214 | year, month = self._date.year, self._date.month |
| 215 | return self.datetime(year, month, int(self._selection[0])) |
| 216 | |
| 217 | def test(): |
| 218 | import sys |
| 219 | root = tkinter.Tk() |
| 220 | root.title('Ttk Calendar') |
| 221 | ttkcal = Calendar(firstweekday=calendar.SUNDAY) |
| 222 | ttkcal.pack(expand=1, fill='both') |
| 223 | |
| 224 | if 'win' not in sys.platform: |
| 225 | style = ttk.Style() |
| 226 | style.theme_use('clam') |
| 227 | |
| 228 | root.mainloop() |
| 229 | |
| 230 | if __name__ == '__main__': |
| 231 | test() |