1

I am creating a date of birth entry form for my application, and want to make it so that they can only enter numbers, and to limit the numbers inputted to the maximum amount needed e.g. 2 for day. When I tried to implement this, it doesn't even let me do anything in the entry widgets. How can I limit the character length and only accept numbers?

def onlyNumbers(char):
    if (len(dayE.get()) >= 2):
        return False
    elif (len(monthE.get()) >= 2):
        return False
    elif (len(yearE.get()) >= 4):
        return False
    elif (char is ""):
        return True
    else:
        return char.isdigit()
dayV = tk.StringVar(self, value = "DD")
monthV = tk.StringVar(self, value = "MM")
yearV = tk.StringVar(self, value = "YYYY")
validation = self.register(onlyNumbers)

dayE = tk.Entry(self, font = BASIC_FONT, textvariable = dayV, width = 5, justify = "center")
dayE.pack()
dayE.config(validate = "key", validatecommand = (validation, "%P"))

monthE = tk.Entry(self, font = BASIC_FONT, textvariable = monthV, width = 5, justify = "center")
monthE.pack()
monthE.config(validate = "key", validatecommand = (validation, "%P"))

yearE = tk.Entry(self, font = BASIC_FONT, textvariable = yearV, width = 5, justify = "center")
yearE.pack()
yearE.config(validate = "key", validatecommand = (validation, "%P"))
stovfl
  • 14,998
  • 7
  • 24
  • 51
Ozzy Kids
  • 57
  • 1
  • 8
  • Does this answer your question? [Interactively validating Entry widget content in tkinter](https://stackoverflow.com/questions/4140437/interactively-validating-entry-widget-content-in-tkinter) – stovfl Mar 27 '20 at 19:04
  • @stovfl not really, I can get the validation code to run, so the onlyNumbers function does run, it just doesn't work properly, it doesn't let me delete values or add any value once the initial DD is deleted – Ozzy Kids Mar 28 '20 at 08:42
  • 1
    With the initialisation of the Entries the `onlyNumbers` function always returns False . This means none of the entered characters will be accepted and the text in the Entries won't change. If `dayE` is being edited `len(monthE.get()) >= 2` and it returns False. If anything else is being edited 'len(dayE.get()) >=2` and it returns False. You need to change the validation function and having two, one each for 2 and 4 characters, will probably be easier. The @stovfl link explains how to do this using the %P parameter. – Tls Chris Mar 28 '20 at 14:56

1 Answers1

1

Question: Entry validation, to limit the character length and only allow numbers

This example implements:

  • Allow only input of digits
  • Limit number of digits according to 'DD', 'MM', 'YYYY'
  • Auto '<Tab>' according the number of digits
  • Allow leading zeros in 'DD', 'MM'
  • Allow only ranges of numbers, 1, 31, 1, 12 and argument years=(<from>, <to>)
  • Returns the edited data as: {'month': 3, 'day': 28, 'year': 2020}

Reference:

  • VALIDATION

    Validation works by setting the validatecommand= option to a script (validateCommand) which will be evaluated according to the validate= option.
    Explanation of the used percent substitutions ( '%W', '%V', '%v', '%P', '%S')

  • SO:interactively-validating-entry-widget-content-in-tkinter

    Be aware of the note there:
    Note: it's important that the validation command returns either True or False. Anything else will cause the validation to be turned off.


Note:
The validation get turned OFF, if the Entry text get changed, either using .insert(..., .delete(... or <textvariable>.set(...).


import tkinter as tk


class DateEntry(tk.Frame):
    def __init__(self, parent, **kwargs):
        years = kwargs.pop('years', (1900, 9999))
        super().__init__(parent, **kwargs)
        
        vcmd = (self.register(self._validate), '%W', '%V', '%v', '%P', '%S')

        for name, text, v1, v2 in (('day', 'DD', 1, 31),
                                   ('month', 'MM', 1, 12),
                                   ('year', 'YYYY', years[0], years[1])):
            e = tk.Entry(self, name=name, width=len(text) + 1, justify="center")
            e.pack(side=tk.LEFT)
            e.insert(0, text)
            e._valid = (len(text), v1, v2)
            e.config(validate="all", validatecommand=vcmd)
            
    def get(self):
        data = {}
        for entry in [self.nametowidget(child) for child in self.children]:
            text = entry.get()
            data[entry.winfo_name()] = int(text) if text.isdigit() else None
        return data

    def _validate(self, widget, cmd, validate, value, text):
        # get this entry reference
        w = self.nametowidget(widget)

        # Clear entry or do nothing
        if cmd in ('focusin', 'forced') or value == '':
            if not value.isdigit():
                w.delete(0, tk.END)
                # Set the 'validate' option again after edit
                w.after_idle(w.config, {'validate': validate})
            return True

        # process key
        elif cmd == 'key' and value.isdigit():
            # get from this entry the valid parameter
            l, v1, v2 = w._valid
    
            # get the startswith chars if YYYY
            if v1 > 1 and len(value) < l:
                l2 = len(value)
                v1, v2 = int(str(v1)[:l2]), int(str(v2)[:l2])

            # allow leading zero in DD / MM
            elif v1 == 1 and len(value) == 1 and int(value) == 0:
                return True

            # return True if all valid else False
            valid = all((text.isdigit(), v1 <= int(value) <= v2, len(value) <= l))
            if valid and len(value) == l:
                self.event_generate('<Tab>', when='tail')

            return valid

        # else return False
        return False

Usage:

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        tk.Button(self, text='print date', command=self.print_date).grid()
        
        self.date_entry = DateEntry(self, years=(2000, 2020))
        self.date_entry.grid()
        
    def print_date(self):
        print('print_date: {}'.format(self.date_entry.get()))
        # >>> print_date: {'month': 3, 'day': 28, 'year': 2020}


if __name__ == "__main__":
    App().mainloop()

Tested with Python: 3.5 - 'TclVersion': 8.6 'TkVersion': 8.6

stovfl
  • 14,998
  • 7
  • 24
  • 51