0

I know there isn't much documentation on this but I would like to restrict entry in a tkinter entry box. Following some code I found on here I currently am doing this:

    def float_only(self,S,d):
        if d == '1': #insert
            if not S in ['.','0','1','2','3','4','5','6','7','8','9']:
                return False
        return True

and later:

        mod_box = tk.Toplevel(self.root)
        self.priceVar = tk.StringVar(mod_box)
        self.priceVar.set('0.00')
        
        vcmd = (mod_box.register(self.float_only),'%S','%d')
        priceEntry = tk.Entry(mod_box,textvariable=self.priceVar,validate='key',validatecommand=vcmd)

This works to only allow the decimal and numbers, but I'd really like if I could have it so each number press puts the inputted number in the last decimal place and moved the rest up while still restricting entry to only numbers like cash registers do. For instance if I inputted 2 then 4 then 0, entry box would show:

0.00 -> 0.02 -> 0.24 -> 2.40

then I wouldn't need to allow the decimal (so they couldn't enter multiple times) and it would just be a smoother experience. Of course I barely knew what I was doing when I got to the point I am now, so help would be appreciate greatly.

maxxslatt
  • 43
  • 6
  • Do you want the user to have the ability to use arrow keys or click to insert or delete anywhere besides the end? For example, if 2.40 is showing, should I be able to move the cursor after the 2 and insert a 0 to make 20.40? For that matter, do you want to allow the backspace key at all? – Bryan Oakley Aug 12 '20 at 19:52

1 Answers1

0

Here's my solution.

  • remove . from pricevar.get() and turn it into a list
  • get the char of the key that was pressed and check if it is numeric
  • if it is numeric, append it to the list
  • insert the decimal back into the list 2 places from the end
  • join the list without the 0 index and assign it to pricevar.set

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        
        self.pricevar = tk.StringVar()
        self.pricevar.set('0.00')
        
        dflt = dict(width=5, font='Consolas 16 bold', insertontime=0)
        self.priceEntry = tk.Entry(self, textvariable=self.pricevar, **dflt)
        self.priceEntry.grid()
        self.priceEntry.bind('<Key>', self.price)
        
    def price(self, event):
        o = list(self.pricevar.get().replace('.', ''))
        c = event.char
        if c.isnumeric():
            o.append(c)
            o.insert(-2, '.')
            self.pricevar.set(''.join(o[1:]))
        return 'break'
        
if __name__ == '__main__':
    app = App()
    app.mainloop()

If you want to turn it into it's own widget it would look something like the below. In this example I removed the StringVar and set the Entry text directly. I also included a reset button and a label. Price also has a lock on it so that once it is 'full' no more text can be entered unless it is reset. If you change the value in the constructor (ex value='00.00') it will all still work and the label will be automatically sized to fit the value

import tkinter as tk


class PriceEntry(tk.Frame):
    #focus in/out colors
    FOC = '#FFFFFF'
    FIC = '#DDDDFF'
    
    def __init__(self, master, row=0, column=0, text='undefined', value='0.00'):
        tk.Frame.__init__(self, master)
        self.grid(row=row, column=column)
        
        #price label
        tk.Label(self, text=text, font='Calibri 12 bold').grid(row=0, column=0, padx=(6, 2), pady=2)
        
        #price entry
        self.price = tk.Entry(self, background=PriceEntry.FOC, width=len(value), font='Consolas 16 bold', insertontime=0)
        self.price.grid(row=0, column=1, padx=2, pady=2)
        
        #insert start value
        self.value = value
        self.price.insert('end', value)
        
        #capture all keypresses
        self.price.bind('<Key>', self.keyHandler)
        
        #since we turned the cursor off, use a different way to indicate focus
        self.price.bind('<FocusOut>', self.indicate)
        self.price.bind('<FocusIn>',  self.indicate)
        
        #price reset button
        tk.Button(self, text=chr(10226), relief='flat', bd=0, font='none 12 bold', command=self.reset).grid(row=0, column=2)
    
    def reset(self):    
        self.price.delete(0, 'end')             #delete old text
        self.price.insert('end', self.value)    #insert init value
        
    def keyHandler(self, event):
        o = list(self.price.get().replace('.', ''))         #remove decimal and return text as a list
        if (c := event.char).isnumeric() and not int(o[0]): #if character is numeric and price isn't "full"
            o.append(c)                                         #append character to list
            o.insert(-2, '.')                                   #replace decimal
            self.price.delete(0, 'end')                         #delete old text
            self.price.insert('end', ''.join(o[1:]))            #insert new text
        return 'break'                                      #stop further propagation
        
    def indicate(self, event):
        if str(event) == '<FocusOut event>':
            self.price['background'] = PriceEntry.FOC
        elif str(event) == '<FocusIn event>':
            self.price['background'] = PriceEntry.FIC
            

class App(tk.Tk):
    WIDTH, HEIGHT, TITLE = 800, 600, 'Price Busters'
    
    def __init__(self):
        tk.Tk.__init__(self)

        #init widget at row0 column0
        self.minimum = PriceEntry(self, 0, 0, 'min') 
      
        #init widget at row0 column1 with a higher price potential
        self.maximum = PriceEntry(self, 0, 1, 'max', '000.00')

       
if __name__ == '__main__':
    app = App()
    app.title(App.TITLE)
    app.geometry(f'{App.WIDTH}x{App.HEIGHT}')
    app.resizable(width=False, height=False)
    app.mainloop()
OneMadGypsy
  • 4,640
  • 3
  • 10
  • 26