2

I am working on a calculator and I have encountered a problem regarding my key-binds. At the end of the initiation of my GUI class, a message box is created that explains the binds to the user.

messagebox.showinfo("Guide", "Key bindings include: 1, 2, 3, 4, 5, 6, 7, 8, 9, 
0, ., +, -, *, /, (, ), Enter, Backspace, Insert and Delete.")

Ironically, this causes all of the binds to not respond until the message box is closed and the tkinter window is deselected then reselected. The binds are coded like this:

master.bind('<Delete>', self.delete)
master.bind('<BackSpace>', self.back)
master.bind('<Return>', self.equals)
master.bind('<Insert>', self.add_answer)

I have attempted to use focus_set() but it does not help. What can I do to make my keyboard binds respond immediately after the message box is closed?

Here is the entirety of my code for context.

from tkinter import *
from tkinter.ttk import *
from tkinter import messagebox  # added by @martineau

class Logic:
    def add_digit(self, *args):
        if type(args[0]) == str:
            self.expression.append(args[0])
        else:
            self.expression.append(args[0].char)
        self.labels[1].config(text="".join(self.expression))

    def add_operation(self, *args):
        if type(args[0]) == str:
            self.expression.append(args[0])
        else:
            self.expression.append(args[0].char)
        self.labels[1].config(text="".join(self.expression))

    def add_answer(self, *args):
        self.expression.extend(list(self.labels[0]['text']))
        self.labels[1].config(text="".join(self.expression))

    def delete(self, *args):
        if self.expression:
            self.expression = list()
            self.labels[1].config(text="".join(self.expression))
        else:
            self.labels[0].config(text="")

    def back(self, *args):
        self.expression = self.expression[:-1]
        self.labels[1].config(text="".join(self.expression))

    def equals(self, *args):
        equation = list()
        number = list()

        if not self.expression:
            self.labels[0].config(text='')

        for value in self.expression:
            if value in self.numpad_texts:
                number.append(value)
            else:
                if number:
                    equation.append(str(float("".join(number))))
                    number = list()
                equation.append(value)

        if number:
            try:
                equation.append(str(float("".join(number))))
            except ValueError:
                messagebox.showerror("Error", "Syntax error: Your expression has incorrect syntax")

        for i in range(len(equation)):
            if equation[i] == '(' and i != 0:
                if equation[i-1] not in self.operation_texts:
                    equation.insert(i, '*')
            elif equation[i] == ')' and i != len(equation)-1:
                if equation[i+1] not in self.operation_texts:
                    equation.insert(i+1, '*')

        if equation:
            try:
                self.labels[0].config(text=str(eval(''.join(equation))))
            except ZeroDivisionError:
                messagebox.showerror("Error", "Zero division error: Your expression has a division by zero")
            except SyntaxError:
                messagebox.showerror("Error", "Syntax error: Your expression has incorrect syntax")

class GUI(Logic):
    numpad_texts = ('7', '8', '9', '4', '5', '6', '1', '2', '3', '0', '.', 'Equals')
    operation_texts = ('/', '*', '-', '+', '(', ')')
    function_texts = ('Delete', 'Back')

    def __init__(self, master):
        master.title('Calculator')

        self.expression = list()

        self.label_frame = Frame(master)
        self.label_frame.grid(columnspan=2)

        self.labels = list()

        for i in range(2):
            self.labels.append(Label(self.label_frame))
            self.labels[i].grid(row=i, column=0, columnspan=4)

        self.labels[0].bind("<Button-1>", self.add_answer)

        self.numpad_frame = Frame(master)
        self.numpad_frame.grid(row=1, rowspan=2)

        self.numpad_buttons = list()

        for i in range(len(self.numpad_texts)):
            self.numpad_buttons.append(Button(self.numpad_frame, text=self.numpad_texts[i], command=lambda i=i: self.add_digit(self.numpad_texts[i])))
            self.numpad_buttons[i].grid(row=i//3, column=i%3)
            if self.numpad_texts != 11:
                master.bind(self.numpad_texts[i], self.add_digit)

        self.numpad_buttons[-1].config(command=self.equals)

        self.operations_frame = Frame(master)
        self.operations_frame.grid(row=1, column=1)

        self.operation_buttons = list()

        for i in range(len(self.operation_texts)):
            self.operation_buttons.append(Button(self.operations_frame, text=self.operation_texts[i], command=lambda i=i: self.add_operation(self.operation_texts[i])))
            self.operation_buttons[i].grid(row=i//2, column=i%2)
            master.bind(self.operation_texts[i], self.add_operation)

        self.functions_frame = Frame(master)
        self.functions_frame.grid(row=2, column=1)

        self.function_buttons = list()

        for i in range(len(self.function_texts)):
            self.function_buttons.append(Button(self.functions_frame, text=self.function_texts[i]))
            self.function_buttons[i].grid(row = 0, column=i%2)

        self.function_buttons[0].config(command=self.delete)
        self.function_buttons[1].config(command=self.back)

        master.bind('<Delete>', self.delete)
        master.bind('<BackSpace>', self.back)
        master.bind('<Return>', self.equals)
        master.bind('<Insert>', self.add_answer)

        messagebox.showinfo("Guide", "Key bindings include: 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, ., +, -, *, /, (, ), Enter, Backspace, Insert and Delete.")

if __name__ == '__main__':
    root = Tk()
    calculator = GUI(root)
    root.mainloop()
martineau
  • 119,623
  • 25
  • 170
  • 301
R. Oliver
  • 29
  • 3
  • The code you posted doesn't run (`NameError: name 'messagebox' is not defined`) – Bryan Oakley Jul 24 '17 at 21:55
  • It all works fine for me when I fix the missing import. What platform are you running on? – Bryan Oakley Jul 24 '17 at 21:57
  • @Bryan: It doesn't work for me on Win 7 even with the missing `import` added. The bound keys don't work (even if you manually bring the keyboard window into focus). @Sierra Mountain Tech's answer fixes that (and nothing else). – martineau Jul 25 '17 at 00:32
  • @R. Oliver: Key-binding sends only the event to the function specified as an argument and no others. These are called "event handlers" and are different from the `command=` callback-type functions many widgets support. Those functions receive no arguments. I think (another) problem with your code is that you're attempting to use many of `class Logic` methods as both `command`-type callbacks _and_ event handlers. It can be done, but you're not doing it. – martineau Jul 25 '17 at 00:54
  • @martineau: your explanation of event handling isn't quite right. Since the events are bound to the root window, they will be in effect whenever any widget in the main window has focus. It's too complicated to explain in a comment box, but bindings on root and toplevel windows are somewhat special. The problem is simply one of windows losing the focus. – Bryan Oakley Jul 25 '17 at 02:34
  • @Bryan: Thanks for the info. However my comment wasn't so much about event-binding precedence. The point of it was that event-handler functions and widget `command=` callbacks have two different calling signatures, yet the OP is trying to use a single method for both purposes (and not working around the differences correctly). – martineau Jul 25 '17 at 04:21

2 Answers2

4

Tkinters messagebox needs to be imported separately:

from tkinter import messagebox

Then add the following underneath the line for your bindings messagebox:

master.focus_force()

This will move the focus back to the root window after the user closes the messagebox and all your bindings will continue to work.

Mike - SMT
  • 14,784
  • 4
  • 35
  • 79
  • 1
    Well, `master.focus_force()` is enough though. No need to change all to `self.master`. Also, about messagebox, it is not on your end. That's how it works. Following answer, explains it. https://stackoverflow.com/questions/24738104/python-tkinter-8-5-import-messagebox – Lafexlos Jul 24 '17 at 22:02
  • I thought it would be an issue using the master argument with first defining it as a class attribute. I guess sense it is only being called once it's not an issue. – Mike - SMT Jul 24 '17 at 22:07
  • I think this answer would be better if you focused more on what was necessary to make the code work. Changing master to self.master is completely unrelated to the problem and makes it harder to see what the significant changes are. – Bryan Oakley Jul 24 '17 at 22:19
  • @BryanOakley: I have updated my code to reflect your suggestion. Thanks for the feedback. – Mike - SMT Jul 24 '17 at 23:18
  • @Lafexlos: I have updated my answer to be more to the point on this question. Thanks for the feedback. – Mike - SMT Jul 24 '17 at 23:18
  • `master.focus_force()` needs to be in every example for `messagebox`, but it doesn't seem to be anywhere but here (well, I will stop looking now, so I don't really know)! – nmz787 Oct 10 '20 at 09:24
0

I'm posting here for I don't have privilege to comment yet.. @R Oliver, I had a similar problem with my entry widget.. The following solution, provided by Brian Oakley as a comment in this question, worked for me, using after_idle() and having the message as a function. Try this: create some function for the info

def message():
    messagebox.showinfo("Guide", "Key bindings include: 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, ., +, -, *, /, (, ), Enter, Backspace, Insert and Delete.")

and then, in main, use the after_idle

if __name__ == '__main__':
    root = Tk()
    calculator = GUI(root)
    root.after_idle(message)
    root.mainloop()
Suri
  • 25
  • 9