2

I’m using Python 3.x to create a multilingual “calendar” widget that has separate spinboxes for year, month, day, etc. A requirement is that whatever widget is used must NEVER hide any part of the GUI at any time, thus ruling out drop down lists etc. , making spinboxes an obvious choice.

If anyone knows where public-domain source code for a pure python/tkinter “date & time entry” widgit (ideally handling leap year and all of the end-of-month cases) can be found, pointing me to that repository would be appreciated.

The closest solution I could find was this OptionMenu solution (Change OptionMenu based on what is selected in another OptionMenu ) but it can’t be used due to the “don’t hide” requirement. Using that as inspiration (and after reviewing the Spinbox documentation at http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/spinbox.html) I came up with the code below. To keep things simple I’ve removed my code that handles leap year and end-of-month issues. The code works except for two problems:

Problem 1 – I can’t get “DaySpinBox” to dynamically adjust its’ range to set itself to the month in the “MonthSpinBox”:

According to how I read Mark Lutz’s “Programming Python” book (third edition, pages 383 & 384) I should be able to dynamically set the maximum/last day of DaySpinBox (by using “values=” or the ”to=” and ”from_=” options) to match the month appearing in MonthSpinBox. I’m tracing the MonthSpinBox variable (“SelectedMonth”) and, as per the book, I’ve tried updating DaySpinBox using these approaches: 1 - Using “to= SelectedMonth”. Some attempts at this appear as comments in the call that creates DaySpinBox. (I’m new to lambdas, perhaps I am not using them correctly?),
2 - Using “DaySpinBox[ ‘to’ ] = SelectedMonth ”, and 3 - Using “DaySpinBox.config( to = SelectedMonth )” .
I’ve also tried using “values=” with all approaches without success (the “to=....” option is preferred).

Question 1: Can Spinboxes be set up to use dynamic ranges or, as I’m starting to suspect, are their ranges unchangeable after the Spinbox has been created?

Question 2: After much web searching I haven’t found anything that explicitly states which Spinbox parameters (the “to=” , “from=” and “value=” keywords) can be dynamically changed and which ones cannot – where can I find this information (for all widget types) ?

Problem 2 – MonthSpinBox is always initialized to January instead of the current month

I’m using the “textvariable” keyword to initialize the year, month and day spinboxes to “today”. This works for YearSpinBox and DaySpinBox but not for MonthSpinBox. (Annoyingly I think MonthSpinBox was working but I broke it trying to fix DaySpinBox). The only obvious difference is that Year and Day spinboxes use integers while the Month spinbox uses strings. Any suggestions?

For both problems I considered the possibility of a LEGB issue but no functions are nested so variable hiding shouldn’t be an issue - unless one of my variables is duplicating and hiding one defined in tkinter, etc.

What am I missing?

BTW, I know about the pros/cons of using globals, also about PEP8 (http://www.python.org/dev/peps/pep-0008/) - see the second bullet in the table of contents. ;>)

Thanks.

from tkinter  import *
from tkinter  import ttk
from datetime import datetime
from collections import OrderedDict  

root = Tk()     # put here so IntVar (etc.) is useable during initialization

StartUpDateTime    = datetime.now()
DefaultYear        = StartUpDateTime.year   
DefaultMonthNumber = StartUpDateTime.month  
DefaultDayOfMonth  = StartUpDateTime.day   

# These are only used to build the Month name & length dictionary  
YearAndMonthLengths =  [ 365, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ] 
EnglishMonthNames = ( 'Entire Year', 'January', 'February', 'March', 
                      'April', 'May', 'June', 'July', 'August', 'September', 
                      'October', 'November', 'December' )
FrenchMonthNames  = ( 'année entière', 'janvier', 'février', 'mars', 
                      'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 
                      'octobre', 'novembre', 'décembre' )
#SpanishMonthNames = ....
#ItalianMonthNames = ....

Month_Names = FrenchMonthNames
Month_Names = EnglishMonthNames    # comment out this line to use French

# Create dictionary containing month names that appear in the Month spinbox.
# OrderedDict used because Month name (=key) field must be language-independent.

DaysInMonth = OrderedDict()    
for i in range( 0, len( Month_Names ) ) :   
    DaysInMonth[ Month_Names[ i ] ] = YearAndMonthLengths[ i ]  

DefaultMonthName   = Month_Names[ DefaultMonthNumber ]
DefaultMonthLength = DaysInMonth[ DefaultMonthName ]
# Initialize the Spinbox interface variables to todays date
SelectedYear        = IntVar(    value = DefaultYear )
SelectedMonthName   = StringVar( value = DefaultMonthName ) 
SelectedMonthLength = IntVar(    value = DefaultMonthLength )
SelectedDay         = IntVar(    value = DefaultDayOfMonth )

#-------------------------------------------------------------------------------

def GetChosenMonthLength( *args ) :

    global SelectedMonthLength
    SelectedMonthLength.set( DaysInMonth[ SelectedMonthName.get()  ] )
    return

#-------------------------------------------------------------------------------

mainframe = Frame( root )
mainframe.grid( row = 0, column = 0 )
# Show today's date.  If it's July 17th, 2023 the spinboxes must be initialized 
# to "July" ("julliet" if using French), "17" and "2023".  
YearSpinBox  = Spinbox( mainframe, 
                        from_ = 2000, to = 2050,
                        textvariable = SelectedYear, 
                        wrap = TRUE, state = 'readonly', width = 5 )
MonthSpinBox  = Spinbox( mainframe,  
                         values = tuple( DaysInMonth.keys() )[1:],                         
                         textvariable = SelectedMonthName, 
                         wrap = TRUE, state = 'readonly', width = 10 )       
DaySpinBox  = Spinbox( mainframe, 
                       from_ = 1, 
                       to = SelectedMonthLength.get(), 
#                       to = DaysInMonth[ SelectedMonthName.get() ],
#                       values = tuple( str( i ) for i in range( 1, SelectedMonthLength.get() + 1 ) ),
#                       values = lambda : tuple( str( i ) for i in range( 1, DaysInMonth[ SelectedMonthName.get() ] + 1 ) )
                       textvariable = SelectedDay,  
                       wrap = TRUE, state = 'readonly', width = 16
                     )
MonthSpinBox.grid( row = 0, column = 0 )
DaySpinBox.grid(   row = 0, column = 1 )
YearSpinBox.grid(  row = 0, column = 2 )

SelectedMonthName.trace( 'w', GetChosenMonthLength ) 

mainloop()

BTW, the sample code omits the debugging code that proved all Spinbox interface variables are being updated correctly - including the variable that contains the last day of the changed/new month (which gets “sent” to the Day Spinbox using the “TO=” keyword).

Community
  • 1
  • 1
user1459519
  • 712
  • 9
  • 20

1 Answers1

2

All options to all widgets can always be configured dynamically. There is only a single exception if I recall correctly, which is a feature almost nobody ever uses: the class option on frames.

Related to updating the day spinbox, I don't see where you're trying to update it so I'm not sure why you think it should be updating. To update the spinbox, attach a command to the month spinbox and then do your update in the callback.

For example:

def update_days():
    maxdays = int(DaysInMonth[SelectedMonthName.get()])
    DaySpinBox.config(to=maxdays)

MonthSpinBox  = Spinbox(...,command=update_days) 

As for why the month isn't set to the current month ... I don't know. It looks like you're doing things right. The only difference between this and the other widgets is that this is a StringVar rather than an IntVar, and the month spinbox has a values argument. Perhaps this is a bug or poorly documented feature in tkinter that is triggered by this difference.

A simply work-around is to add SelectedMonthName.set(DefaultMonthName) after creating the month spinbox to reset the stringvar to the proper default.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Thanks to @Bryan for confirming my understanding, but none of the ways I’ve tried to update the Day Spinbox have worked. The issue is that the MAXIMUM value of the Day Spinbox is not updated to match the last day of the month in the Month Spinbox. (The value in the Spinbox updates properly.) To see the problem, select January 31, then change month to February. As desired, the DISPLAYED date in the Spinbox will change to February 28 (leap years ignored) – but the Day Spinbox still allows the day to be changed to February 31! It is this (year and month dependent) problem I need to fix. – user1459519 Dec 12 '13 at 19:55
  • @user1459519: I've updated my answer to show you how to do it. – Bryan Oakley Dec 12 '13 at 20:07
  • Doh! Many thanks for spotting the problem, once again your help has been quite valuable. I had looked at the code for so long that I couldn’t see the forest for the trees (the omitted “command=” keyword and it’s associated helper function). After making both changes the code now works. Others should note that I also added the same “command=” line to the Year Spinbox to handle February 29th when the year is changed. – user1459519 Dec 12 '13 at 22:18