56

Is there any standard way tkinter apps allow the user to choose a date?

2
  • Impropable, as tkinter is very minimalistic. If you want something fancy without building it yourself, your best bet is one of the larger, batteries-included GUI toolkits (I recommend Qt).
    – user395760
    Commented Dec 14, 2010 at 20:23
  • 21
    +1 totally reasonable question. This is the kind of UI thing that should be standardized within a toolkit. Surprised tk doesn't offer it.
    – Anne
    Commented Jan 27, 2012 at 21:20

8 Answers 8

33

In case anyone still needs this - here's a simple code to create a Calendar and DateEntry widget using tkcalendar package.

pip install tkcalendar (for installing the package)

try:
    import tkinter as tk
    from tkinter import ttk
except ImportError:
    import Tkinter as tk
    import ttk

from tkcalendar import Calendar, DateEntry

def example1():
    def print_sel():
        print(cal.selection_get())

    top = tk.Toplevel(root)

    cal = Calendar(top,
                   font="Arial 14", selectmode='day',
                   cursor="hand1", year=2018, month=2, day=5)
    cal.pack(fill="both", expand=True)
    ttk.Button(top, text="ok", command=print_sel).pack()

def example2():
    top = tk.Toplevel(root)

    ttk.Label(top, text='Choose date').pack(padx=10, pady=10)

    cal = DateEntry(top, width=12, background='darkblue',
                    foreground='white', borderwidth=2)
    cal.pack(padx=10, pady=10)

root = tk.Tk()
s = ttk.Style(root)
s.theme_use('clam')

ttk.Button(root, text='Calendar', command=example1).pack(padx=10, pady=10)
ttk.Button(root, text='DateEntry', command=example2).pack(padx=10, pady=10)

root.mainloop()
12

Not as far as I could find. For anyone who wants to do this in the future:

I used tkSimpleDialog and ttkcalendar.py (with modifications from this SO post) to make a CalendarDialog. The modified versions of the three files are available on my github.

Below is the code in my CalendarDialog.py:

import Tkinter

import ttkcalendar
import tkSimpleDialog

class CalendarDialog(tkSimpleDialog.Dialog):
    """Dialog box that displays a calendar and returns the selected date"""
    def body(self, master):
        self.calendar = ttkcalendar.Calendar(master)
        self.calendar.pack()

    def apply(self):
        self.result = self.calendar.selection

# Demo code:
def main():
    root = Tkinter.Tk()
    root.wm_title("CalendarDialog Demo")

    def onclick():
        cd = CalendarDialog(root)
        print cd.result

    button = Tkinter.Button(root, text="Click me to see a calendar!", command=onclick)
    button.pack()
    root.update()

    root.mainloop()


if __name__ == "__main__":
    main()
3
  • 2
    Heh, the python source file you linked doesn't actually HAVE those modifications in it - ran into a stumbling block until I realized it had to be modified to get it to play nice with an actual Tkinter app :P Commented Jul 21, 2015 at 14:51
  • Yeah, that's why I linked to the patched versions on my github as well.
    – Moshe
    Commented Jul 21, 2015 at 15:22
  • Ahh gotcha, thanks. I just assumed the links all went to the same place because I didn't read them close enough to see they weren't all github :P Commented Jul 21, 2015 at 23:20
12

Try with the following:

from tkinter import *

from tkcalendar import Calendar,DateEntry

root = Tk()

cal = DateEntry(root,width=30,bg="darkblue",fg="white",year=2010)

cal.grid()

root.mainloop()

where tkcalendar library should be downloaded and installed through pip install tkcalender command.

0
6

The answers here provide most of what you asked for, but I felt none of them were completely satisfying. As a Tkinter newbie, I needed more hand-holding. My complaints:

  1. Assuming I want the calendar to default to a 2012 date rather than today's date,
  2. Not returning the result as a variable for future use,
  3. An unnecessary top layer menu before getting to the calendar, and
  4. Not cleaning up the windows and exiting mainloop after a selection has been made.

So with gratitude to the previous answers which helped me immensely, here's my version.

def get_date():
    import tkinter as tk
    from tkinter import ttk
    from tkcalendar import Calendar, DateEntry

    def cal_done():
        top.withdraw()
        root.quit()

    root = tk.Tk()
    root.withdraw() # keep the root window from appearing

    top = tk.Toplevel(root)

    cal = Calendar(top,
                   font="Arial 14", selectmode='day',
                   cursor="hand1")
    cal.pack(fill="both", expand=True)
    ttk.Button(top, text="ok", command=cal_done).pack()

    selected_date = None
    root.mainloop()
    return cal.selection_get()

selection = get_date()
print(selection)
3
  • Interesting how cal_done is defined within get_date function. I've only seen it done within classes and mainline before. Commented Nov 20, 2020 at 16:07
  • This is perfect. Thank you so much for this. Commented Dec 3, 2020 at 11:00
  • Best answer IMO. The only think I would change is in the kill command use top.destroy() and root.destroy(). This will allow you to use the Spyder IDE without issues when you make the selection. Otherwise you have to keep creating new terminals. Commented Feb 11, 2023 at 21:11
5

No, but you can get it from the user as a datetime element from a formatted string..

Example:

import datetime

userdatestring = '2013-05-10'
thedate = datetime.datetime.strptime(userdatestring, '%Y-%m-%d') 

Check out http://docs.python.org/2/library/datetime.html#strftime-strptime-behavior. It's handy, although not the most user friendly way of getting a date.

5

This is how we code a python datepicker in tkinter.

# !/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Rambarun Komaljeet
# License: Freeware
# ---------------------------------------------------------------------------
import calendar
import tkinter as tk
import time
from tkinter import ttk


class MyDatePicker(tk.Toplevel):
    """
    Description:
        A tkinter GUI date picker.
    """

    def __init__(self, widget=None, format_str=None):
        """
        :param widget: widget of parent instance.

        :param format_str: print format in which to display date.
        :type format_str: string

        Example::
            a = MyDatePicker(self, widget=self.parent widget,
                             format_str='%02d-%s-%s')
        """

        super().__init__()
        self.widget = widget
        self.str_format = format_str

        self.title("Date Picker")
        self.resizable(0, 0)
        self.geometry("+630+390")

        self.init_frames()
        self.init_needed_vars()
        self.init_month_year_labels()
        self.init_buttons()
        self.space_between_widgets()
        self.fill_days()
        self.make_calendar()

    def init_frames(self):
        self.frame1 = tk.Frame(self)
        self.frame1.pack()

        self.frame_days = tk.Frame(self)
        self.frame_days.pack()

    def init_needed_vars(self):
        self.month_names = tuple(calendar.month_name)
        self.day_names = tuple(calendar.day_abbr)
        self.year = time.strftime("%Y")
        self.month = time.strftime("%B")

    def init_month_year_labels(self):
        self.year_str_var = tk.StringVar()
        self.month_str_var = tk.StringVar()

        self.year_str_var.set(self.year)
        self.year_lbl = tk.Label(self.frame1, textvariable=self.year_str_var,
                                 width=3)
        self.year_lbl.grid(row=0, column=5)

        self.month_str_var.set(self.month)
        self.month_lbl = tk.Label(self.frame1, textvariable=self.month_str_var,
                                  width=8)
        self.month_lbl.grid(row=0, column=1)

    def init_buttons(self):
        self.left_yr = ttk.Button(self.frame1, text="←", width=5,
                                  command=self.prev_year)
        self.left_yr.grid(row=0, column=4)

        self.right_yr = ttk.Button(self.frame1, text="→", width=5,
                                   command=self.next_year)
        self.right_yr.grid(row=0, column=6)

        self.left_mon = ttk.Button(self.frame1, text="←", width=5,
                                   command=self.prev_month)
        self.left_mon.grid(row=0, column=0)

        self.right_mon = ttk.Button(self.frame1, text="→", width=5,
                                    command=self.next_month)
        self.right_mon.grid(row=0, column=2)

    def space_between_widgets(self):
        self.frame1.grid_columnconfigure(3, minsize=40)

    def prev_year(self):
        self.prev_yr = int(self.year_str_var.get()) - 1
        self.year_str_var.set(self.prev_yr)

        self.make_calendar()

    def next_year(self):
        self.next_yr = int(self.year_str_var.get()) + 1
        self.year_str_var.set(self.next_yr)

        self.make_calendar()

    def prev_month(self):
        index_current_month = self.month_names.index(self.month_str_var.get())
        index_prev_month = index_current_month - 1

        #  index 0 is empty string, use index 12 instead,
        # which is index of December.
        if index_prev_month == 0:
            self.month_str_var.set(self.month_names[12])
        else:
            self.month_str_var.set(self.month_names[index_current_month - 1])

        self.make_calendar()

    def next_month(self):
        index_current_month = self.month_names.index(self.month_str_var.get())

        try:
            self.month_str_var.set(self.month_names[index_current_month + 1])
        except IndexError:
            #  index 13 does not exist, use index 1 instead, which is January.
            self.month_str_var.set(self.month_names[1])

        self.make_calendar()

    def fill_days(self):
        col = 0
        #  Creates days label
        for day in self.day_names:
            self.lbl_day = tk.Label(self.frame_days, text=day)
            self.lbl_day.grid(row=0, column=col)
            col += 1

    def make_calendar(self):
        #  Delete date buttons if already present.
        #  Each button must have its own instance attribute for this to work.
        try:
            for dates in self.m_cal:
                for date in dates:
                    if date == 0:
                        continue

                    self.delete_buttons(date)

        except AttributeError:
            pass

        year = int(self.year_str_var.get())
        month = self.month_names.index(self.month_str_var.get())
        self.m_cal = calendar.monthcalendar(year, month)

        #  build dates buttons.
        for dates in self.m_cal:
            row = self.m_cal.index(dates) + 1
            for date in dates:
                col = dates.index(date)

                if date == 0:
                    continue

                self.make_button(str(date), str(row), str(col))

    def make_button(self, date, row, column):
        """
        Description:
            Build a date button.

        :param date: date.
        :type date: string

        :param row: row number.
        :type row: string

        :param column: column number.
        :type column: string
        """
        exec(
            "self.btn_" + date + " = ttk.Button(self.frame_days, text=" + date
            + ", width=5)\n"
            "self.btn_" + date + ".grid(row=" + row + " , column=" + column
            + ")\n"
            "self.btn_" + date + ".bind(\"<Button-1>\", self.get_date)"
        )

    def delete_buttons(self, date):
        """
        Description:
            Delete a date button.

        :param date: date.
        :type: string
        """
        exec(
            "self.btn_" + str(date) + ".destroy()"
        )

    def get_date(self, clicked=None):
        """
        Description:
            Get the date from the calendar on button click.

        :param clicked: button clicked event.
        :type clicked: tkinter event
        """

        clicked_button = clicked.widget
        year = self.year_str_var.get()
        month = self.month_str_var.get()
        date = clicked_button['text']

        self.full_date = self.str_format % (date, month, year)
        print(self.full_date)
        #  Replace with parent 'widget' of your choice.
        try:
            self.widget.delete(0, tk.END)
            self.widget.insert(0, self.full_date)
        except AttributeError:
            pass


if __name__ == '__main__':
    def application():
        MyDatePicker(format_str='%02d-%s-%s')

    root = tk.Tk()
    btn = tk.Button(root, text="test", command=application)
    btn.pack()
    root.mainloop()
2
  • 1
    Error message please.Alsp note that is python 3 code. Commented Apr 15, 2018 at 0:24
  • In fact I'm using python 2. The error starts not findind "super()". Commented Apr 26, 2018 at 18:36
1

You should install the tkcalendar module. Use pip3 for this:

pip3 install tkcalendar

After the install you can import the module in your code.

Use this:

from tkcalendar import Calendar
0

If you don't want to add another library and you are happy with a very simple Spinbox that accepts iso dates with a little up and down arrow, looking like this:

Screenshot of Spinbox

You can use this code:

from datetime import date, timedelta
from tkinter import Spinbox, StringVar, Event

_PLUS_ONE_DAY = timedelta(days=1)
_MINUS_ONE_DAY = timedelta(days=-1)


class DatePicker(Spinbox):
    def __init__(self, master=None, initial: date = None, **kw):
        if initial is None:
            initial = date.today()
        self.date = initial
        self.string_var = StringVar(master, "1")
        super().__init__(master, textvariable=self.string_var, from_=0, to=10000, **kw)

        self.string_var.set(initial.isoformat())
        self.config(command=self._check_value_change)
        self.string_var.trace_add("write", self._on_string_var_change)
        self.bind('<Return>', self._on_enter_pressed)

    def _on_enter_pressed(self, event: Event):
        self.string_var.set(self.date.isoformat())
        self.winfo_toplevel().focus_set()

    def _on_string_var_change(self, s0: str, s1: str, s2: str):
        try:
            year, month, day = self.string_var.get().split("-")
            self.date = date(int(year), int(month), int(day))
        except ValueError:
            return

    def _check_value_change(self):
        new_value = self.get()
        if new_value == str(self.date.year - 1):
            self.date += _MINUS_ONE_DAY
        elif new_value == str(self.date.year + 1):
            self.date += _PLUS_ONE_DAY

        self.string_var.set(self.date.isoformat())

While the code is relatively short, its not particularly pretty, as it tricks a Spinbox to behave like an input method for dates. Call date_picker.date to get the currently held date value.

Please note that changing the format is not straight forward. Leave a comment if you are interested in a version that supports other formats

Not the answer you're looking for? Browse other questions tagged or ask your own question.