Question

In the code below I have managed to partially validate the data entered into the self.e2 entry widget, unfortunately if the entry widget is empty and the Submit button is pressed then a ValueError is generated" ValueError: invalid literal for int() with base 10: '' " I would like to have the program recognise that the e2 entry widget is empty, have the ValueError trapped and return focus back to the entry widget.

I have attempted to do this using the is_valid_int and invalid_int methods but this is not working.

from tkinter import *
from tkinter import ttk
from tkinter.scrolledtext import *

class DailyOrderGUI:
    def __init__(self, parent):
        #Data entring frame
        self.frame = Frame(parent, bg = "grey")
        self.frame.grid(row=0)
        self.label1 = Label(self.frame, text = "Mrs CackleBerry's Egg Ordering System", wraplength = 200, bg="grey", font=("Comic Sans MS", "14", "bold"))
        self.label1.grid(row = 0, columnspan = 3, padx = 5, pady = 5)
        self.superegg_img = PhotoImage(file = "images/superegg.gif")
        self.label5 = Label(self.frame,  bg="grey", image = self.superegg_img)
        self.label5.grid(row = 0, column= 3, padx = 5, pady = 5)
        self.v = StringVar()
        self.v.set("Monday")
        self.label2 = Label(self.frame, text = "Which day are you ordering for?", bg="grey", font=("Arial", "12", "bold"))
        self.label2.grid(row = 1, columnspan = 4, sticky = W)
        self.rb1 = Radiobutton(self.frame, variable = self.v, bg="grey", value = "Monday", text = "Monday")
        self.rb1.grid(row = 2, column = 0, sticky = W)
        self.rb2 = Radiobutton(self.frame, variable = self.v, bg="grey", value = "Tuesday", text = "Tuesday")
        self.rb2.grid(row = 2, column = 1, sticky = W)
        self.rb3 = Radiobutton(self.frame, variable = self.v, bg="grey", value = "Wednesday", text = "Wednesday")
        self.rb3.grid(row = 2, column = 2, sticky = W)
        self.rb4 = Radiobutton(self.frame, variable = self.v, bg="grey", value = "Thursday", text = "Thursday")
        self.rb4.grid(row = 2, column = 3, sticky = W)
        self.rb5 = Radiobutton(self.frame, variable = self.v, bg="grey", value = "Friday", text = "Friday")
        self.rb5.grid(row = 3, column = 0, sticky = W)
        self.rb6 = Radiobutton(self.frame, variable = self.v, bg="grey", value = "Saturday", text = "Saturday")
        self.rb6.grid(row = 3, column = 1, sticky = W)
        self.rb7 = Radiobutton(self.frame, variable = self.v, bg="grey", value = "Sunday", text = "Sunday")
        self.rb7.grid(row = 3, column = 2, sticky = W)
        self.label3 = Label(self.frame, text = "Customer's Name:?(Press \"Orders Complete\" to finish)", bg="grey", font=("Arial", "12", "bold"))
        self.label3.grid(row = 4, columnspan = 4,padx = 5, sticky = W)
        self.e1 = Entry(self.frame, width = 30)
        self.e1.grid(row = 5, columnspan = 4, sticky = W, padx = 5, pady=3)
        self.e1.focus()
        self.label4 = Label(self.frame, text = "How many eggs being ordered:?", bg="grey", font=("Arial", "12", "bold"))
        self.label4.grid(row = 6, columnspan = 4,padx = 5,sticky = W)

        integer = self.create_integer_widget()

        self.btn1 = Button(self.frame, text = "Submit")
        self.btn1.grid(row = 8, padx = 5, pady = 3, sticky = E+W)
        self.btn1.bind("<Button-1>", self.get_orders)
        self.btn2 = Button(self.frame, text = "Orders Complete", command = self.show_summary_result)
        self.btn2.grid(row = 8, column = 3, padx = 5, pady = 3, sticky = E+W)

        #Summary Frame
        self.summ_frame = Frame(parent, bg = "grey")
        self.summ_frame.grid(row=0)
        self.summ_label1 = Label(self.summ_frame, text = "Mrs CackleBerry's Egg Ordering System", bg="grey", font=("Comic Sans MS", "14", "bold"))
        self.summ_label1.grid(row = 0, columnspan = 4, padx = 5, pady = 5)
        self.scrolled_display = ScrolledText(self.summ_frame, width = 50, height = 10, bg="thistle", font=("Times New Roman", "12"))
        self.scrolled_display.grid(row = 1, columnspan = 2, padx = 5, pady = 20, sticky = W)
        self.data_entry_btn = Button(self.summ_frame, text = "Back to Data Entry", command = self.show_data_entry_frame)
        self.data_entry_btn.grid(row = 2, column = 0, sticky = SE, padx = 5, pady = 20)

        self.egg_orders=[]

        self.show_data_entry_frame()

    def create_integer_widget(self):
        self.e2 = ttk.Entry(self.frame, width = 10, validate='key')
        self.e2['validatecommand'] = (self.frame.register(self.is_valid_int), '%P')
        self.e2['invalidcommand'] = (self.frame.register(self.invalid_int), '%W')
        self.e2.grid(row = 7, columnspan = 4, sticky = W, padx = 5, pady=3)
        self.e2.bind("<Return>", self.get_orders)
        return self.e2

    def is_valid_int(self, txt):
        # txt - value in %P
        if not txt:         # do not accept empty string
            return False

        try:
            int(txt)
            return True     # accept integer

        except ValueError:  # not an integer
            return False

    def invalid_int(self, widgetName):
        # called automatically when the
        # validation command returns 'False'

        # get entry widget

        widget = self.frame.nametowidget(widgetName)

        # clear entry
        widget.delete(0, END)

        # return focus to integer entry
        widget.focus_set()
        widget.bell()

    def show_data_entry_frame(self):
        self.summ_frame.grid_remove()
        self.frame.grid()
        root.update_idletasks()

    def show_summary_result(self):
        self.frame.grid_remove()
        self.summ_frame.grid()
        root.update_idletasks()
        self.scrolled_display.delete('1.0', END)
        if len(self.egg_orders) == 0:
            self.scrolled_display.insert(END, "No Orders")
        else:
            total = 0
            self.scrolled_display.insert(END, "Orders for " + self.v.get() + "\n")
            for i in range(len(self.egg_orders)):
                total += self.egg_orders[i].num_eggs
                self.scrolled_display.insert(END, str(self.egg_orders[i]) + "\n")
            self.scrolled_display.insert(END, "" + "\n")
            self.scrolled_display.insert(END, "Summary for " + self.v.get() + "\n")
            self.scrolled_display.insert(END, "" + "\n")
            self.scrolled_display.insert(END, "Total eggs: " + str(total) + "\n")
            self.scrolled_display.insert(END, "Dozens required: " + str(self.get_dozens(total)) + "\n")
            average = 0
            if len(self.egg_orders) > 0:
                average = total / len(self.egg_orders)
            self.scrolled_display.insert(END, "Average number of eggs per customer: {0:.1f}".format(average) + "\n")

    def get_orders(self, event):
        """
        Collects order information - name, number of eggs in a loop
        """
        self.name = self.e1.get()
        self.no_eggs = self.e2.get()
        self.e1.delete(0, END)
        self.e2.delete(0, END)
        self.e1.focus()
        self.egg_orders.append(EggOrder(self.name, self.no_eggs))

    def get_dozens (self, total):
        """
        returns whole number of dozens required to meet required number of eggs
        """
        num_dozens = total//12
        if total%12 != 0:
            num_dozens += 1
        return num_dozens

class EggOrder:

    price_per_doz = 6.5
    def __init__(self, name, num_eggs):
        self.name = name
        self.num_eggs = int(num_eggs)


    def calc_price(self):
        self.price = EggOrder.price_per_doz/12 * self.num_eggs
        return self.price

    def __str__(self):
        return("{} ordered {} eggs. The price is ${:.2f}".format(self.name, self.num_eggs , self.calc_price()))



#main routine
if __name__== "__main__":
    root = Tk()
    root.title("Mrs Cackleberry's Egg Ordering Program")
    frames = DailyOrderGUI(root)
    root.mainloop()
Was it helpful?

Solution

Let's trace through what happens when you click "Submit".

First:

self.btn1 = Button(self.frame, text = "Submit")
self.btn1.grid(row = 8, padx = 5, pady = 3, sticky = E+W)
self.btn1.bind("<Button-1>", self.get_orders)

So, it calls self.get_orders. And the last line in that function is:

self.no_eggs = self.e2.get()
# ...
self.egg_orders.append(EggOrder(self.name, self.no_eggs))

And inside the EggOrder.__init__ function you've got this:

self.num_eggs = int(num_eggs)

That's presumably where the error happens.

(Note that all of that work would have been unnecessary if you'd posted the traceback instead of just the error string.)


So, when self.e2.get() returns an empty string, you end up calling int(''), and that raises a ValueError.

Unless you want to try to check for that possibility in advance (which is rarely a good idea), you will need a try:/except ValueError: somewhere. The question is, where? Well, that depends on what you want to do.

If you want to create an empty EggOrder, you'd do it inside EggOrder.__init__:

try:
    self.num_eggs = int(num_eggs)
except ValueError:
    self.num_eggs = 0

On the other hand, if you want to not create an EggOrder at all, you'd do it inside self.get_orders:

try:
    order = EggOrder(self.name, self.no_eggs)
except ValueError:
    # pop up an error message, log something, call self.invalid_int, whatever
else:
    self.egg_orders.append(order)

Of course you probably want to do this before all the destructive stuff (self.e1.delete(0, END), etc.).


Also, if you wanted to call invalid_int as-is, you'd need to pass it the name of the self.e2 widget. Since you didn't give it one, it'll be something dynamic and unpredictable like .1234567890, which you'll have to ask the widget for, just so you can look the widget back up by that name. It would be simpler to factor out the core functionality, something like this:

def invalid_int(self, widgetName):
    widget = self.frame.nametowidget(widgetName)
    self.handle_invalid_int(widget)
def handle_invalid_int(self, widget):
    widget.delete(0, END)
    # etc.

Then you can just call handle_invalid_int(self.e2).


Meanwhile, you've tried adding a validation function to check for a valid int input.

is_valid_int should work fine. However, the if not txt part is completely unnecessary—int(txt) will already handle an empty string the same way it handles a non-numeric string.

And the way you've hooked it up should work too.

However, a validation function runs on each edit to the Entry widget, not when you click some other random widget elsewhere. For example, if you try typing letters into the Entry, it should erase them as soon as you can type them. (Note that if you type 123a456 you'll end up with 456, not 123456, which may or may not be what you want…)

So, you've done that right, but it's not what you wanted to do.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top