How To Add Bind Key To Widget When Callback Method Is Not User Defined Method

Hello,

I have a test code that contains two classes. Each class corresponds to a different window. The first class ‘App’ is the main window. The ‘UserForm’ class corresponds to the second window. The second window opens up after the ‘Create user’ button is pressed in the first window. I have gone ahead and added the ‘Return’ bind option key so that a I can enable it by pressing the Enter key
(in addition to enabling it with the left-side mouse click). I would like to do the same for the ‘Submit’ button in the other window but appears to not be accepting the same set up. Note that the callback method for the Submit button is: self.destroy, and not an actual developer defined method. This is where the issue appears to lie.

Does someone know how to set up a bind key when the callback method is not a developer defined method but is an internal method?

Any help would be appreciated.

Here is the code:

import tkinter as tk
from collections import namedtuple

User = namedtuple("User", ["username", "password", "user_type"])

class UserForm(tk.Toplevel): # Secondary window
    
    def __init__(self, parent, user_type):
        
        super().__init__(parent)

        tk.Tk.title(self,'Login')        # Window title      

        # Create two attributes of type 'StringVar()
        self.username = tk.StringVar()
        self.password = tk.StringVar()
        
        self.user_type = user_type
        label = tk.Label(self, text = "Create a new " + user_type.lower(), font = 'Helvetica 9 bold')
        entry_name = tk.Entry(self, textvariable = self.username)
        entry_pass = tk.Entry(self, textvariable = self.password, show = "*")
        btn = tk.Button(self, text = "Submit", bg = 'light blue', command = self.destroy)        

        entry_name.focus_set()  # Put cursor in 'Username' cell entry at start up
        btn.bind("<Return>")    # Add 'Enter' key as additional input to button

        tk.Label(self, text = "Username:").grid(row = 1, column = 0)
        tk.Label(self, text = "Password:").grid(row = 2, column = 0)

        # Display widgets
        label.grid(row = 0, columnspan = 2, pady = 10)
        entry_name.grid(row = 1, column = 1, padx = 15)
        entry_pass.grid(row = 2, column = 1, padx = 15)
        btn.grid(row = 3, pady = 10, columnspan = 2)     
        
    def open(self):
        
        self.grab_set()
        self.wait_window()
        
        username = self.username.get()
        password = self.password.get()
        
        return User(username, password, self.user_type)

class App(tk.Tk): # main window
    
    def __init__(self):
        
        super().__init__()

        tk.Tk.title(self,'Selection')    # Window title
        tk.Tk.geometry(self, '220x170')  # Window size         

        # Tuple of strings - RadioButton labels
        user_types = ("Administrator", "Supervisor", "Regular user")

        # Create attribute of type StringVar()
        self.user_type = tk.StringVar(self, user_types[0])

        # Create label and display it
        label = tk.Label(self, text = "Select user type:", font = 'Helvetica 9 bold')
        label.pack(padx = 10, pady = 10)

        # List comprehension to create radio click selection buttons
        radios = [tk.Radiobutton(self, text = t, value = t, variable = self.user_type) for t in user_types]

        # Display (pack) and configure the alignment of the radio click buttons
        for radio in radios:
            
            radio.pack(padx = 10, anchor = tk.W)        

        # Create button and display it
        btn = tk.Button(self, text = "Create user", bg = 'light blue', command = self.open_window)
        btn.bind("<Return>", self.open_window) # Add 'Enter' key as additional input 
        btn.pack(pady = 10)           
        
    def open_window(self, *args):
        
        window = UserForm(self, self.user_type.get())
        user = window.open()
        print(user)

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


The problem is that bind() expects a function which will be passed a single argument, an Event object with additional info. destroy() takes no parameters, so that doesn’t work. What you’ll need to do is define a function that wraps destroy and accepts the additional parameter:

def on_submit(event):
    self.destroy()
btn.bind("<Return>", on_submit)

Or by using lambda to do the exact same thing inline:

btn.bind("<Return>", lambda event: self.destroy())

As an aside, I’d suggest you probably don’t actually want to use destroy() - instead, use withdraw() to hide the window, allowing you to reuse it if the user triggers this dialog box again.

1 Like

Hi,

thank you for your suggestion. Works as advertised! :wink:

Can you please elaborate on the ‘withdraw()’ method. When I selected the ‘Submit’ button, the secondary window closed just as before with the ‘destroy()’ method. What is going on in the background that is different between the two?

The difference is that destroy(), well, destroys the window along with all their children, so it no longer exists. You’ll have to re-create all those widgets. withdraw() on the other hand just hides the window, but it still exists so you can easily call deiconify() to bring it back if you want to use it again. Maybe not as useful for a login dialog, but for things that you expect the user to open and close a bunch, it’s more efficient.

Thank you for the clarification. This is in line with the following thread.

Difference between iconify() and withdraw() in Python Tkinter - Stack Overflow

Sometimes when you read it from more than one source, it helps with the understanding!

Thank you again.

Much appreciated! :+1:

******************** UPDATE ****************************
Ok, after additional testing, with the ‘withdraw()’ method, the main window will not close
after clicking on the ‘x’ on the top right corner. With the ‘destroy()’ method it will. I now see why the ‘destroy()’ method was used in the test code.

To handle the close button, you have to use a specific command to bind an event handler: window.wm_protocol("WM_DELETE_WINDOW", function_to_call)

Thank you. For now, to keep it simple, I will just use destroy() for now.

Hello,

I have a related issue. How do you add a bind key to a Radiobutton? I did a few searches and came upon this thread:

python - [Tkinter]Binding Keyboard Keys to a Radiobutton - Stack Overflow

I attempted running the ‘solution’ on the code below. Lo and behold … it didn’t work.

Here is the test code:

import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showinfo
from tkinter import *

# root window
root = tk.Tk()
root.geometry('300x220')
root.resizable(False, False)
root.title('Radio Button Demo')


def show_selected_size(*args):
    showinfo(
        title = 'Result',
        message = selected_size.get()
    )

def tabSelect(event = '<Return>'):#*args):

    print('<Return> key recognized')        
    

selected_size = tk.StringVar()

sizes = (('Small', 'S'),
         ('Medium', 'M'),
         ('Large', 'L'),
         ('Extra Large', 'XL'),
         ('Extra Extra Large', 'XXL'))

selected_size.set(sizes[2][1]) # 'L' -> Set default setting

# Label
label = ttk.Label(text = "What's your t-shirt size?")
label.pack(fill = 'x', padx = 5, pady = 5)
    
# Radiobuttons
for size in sizes:
    
    r = ttk.Radiobutton(
        root,
        text = size[0],
        value = size[1],
        variable = selected_size,
        command = tabSelect
    )
    r.pack(fill = 'x', padx = 5, pady = 5)

# r.bind_all("<Return>", tabSelect)   
r.bind("<Return>", tabSelect) 

# button
button = ttk.Button(
    root,
    text = "Get Selected Size",
    command = show_selected_size)

button.bind("<Return>", show_selected_size)
button.pack(fill = 'x', padx = 5, pady = 5)


root.mainloop()


I have updated the code such that the bind key is attached. Note that it only works after the last Radiobutton is selected via the tab and Enter key combination. If I instead use the .bind_all() method, it will work for all Radiobuttons in addition to the ‘regular’ button.

Does someone know how to attach bind keys to Radiobuttons? The objective is so that in addition to the mouse click for radiobutton selection, I would also like to make the selection via the enter/return key. That is, press/toggle the ‘Tab’ key to hop along the different buttons and press the ‘Enter’ key when making the selection.

From the following website: Tkinter Radiobutton (tutorialspoint.com)

it states that if you call the invoke() method, it will act just like clicking the mouse pointer. This did not work. The way that I tested it it, is that I put this code inside the tab_select() method. As is:

def tabSelect(event = '<Return>'):#*args):
    r.invoke()
    print('<Return> key recognized')  

If it does not work like this, then how does the ‘invoke()’ method work?

Hello,

for those following this thread, I believe I have a potential solution. In order to get the binding feature that I desired (changing the radiobutton selected with the ‘Tab’ / ‘Enter’ keystroke combination pair), I needed to create the radiobuttons independently as opposed to creating them via a for loop with the same widget variable name. I also created independent callback methods corresponding to each individual radiobutton. Inside each callback method, I updated the radiobutton selected.

Here is the sample code:

import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showinfo
from tkinter import *

# root window
root = tk.Tk()
root.geometry('300x220')
root.resizable(False, False)
root.title('Radio Button Demo')

# Pop up message
def show_selected_size(*args):
    showinfo(
        title = 'Result',
        message = selected_size.get()
    )

# Callback methods
def tabSelect1(*args):
    selected_size.set('S') 
def tabSelect2(*args):
    selected_size.set('M')
def tabSelect3(*args):
    selected_size.set('L')
def tabSelect4(*args):
    selected_size.set('XL')
def tabSelect5(*args):
    selected_size.set('XXL')     

# Create widget variable    
selected_size = tk.StringVar()
selected_size.set('L') # 'L' -> Set default setting

# Create Label
label = ttk.Label(text = "What's your t-shirt size?")
label.pack(fill = 'x', padx = 5, pady = 5)

# Create Radiobuttons
r1 = ttk.Radiobutton(root, text = 'Small', value = 'S', variable = selected_size, command = tabSelect1)
r2 = ttk.Radiobutton(root, text = 'Medium', value = 'M', variable = selected_size, command = tabSelect2)
r3 = ttk.Radiobutton(root, text = 'Large', value = 'L', variable = selected_size, command = tabSelect3)
r4 = ttk.Radiobutton(root, text = 'Extra Large', value = 'XL', variable = selected_size, command = tabSelect4)
r5 = ttk.Radiobutton(root, text = 'Extra Extra Large', value = 'XXL', variable = selected_size, command = tabSelect5)

# Radiobutton placements
r1.pack(fill = 'x', padx = 5, pady = 5)
r2.pack(fill = 'x', padx = 5, pady = 5)
r3.pack(fill = 'x', padx = 5, pady = 5)
r4.pack(fill = 'x', padx = 5, pady = 5)
r5.pack(fill = 'x', padx = 5, pady = 5)

# Radiobutton callback method bindings
r1.bind("<Return>", tabSelect1)
r2.bind("<Return>", tabSelect2)
r3.bind("<Return>", tabSelect3)
r4.bind("<Return>", tabSelect4)
r5.bind("<Return>", tabSelect5)

# Create button
button = ttk.Button(root, text = "Get Selected Size", command = show_selected_size)

button.bind("<Return>", show_selected_size)
button.pack(fill = 'x', padx = 5, pady = 5)

root.mainloop()

Hope it helps someone in a similar ‘bind’.