Home > Software engineering >  How can I set focus to an unknown child of the Frame widget
How can I set focus to an unknown child of the Frame widget

Time:02-15

There is a Frame which contains number of Label and Text widgets. Those are generated dynamically from JSON document.

What I want is to set the focus on the first Text widget in the Frame. The name of that Text widget depends on JSON document, so I can't just get it with Frame.children['name'].

Here's a function that makes the work. But I wonder if there's more convenient way to do so. I thought about getting the first widget in keyboard traversal hierarchy, but couldn't find any example. Please come up with any suggestion. Thanks.

def focus_to_child(event):
    for k, v in event.widget.children.items():
        if type(v) is Text:
            break
    try:
        event.widget.children[k].focus()
    except UnboundLocalError:
        pass

Minimal reproducible example. There're far less widgets in the example below than I have in my project. So a first Text widget is the next after Treeview in a keyboard traversal hierarchy. Don't let this fact confuse you =)

from random import randint as ri
from tkinter import Tk, ttk, Text

# here's example struct and values, in project MAP is initialized from JSON documents
MAP = {'_a_A': {'_id': '_a_A', 'name': chr(ri(97, 122)), 'quit_prob': 0.1, 'changed': False},
       '_a_B': {'_id': '_a_B', 'name': chr(ri(97, 122)), 'quit_prob': 0.25, 'changed': False},
       '_x_Z': {'_id': '_x_Z', 'name': chr(ri(97, 122)), 'quit_prob': 0.75, 'changed': True}}

# Treeview items selection event handle
def tv_sel_handler(event):
    try:
        # item id of the document that user has selected
        sel_iid = event.widget.selection()[0]
    except IndexError:
        sel_iid = ''
    try:
        generate_entries(MAP[sel_iid])
        # here we should set focus to the first Text in the data_entries Frame
        focus_to_child()
    except KeyError:
        pass

# this is my option at the moment, but I doubt that it's the most convenient method
def focus_to_child():
    for k, v in data_entries.children.items():
        if type(v) is Text:
            break
    try:
        data_entries.children[k].focus()
    except UnboundLocalError:
        pass

# generate Entries for selected document fields
def generate_entries(document):
    
    def focus_widget(event):
        if event.state == 8:
            event.widget.tk_focusNext().focus()
        elif event.state == 9:
            event.widget.tk_focusPrev().focus()
        return 'break'
    
    row = 0
    for field in document.keys():
        # label
        ttk.Label(data_entries, text=field).grid(row=row, column=0, sticky='nw')
        # widget to edit document fields 
        tf = Text(data_entries, name=field, wrap='word', width=20, height=2)
        tf.insert('1.0', document[field])
        tf.grid(row=row 1, sticky='nwe', padx=0, pady=5)
        tf.bind('<Tab>', focus_widget)
        tf.bind('<Shift-Tab>', focus_widget)
        tf.bind('<FocusIn>', lambda event: event.widget.tag_add('sel', '1.0', 'end'))
        # separator
        ttk.Label(data_entries).grid(row=row 2)
        row  = 3

root = Tk()
# list of documents
data_docs = ttk.Treeview(root, selectmode='browse', show=('tree',))
data_docs.column('#0', width=220)
data_docs.grid(row=0, column=0, sticky='nsw')
data_docs.bind('<<TreeviewSelect>>', tv_sel_handler)
for doc in MAP.values():
    data_docs.insert('', 'end', doc['_id'], text=doc['_id'][3:])

# frame for dynamic entries
data_entries = ttk.Frame(root, width=1000, height=450, padding=(15,0))
data_entries.grid(row=0, column=1, sticky='nsew')

root.mainloop()

CodePudding user response:

I don't know about convenient, but a more direct and efficient way to do it would be by simply remembering what the first text widget is when they're created — which is very easy to do (see the ALL CAPS COMMENTS below):

# generate Entries for selected document fields
def generate_entries(document):

    def focus_widget(event):
        if event.state == 8:
            event.widget.tk_focusNext().focus()
        elif event.state == 9:
            event.widget.tk_focusPrev().focus()
        return 'break'

    row = 0
    for field in document.keys():
        # label
        ttk.Label(data_entries, text=field).grid(row=row, column=0, sticky='nw')
        # widget to edit document fields
        tf = Text(data_entries, name=field, wrap='word', width=20, height=2)
        if row == 0:                      # FIRST TEXT?
            data_entries.first_text = tf  # REMEMBER IT.
        tf.insert('1.0', document[field])
        tf.grid(row=row 1, sticky='nwe', padx=0, pady=5)
        tf.bind('<Tab>', focus_widget)
        tf.bind('<Shift-Tab>', focus_widget)
        tf.bind('<FocusIn>', lambda event: event.widget.tag_add('sel', '1.0', 'end'))
        # separator
        ttk.Label(data_entries).grid(row=row 2)
        row  = 3

Then there will no longer by a need to search for it via a for loop:

def focus_to_child():
    try:
        data_entries.first_text.focus()
    except AttributeError:
        pass
  • Related