Search code examples
pythonuser-interfacetkintertkinter-entrytkinter-button

How can I reuse logic to handle a keypress and a button click in Python's tkinter GUI?


I have this code:

from tkinter import *
import tkinter as tk

class App(tk.Frame):
    def __init__(self, master):
        def print_test(self):
            print('test')

        def button_click():
            print_test()

        super().__init__(master)
        master.geometry("250x100")
        entry = Entry()

        test = DoubleVar()
        entry["textvariable"] = test
        entry.bind('<Key-Return>', print_test)
        entry.pack()
        button = Button(root, text="Click here", command=button_click)
        button.pack()

root = tk.Tk()
myapp = App(root)
myapp.mainloop()

A click on the button throws:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\tkinter\__init__.py", line 1921, in __call__
    return self.func(*args)
  File "[somefilepath]", line 10, in button_click
    print_test()
TypeError: App.__init__.<locals>.print_test() missing 1 required positional argument: 'self'

While pressing Enter while in the Entry widget works, it prints: test

See:

enter image description here

Now if I drop the (self) from def print_test(self):, as TypeError: button_click() missing 1 required positional argument: 'self' shows, the button works, but pressing Enter in the Entry widget does not trigger the command but throws another exception:

enter image description here

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\tkinter\__init__.py", line 1921, in __call__
    return self.func(*args)
TypeError: App.__init__.<locals>.print_test() takes 0 positional arguments but 1 was given

How can I write the code so that both the button click event and pressing Enter will trigger the print command?


Solution

  • Commands callbacks for button clicks are called without arguments, because there is no more information that is relevant: the point of a button is that there's only one "way" to click it.

    However, key presses are events, and as such, callbacks for key-binds are passed an argument that represents the event (not anything to do with the context in which the callback was written).

    For key-press handlers, it's usually not necessary to consider any information from the event. As such, the callback can simply default this parameter to None, and then ignore it:

    def print_test(event=None):
        print('test')
    

    Now this can be used directly as a handler for both the key-bind and the button press. Note that this works perfectly well as a top-level function, even outside of the App class, because the code uses no functionality from App.

    Another way is to reverse the delegation logic. In the original code, the button handler tries to delegate to the key-press handler, but cannot because it does not have an event object to pass. While it would work to pass None or some other useless object (since the key-press handler does not actually care about the event), this is a bit ugly. A better way is to delegate the other way around: have the key-press handler discard the event that was passed to it, as it delegates to the button handler (which performs a hard-coded action).

    Thus:

    from tkinter import *
    import tkinter as tk
    
    def print_test():
        print('test')
    
    def enter_pressed(event):
        print_test()
    
    class App(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.geometry("250x100")
    
            entry = Entry()
            test = DoubleVar()
            entry["textvariable"] = test
            entry.bind('<Key-Return>', enter_pressed)
            entry.pack()
    
            button = Button(root, text="Click here", command=print_test)
            button.pack()
    
    
    root = tk.Tk()
    myapp = App(root)
    myapp.mainloop()