2

I am looking for a way to implement a time picker in a tkinter application.

I was able to implement this (probably not in the best way) using the spinbox widget and also using @PRMoureu's wonderful answer for validation. What I have right now is this -

import tkinter as tk

class App(tk.Frame):
    def __init__(self,parent):
        super().__init__(parent)
        self.reg=self.register(self.hour_valid)
        self.hourstr=tk.StringVar(self,'10')
        self.hour = tk.Spinbox(self,from_=0,to=23,wrap=True,validate='focusout',validatecommand=(self.reg,'%P'),invalidcommand=self.hour_invalid,textvariable=self.hourstr,width=2)
        self.reg2=self.register(self.min_valid)
        self.minstr=tk.StringVar(self,'30')
        self.min = tk.Spinbox(self,from_=0,to=59,wrap=True,validate='focusout',validatecommand=(self.reg2,'%P'),invalidcommand=self.min_invalid,textvariable=self.minstr,width=2)
        self.hour.grid()
        self.min.grid(row=0,column=1)
    def hour_invalid(self):
        self.hourstr.set('10')
    def hour_valid(self,input):
        if (input.isdigit() and int(input) in range(24) and len(input) in range(1,3)):
            valid = True
        else:
            valid = False
        if not valid:
            self.hour.after_idle(lambda: self.hour.config(validate='focusout'))
        return valid
    def min_invalid(self):
        self.minstr.set('30')
    def min_valid(self,input):
        if (input.isdigit() and int(input) in range(60) and len(input) in range(1,3)):
            valid = True
        else:
            valid = False
        if not valid:
            self.min.after_idle(lambda: self.min.config(validate='focusout'))
        return valid
root = tk.Tk()
App(root).pack()
root.mainloop()

This seems like a pretty common requirement in GUI applications so I think there must be a more standard way to achieve this. How can I implement a user picked time widget in a cleaner way? I am asking this because the tiny feature I want implemented is when incrementing/decrementing the minute-spinbox, if it loops over, the hour-spinbox should accordingly increase/decrease. I thought of achieving this by setting a callback function, but I would not come to know which button of the spinbox exactly was triggered (up or down).

Rahul
  • 177
  • 3
  • 9
  • @PaulRooney I was able to find modules such as tkcalendar that helped pick dates, but unfortunately they cannot pick time. – Rahul Jul 15 '19 at 06:41
  • 1
    Actually this looked pretty standard to me. The methods are aimed to control user input - if you don't want the user to type in a number, you can simply add `state="readonly"` in your `Spinbox` and delete those validate methods and command. – Henry Yik Jul 15 '19 at 06:50
  • @HenryYik That actually makes things quite simple as that feature wasn't necessary for me.I'll make sure from now to always read the documentation thoroughly before implementing. – Rahul Jul 15 '19 at 07:25

2 Answers2

5

You can trace the changes on your minutes and act accordingly. Below sample shows how to automatically increase hour when minutes increases pass 59; you can adapt and figure out how to do the decrease part.

import tkinter as tk

class App(tk.Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.hourstr=tk.StringVar(self,'10')
        self.hour = tk.Spinbox(self,from_=0,to=23,wrap=True,textvariable=self.hourstr,width=2,state="readonly")
        self.minstr=tk.StringVar(self,'30')
        self.minstr.trace("w",self.trace_var)
        self.last_value = ""
        self.min = tk.Spinbox(self,from_=0,to=59,wrap=True,textvariable=self.minstr,width=2,state="readonly")
        self.hour.grid()
        self.min.grid(row=0,column=1)

    def trace_var(self,*args):
        if self.last_value == "59" and self.minstr.get() == "0":
            self.hourstr.set(int(self.hourstr.get())+1 if self.hourstr.get() !="23" else 0)
        self.last_value = self.minstr.get()

root = tk.Tk()
App(root).pack()
root.mainloop()
Henry Yik
  • 22,275
  • 4
  • 18
  • 40
  • How can I get the traced values in a variable so I can then use the timestamp? – bloo Jul 25 '20 at 11:31
  • They are bound to two `StringVar`. You can simply call the `get` method on `self.hourstr` and `self.minstr`. – Henry Yik Jul 25 '20 at 14:33
  • I was trying to have a button where after the user has chosen the desired time, say 10 (hours) 54 (mins) , I could then use those string values and convert them to a datetime object in order to filter a pandas dataframe object, so I created a function outside the class def get_time(): timehour = App(root).trace_varhour(), print(timehour) and then a button with command=get_time, but it returns the default value rather than the traced. – bloo Jul 25 '20 at 14:43
  • Store the instance of the class in a variable like `app = App(root)`, then call `app.pack()` on a separate line. When you need to retrieve the time, use `app.hourstr.get()` and `app.minstr.get()`. – Henry Yik Jul 25 '20 at 15:06
  • Following the above, I should have: import tkinter, class App(tk.Frame) and all the content, root = tk.Tk(), App(root).pack, app= App(root), app.pack(), print(app.hourstr.get(), which a) creates two App frames in the Gui and b) the print returns the default value. – bloo Jul 25 '20 at 15:24
  • Delete `App(root).pack`. Also this is not the place for problems unrelated to the question of the original post. If you have problem working on `tkinter`, ask a new question instead. – Henry Yik Jul 25 '20 at 15:31
  • 1
    The helpful part was the "Delete App(root).pack, I found the solution which I am going to post in https://stackoverflow.com/questions/63088681/tkinter-spinbox-how-can-i-get-traced-values-into-a-variable?noredirect=1#comment111564361_63088681 for anyone that it might help. It wasn't a problem with tkinter, it was just a follow up question on your solution but the app.hourstr.get() was not the right place. Thank you for your time. – bloo Jul 25 '20 at 15:42
2

Thanks Henry for your code, it is excellant. Here is my extension for Seconds:

# You can trace the changes on your minutes and act accordingly. Below sample shows how to automatically increase hour when minutes increases pass 59 and similarly Seconds increase pass 59; you can adapt and figure out how to do the decrease part.

import tkinter as tk

class App(tk.Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.hourstr=tk.StringVar(self,'10')
        self.hour = tk.Spinbox(self,from_=0,to=23,wrap=True,textvariable=self.hourstr,width=2,state="readonly")
        self.minstr=tk.StringVar(self,'30')

        self.min = tk.Spinbox(self,from_=0,to=59,wrap=True,textvariable=self.minstr,width=2)    # ,state="readonly"
        self.secstr=tk.StringVar(self,'00')
        self.sec = tk.Spinbox(self,from_=0,to=59,wrap=True,textvariable=self.secstr,width=2) 

        self.last_valueSec = ""
        self.last_value = ""        
        self.minstr.trace("w",self.trace_var)
        self.secstr.trace("w",self.trace_varsec)

        self.hour.grid()
        self.min.grid(row=0,column=1)
        self.sec.grid(row=0,column=2)

    def trace_var(self,*args):
        if self.last_value == "59" and self.minstr.get() == "0":
            self.hourstr.set(int(self.hourstr.get())+1 if self.hourstr.get() !="23" else 0)   
        self.last_value = self.minstr.get()

    def trace_varsec(self,*args):
        if self.last_valueSec == "59" and self.secstr.get() == "0":
            self.minstr.set(int(self.minstr.get())+1 if self.minstr.get() !="59" else 0)
            if self.last_value == "59":
                self.hourstr.set(int(self.hourstr.get())+1 if self.hourstr.get() !="23" else 0)            
        self.last_valueSec = self.secstr.get()

root = tk.Tk()
App(root).pack()
root.mainloop()