How to set focus with multiple class instances for key binding python tkinter

Question:

What am I doing wrong in python focus for binding?

I’m trying to bind the button widget to keyboard enter and nothing happens, or the focus ends on the last instance of the class to be loaded.

I added self.bind('<Return>', lambda e: self.convert) to the base code after the button.nothing.
Tried container.bind('<Return>', lambda e: self.convert) and nothing.
I’m at a loss as to why the code isn’t binding the key event other than the focus is ending up in the wrong location (focus always ends up on the last instance of the class).
This is the code I’m working from https://github.com/photodude/Tkconverter

Link in the readme to the source non-OOP tutorial and all files to the source tutorial before I modified the code to be more OOP

How do I get the focus on the "active" user visible instance of the class?

Minimal, Reproducible Example
a link to the code is provided above.

  1. Run the python code from the App.py file
  2. In the GUI select "F to C"
  3. enter a numeric value (i.e. like 98 or 100) in the entry box
  4. press the enter key.

When you are on "F to C" and you press enter you get a popup error "could not convert string to float" but when you are on "C to F" the correct operation (temperature conversion) occurs. The "error" that pops up on "F to C" is the same as when you click the "convert" button and the entry field is blank or has something other than a numeric value. This popup error occurs on "C to F" and press enter key with a blank entry or non-numeric value.
If you are on "F to C" or "C to F", enter a numeric value (i.e. like 98 or 100) in the entry box, and click the "convert" button everything works as expected.

Expected binding behavior is for the active instance visible to the user to have the active focus for the key binding like the "convert" button has.

Code posted per community request. See Github for current code and history of attempted fixes.
App.py

# source https://www.pythontutorial.net/tkinter/tkraise/

import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showerror
from ControlFrame import ControlFrame

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

        self.title('Temperature Converter')
        self.geometry('300x120')
        self.resizable(False, False)


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

ControFrame.py

# source https://www.pythontutorial.net/tkinter/tkraise/
import tkinter as tk
from tkinter import ttk
from ConverterFrame import ConverterFrame
from TemperatureConverter import TemperatureConverter

class ControlFrame(ttk.LabelFrame):
    def __init__(self, container):

        super().__init__(container)
        self['text'] = 'Options'

        # radio buttons
        self.selected_value = tk.IntVar()

        ttk.Radiobutton(
            self,
            text='F to C',
            value=0,
            variable=self.selected_value,
            command=self.change_frame).grid(column=0, row=0, padx=5, pady=5)

        ttk.Radiobutton(
            self,
            text='C to F',
            value=1,
            variable=self.selected_value,
            command=self.change_frame).grid(column=1, row=0, padx=5, pady=5)

        self.grid(column=0, row=1, padx=5, pady=5, sticky='ew')

        # initialize frames
        self.frames = {}
        self.frames[0] = ConverterFrame(
            container,
            'Fahrenheit',
            TemperatureConverter.fahrenheit_to_celsius
            )
        self.frames[1] = ConverterFrame(
            container,
            'Celsius',
            TemperatureConverter.celsius_to_fahrenheit
            )

        self.change_frame()

    def change_frame(self):
        for frame in self.frames.values():
            frame.reset()
            frame.grid_remove()
        frame = self.frames[self.selected_value.get()]
        frame.reset()
        frame.tkraise()
        frame.grid()
        frame.focus_set()

ConverterFrame.py

# source https://www.pythontutorial.net/tkinter/tkraise/
import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showerror

class ConverterFrame(ttk.Frame):
    def __init__(self, container, unit_from, converter):
        super().__init__(container)

        self.unit_from = unit_from
        self.converter = converter

        self.master.bind('<Return>', lambda event=None: self.enter_key())

        # field options
        options = {'padx': 5, 'pady': 0}

        # temperature label
        self.temperature_label = ttk.Label(self, text=self.unit_from)
        self.temperature_label.grid(column=0, row=0, sticky='w', **options)

        # temperature entry
        self.temperature = tk.StringVar()
        self.temperature_entry = ttk.Entry(self, textvariable=self.temperature)
        self.temperature_entry.grid(column=1, row=0, sticky='w', **options)
        self.temperature_entry.focus()

        # button
        self.convert_button = ttk.Button(self, text='Convert')
        self.convert_button.grid(column=2, row=0, sticky='w', **options)
        self.convert_button.configure(command=self.convert)
        # self.convert_button.bind('<Return>', lambda event=None: self.convert)

        # result label
        self.result_label = ttk.Label(self)
        self.result_label.grid(row=1, columnspan=3, **options)

        # add padding to the frame and show it
        self.grid(column=0, row=0, padx=5, pady=5, sticky="nsew")

    def convert(self, event=None):
        """  Handle button click event
        """
        try:
            input_value = float(self.temperature.get())
            result = self.converter(input_value)
            self.result_label.config(text=result)
        except ValueError as error:
            showerror(title='Error', message=error)

    def reset(self):
        self.temperature_entry.delete(0, "end")
        self.result_label.text = ''

    def enter_key(self):
        self.convert()

TemperatureConverter.py

# source https://www.pythontutorial.net/tkinter/tkraise/
class TemperatureConverter:
    @staticmethod
    def fahrenheit_to_celsius(f, format=True):
        result = (f - 32) * 5/9
        if format:
            return f'{f} Fahrenheit = {result:.2f} Celsius'
        return result

    @staticmethod
    def celsius_to_fahrenheit(c, format=True):
        result = c * 9/5 + 32
        if format:
            return f'{c} Celsius = {result:.2f} Fahrenheit'
        return result

Tried solution from Python Tkinter: Binding Keypress Event to Active Tab in ttk.Notebook as below simplified example but was unsuccessful

class ConverterFrame(ttk.Frame):
    def __init__(...):
        ...
        tag = str(self)
        self._add_bindtag(self, tag)
        self.bind_class(tag, '<Return>', lambda event=None: self.convert())

    def _add_bindtag(self, widget, tag):
        bindtags = widget.bindtags()
        if tag not in bindtags:
            widget.bindtags((tag,) + bindtags)
        for child in widget.winfo_children():
            self._add_bindtag(child, tag)
Asked By: Walt Sorensen

||

Answers:

The solution here was to correct ControlFrame.py to initialize each instance of the frames on change_frame(). the original error in the source tutorial instantiated the ConverterFrame() class in a way that the last one would replace all previous ones inrelationship to focus and bindings.

by moving self.frames[0] = ConverterFrame(...)… into specific functions and calling those on change_frame() the frame replacement issue was corrected.

Full Modified code is on GitHub https://github.com/photodude/Tkconverter

Answered By: Walt Sorensen
Categories: questions Tags: , , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.