Search code examples
pythontkintertkinter-entry

How to compare the inputs of two entries using single tkinter function for a bunch of such pairs of entries?


I want to validate two tkinter entries. One called minimum and the other called maximum. Of course, I want to make sure that minimum does not exceed maximum. And there is a third entry called increment which has to be lesser than maximum. There are a set of 15 such entries which I am trying to validate.

I have tried using for loop and tracing the textvariable of each entry. But inside the for loop, I am able to validate only a single entry box. Also, when I skip the validation for that specific one entry called the txtCab, it throws the following exception: If I do it for all the widgets, it does work, but fails some times.

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\beejb\AppData\Local\Programs\Python\Python37-32\lib\tkinter\__init__.py", line 1705, in __call__
    return self.func(*args)
  File "C:\PROSAIL_5B_Fortran\PROSAIL_5B_FORTRAN\PROSAIL.py", line 191, in min_max
    minVar = eval("self.txtVar_"+ str(wid)+ "_min.get()")
  File "<string>", line 1, in <module>
NameError: name 'self' is not defined

The validation function I have used is:

def min_max(*args):
            alltextFields = ["N","Cab","Car","Cw","Cm","Cbrown", "rsoil0","LIDFa","LIDFb","TypeLIDF","LAI","hspot","tts","tto","psi" ]
            for wid in alltextFields:
                if eval("self." + wid + "_variable.get()"):
                    minVar = eval("self.txtVar_"+ str(wid)+ "_min.get()")
                    maxVar = eval("self.txtVar_"+ str(wid) + "_max.get()")
                    rangeVar = eval("self.txtVar_"+ str(wid) + "_range.get()")
##
##            print((minVar))
##            print((maxVar))
##            print((rangeVar))

            if len(minVar) > 0 and len(maxVar):
                if (minVar) > (maxVar):
                    messagebox.showinfo("Input Error", "Minimum should not be greater than maximum")

            if len(rangeVar) > 0 and len(maxVar) > 0:
                if (rangeVar) > (maxVar) :
                    messagebox.showinfo("Input Error", "Increment cannot exceed maximum limit")

##            print(self.txtVar_Cab_min.get()); print(self.txtVar_Cab_max.get());
##            print(self.txtVar_N_min.get()); print(self.txtVar_N_max.get());
            if len(self.txtVar_Cab_min.get()) > 0 and len(self.txtVar_Cab_max.get()) > 0 and len(self.txtVar_Cab_range.get()) > 0:
                if (self.txtVar_Cab_min.get()) > (self.txtVar_Cab_max.get()):
                    messagebox.showinfo("Input Data Error", "Minimum should not be greater than maximum!!")
                if (self.txtVar_Cab_range.get()) > (self.txtVar_Cab_max.get()):
                    messagebox.showinfo("Error", "Increment cannot exceed maximum!!")

Another validation function I have tried is:

    def validateMRM(self,value, text,W):
        vMin,vMax,vRange;
        entry = self.controller.nametowidget(W)
        print(entry)
        if entry == self.txt_N_min:
            print(entry.get())
            print(self.txtVar_N_max.get())
            print(self.txtVar_N_range.get())
        alltextFields = ["txt_N","txt_Cab","txt_Car","txt_Cab","txt_Cw","txt_Cw","txt_Cm","txt_Cbrown","txt_Cm", "txt_rsoil0",
                                    "txt_LIDFa","txt_LIDFb","txt_TypeLIDF","txt_LAI","txt_hspot","txt_hspot","txt_tts","txt_tto","txt_psi"
                                ]
        for wid in alltextFields:
            typeOfVar = wid.split("_")

            if entry == eval("self.txt_" + str(typeOfVar[1])+ "_min"):
                vMin = eval("self.txtVar_" + str(typeOfVar[1])+ "_min.get()")
                print(eval("self.txtVar_" + str(typeOfVar[1])+ "_min.get()"))
                vMax = eval("self.txtVar_" + str(typeOfVar[1])+ "_max.get()")
                print(eval("self.txtVar_" + str(typeOfVar[1])+ "_max.get()"))
                vRange = eval("self.txtVar_" + str(typeOfVar[1])+ "_range.get()")
                print(eval("self.txtVar_" + str(typeOfVar[1])+ "_range.get()"))

        print(vMin); print(vMax); print(vRange)

        if len(vMin) > 0 and len(vMax) > 0 and len(vRange) > 0:
            if (vMin) > (vMax):
                messagebox.showinfo("Error", "Minimum cannot be greater than maximum")
            if (vRange) > (vMax) :
                messagebox.showinfo("Error", "Increment cannot exceed the maximum limit")        
        print(len(entry.get()))
        if len(entry.get())>2:

And here is how all the entries are created:

 self.lbl_N = tk.Label(self,text="Structure Coefficient(N)",anchor="w",width=40,bg='white'); self.lbl_N.grid(row=3,column=4,padx=4,pady=4);
        self.N_variable = tk.BooleanVar()
        self.chk_N = tk.Checkbutton(self,variable=self.N_variable, command=lambda:self.show_hide()); self.chk_N.grid(row=3,column=6,padx=4,pady=4);
        self.txt_N = tk.Entry(self,width=10,validate = 'key', validatecommand = vcmd); self.txt_N.grid(row=3,column=7,padx=4,pady=4);

        self.txtVar_N_min = tk.StringVar(); self.txtVar_N_max = tk.StringVar(); self.txtVar_N_range = tk.StringVar();
        self.txtVar_N_min.trace("w", min_max); self.txtVar_N_max.trace("w", min_max); self.txtVar_N_range.trace("w", min_max);

        self.txt_N_min = tk.Entry(self,width=5,validate = 'key',textvariable=self.txtVar_N_min, validatecommand = vcmd_min_max);
        self.txt_N_max = tk.Entry(self,width=5,validate = 'key', textvariable=self.txtVar_N_max,validatecommand = vcmd_min_max);
        self.txt_N_range = tk.Entry(self,width=5,validate = 'key', textvariable=self.txtVar_N_range,validatecommand = vcmd_min_max); 

There are a set of fourteen such entries and I need to validate each of them.

But none of this gives the actual output I want. It works some time and fails some other times. I am not sure why is that happening and I have spent a hell of time with this validation.


Solution

  • I'm not sure whether this answers your question but it should point you in the right direction.

    I couldn't make much sense of your code. I've produced a 15 row x 4 column grid. The 4th column is a message that the 3 fields next to it are 'OK' or if not indicate the problem. The validation is run on the whole grid for each keypress. If this is too slow a validate button could launch the validation instead.

    import tkinter as tk
    from tkinter import ttk
    
    def rec(): return {'lo': 0, 'hi': 0, 'step': 0, 'ok': '' }
    
    root = tk.Tk()
    root.title('SO Question')
    
    def entry(id, ent_dict, var_dict, v=0):
        """ Add an Entry Widget to the root, with associated StringVar."""
        var_dict[id] = tk.StringVar()
        var_dict[id].set(str(v))
        ent_dict[id] = ttk.Entry(root, textvariable= var_dict[id], width = 10 )
        return ent_dict[id]
    
    def do_validate(lo, hi, step):
        """ Return OK if lo, hi and step are consistent else an error string. """
        if lo < hi and step < hi: return 'OK'
        txt = ''
        if lo >= hi: 
            txt = 'lo >= hi. ' 
        if step >= hi:
            txt += 'step >= hi.'
        return txt 
    
    def conv(txt):
        """ Convert text to float.  Return 0.0 if not valid float e.g "" or 'a' """
        try: 
            return float(txt)
        except ValueError:
            return 0.0 
    
    def oklabel(ent_dict, var_dict):
        """ Add an OK Label to a row. """
        lo = conv(var_dict['lo'].get())
        hi = conv(var_dict['hi'].get())
        step = conv(var_dict['step'].get())
        var_dict['ok'] = tk.StringVar()
        var_dict['ok'].set(do_validate(lo, hi, step))
        ent_dict['ok'] = ttk.Label(root, textvariable = var_dict['ok'], width = -17)
        return ent_dict['ok']  # Return the Label object for gridding. 
    
    def do_check(*args):
        """ Loop through the rows setting the validation string in each one. """ 
        for var_dict in stringvars:
            lo = conv(var_dict['lo'].get())
            hi = conv(var_dict['hi'].get())
            step = conv(var_dict['step'].get())
            var_dict['ok'].set(do_validate(lo, hi, step))
    
    # Add column labels
    ttk.Label(root, text='Minimums').grid(row=0, column=0)
    ttk.Label(root, text =' Maximums').grid(row=0, column=1)
    ttk.Label(root, text='Increment').grid(row=0, column=2)
    ttk.Label(root, text='Valid').grid(row=0, column=3)
    
    # Create containers for he Entries and Stringvars
    entries =[]
    stringvars = []
    
    # Add 15 rows of Entries / Validation Labels to the UI. 
    for row in range(1, 16):
        tempe=rec()
        tempv=rec()       
        entry('lo', tempe, tempv, 0).grid(row = row, column=0)
        entry('hi', tempe, tempv, 0).grid(row = row, column=1)
        entry('step', tempe, tempv, 0).grid(row = row, column=2)
        oklabel(tempe, tempv).grid(row = row, column = 3)
        entries.append(tempe)
        stringvars.append(tempv)
    
    # Bind do_check to all Entry widgets.
    root.bind_class('TEntry', '<KeyPress>', do_check, add='+')
    root.bind_class('TEntry', '<BackSpace>', do_check, add='+')
    root.bind_class('TEntry', '<Delete>', do_check, add='+')
    
    root.mainloop()
    

    In the past I've got stuck trying to validate multiple fields by not allowing inconsistent entries. It is difficult for users to follow what is required to correct fields. They have to work in the correct order. e.g. lo = 100, hi = 9, and step = 1. Should the UI allow the last zero in 100 to be deleted, leaving 10 which is gt 9?

    This could be extended to activate a 'Next' button only if all rows are OK.

    Edit 1 - Response to Comment

    This has a function to create and activate each row of the display. Each row has it's own variables and checking function. They are triggered by the trace on the three Entry StringVars, there's no need to use validate.

    import tkinter as tk
    from tkinter import ttk
    
    def to_float(txt):
        """ Safely convert any string to a float.  Invalid strings return 0.0 """ 
        try:
            return float(txt)
        except ValueError: 
            return 0.0
    
    def row_n( parent, n, init_show = 0 ):
        """ Create one row of the display. """
        # tk.Variables
        v_show = tk.IntVar()
        v_min = tk.StringVar()
        v_max = tk.StringVar()
        v_incr = tk.StringVar()
        v_message = tk.StringVar()
    
        # Initialise variables
        v_min.set('0')
        v_max.set('1')
        v_incr.set('1')  # Can the increment be zero?
        v_show.set(init_show)
        v_message.set("OK")
    
        def do_trace(*args):
            """ Runs every time any of the three Entries change value. 
                Sets the message to the appropriate text.
            """
            lo = to_float(v_min.get())
            hi = to_float(v_max.get())
            inc = to_float(v_incr.get())
            if lo < hi and inc <=hi:
                v_message.set('OK')
            else:
                txt = ''
                if lo >= hi: 
                    txt += 'Min >= Max'
                if inc > hi:
                    if len(txt): txt += ' & '
                    txt += 'Incr > Max'
                v_message.set(txt)
    
        # Set trace callback for changes to the three StringVars
        v_min.trace('w', do_trace)
        v_max.trace('w', do_trace)
        v_incr.trace('w', do_trace)
    
        def activation(*args):
            """ Runs when the tickbox changes state """
            if v_show.get():
                e_min.grid(row = n, column = 1)
                e_max.grid(row = n, column = 2)
                e_inc.grid(row = n, column = 3)
                message.grid(row = n, column = 4)
            else:
                e_min.grid_remove()
                e_max.grid_remove()
                e_inc.grid_remove()
                message.grid_remove()
    
        tk.Checkbutton(parent, 
            text = 'Structure Coefficient {} :'.format(n), 
            variable = v_show, command = activation ).grid(row = n, column = 0)
        e_min = tk.Entry(parent, width=5, textvariable = v_min)
        e_max =tk.Entry(parent, width=5, textvariable = v_max)
        e_inc = tk.Entry(parent, width=5, textvariable = v_incr)
        message = tk.Label(parent, width=-15, textvariable = v_message)
        activation()
    
        return { 'Min': v_min, 'Max': v_max, 'Inc': v_incr }
    
    def show_results():
        print('Min   Max  Inc')
        for row in rows:
            res = '{}   {}  {}'.format(row['Min'].get(), row['Max'].get(), row['Inc'].get())
            print( res )
    
    root = tk.Tk()
    root.title('SO Question')
    
    ttk.Label(root, text='Minimums').grid(row=0, column=1)
    ttk.Label(root, text =' Maximums').grid(row=0, column=2)
    ttk.Label(root, text='Step', width = 5 ).grid(row=0, column=3)
    ttk.Label(root, text='Valid', width = 15 ).grid(row=0, column=4)
    
    rows = []
    for r in range(1,16):
        rows.append(row_n(root, r, init_show=r%3 == 0 ))
    
    tk.Button(root, command=show_results, text = '  Show Results  ').grid(column=1, pady = 5)
    
    root.mainloop()
    

    This is another approach. Does this help.