Search code examples
python-3.xtkinterfocustkinter-text

Different focus order and stacking order tkinter


In the following example, I have created several text entries, and some can expand/collapse when you focus there.

However, when some expand (specifically, the ones named as entry_2_problem_1 and entry_5_problem_2, where I have also inserted text "Here") they end "below" another text entry - I mean, because of the stacking order they are below another entry.

I could fix this by using lift() on entry_2_problem_1 and entry_5_problem_2 after the creation of the up-stacked entries which are entry_3 and entry_6, but these would change my focus order. I want a "natural" focus order, from left to right and up to down.

Below, you can see the code with some commented lines: If you uncomment those you will find that the stacking problem is substituted by a focus-order problem (as it is not truly from left to right as you will notice when using tab).

Also, consider that anything as leaving more blank spaces between widgets is discarded because of many reasons in the real code I am working on

MRE:

from tkinter import Tk, Text

def focus_next_widget(event):
    event.widget.tk_focusNext().focus()
    return("break")

class iText(Text):
    def __init__(self, stdwidth_mult=2.5, stdheight_mult=3, **kwargs):
        super(iText, self).__init__(**kwargs)

        self.stdwidth = kwargs.get('width')
        self.stdheight = kwargs.get('height')
        self.stdwidth_mult = stdwidth_mult
        self.stdheight_mult = stdheight_mult

def text_resizer(event):
    if event.widget == event.widget.focus_get():
        if not event.widget.stdheight == None:event.widget.configure(height=int(event.widget.stdheight*event.widget.stdheight_mult))
        if not event.widget.stdwidth == None: event.widget.configure(width=int(event.widget.stdwidth*event.widget.stdwidth_mult))
    else:
        if not event.widget.stdheight == None:event.widget.configure(height=event.widget.stdheight)
        if not event.widget.stdwidth == None: event.widget.configure(width=event.widget.stdwidth)

window = Tk()
window.geometry("300x300")

entry1 = iText(width=4, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
entry1.place(x=6.0, y=68.0, height=43.0)
entry1.bind("<Tab>", focus_next_widget)
entry1.bind('<FocusIn>', text_resizer)
entry1.bind('<FocusOut>', text_resizer)

# First problematic entry
entry_2_problem_1 = iText(width=4, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
entry_2_problem_1.place(x=6.0, y=116.0, height=43.0)
entry_2_problem_1.insert(1.0, 'Here')
entry_2_problem_1.bind("<Tab>", focus_next_widget)
entry_2_problem_1.bind('<FocusIn>', text_resizer)
entry_2_problem_1.bind('<FocusOut>', text_resizer)

entry_3 = iText(stdheight_mult=1, height=1, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
entry_3.place(x=70.0, y=121.0, width=102.0)
entry_3.bind("<Tab>", focus_next_widget)
entry_3.bind('<FocusIn>', text_resizer)
entry_3.bind('<FocusOut>', text_resizer)
# The following line solves the stacking problem, but creates a focus order one
# entry_2_problem_1.lift()

entry_4 = iText(width=4, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
entry_4.place(x=6.0, y=165.0, height=43.0)
entry_4.bind("<Tab>", focus_next_widget)
entry_4.bind('<FocusIn>', text_resizer)
entry_4.bind('<FocusOut>', text_resizer)

# Second problematic entry
entry_5_problem_2 = iText(width=4, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
entry_5_problem_2.place(x=6.0, y=213.0, height=43.0)
entry_5_problem_2.insert(1.0, 'Here')
entry_5_problem_2.bind("<Tab>", focus_next_widget)
entry_5_problem_2.bind('<FocusIn>', text_resizer)
entry_5_problem_2.bind('<FocusOut>', text_resizer)

entry_6 = iText(stdheight_mult=1, height=1, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
entry_6.place(x=70.0, y=218.0, width=102.0, height=34.0)
entry_6.bind("<Tab>", focus_next_widget)
entry_6.bind('<FocusIn>', text_resizer)
entry_6.bind('<FocusOut>', text_resizer)
# The following line solves the stacking problem, but creates a focus order one
# entry_8_problem_2.lift()

window.mainloop()

Also, some photos of the current and desired output, concerning the stacking problem.

Current stacking-situation (with GOOD focus order)

Desired stacking-situation (but has a BAD focus order behavior)


Solution

  • I'm unable to comment (due to rep threshold) so I couldn't ask a clarifying question, but I believe the behavior you desire is that pressing tab advances through each row of iText entry boxes in the order that you declared them, as per tkinter default behavior until you lift() an object.

    Simplest answer

    The simplest way to do this is to use .lift() when a widget is focused rather than when they are declared, so that the widget you are gaining focus on is always elevated above the rest while you use it:

    def text_resizer(event):
        if event.widget == event.widget.focus_get():
            event.widget.lift() # Always lift the current widget when it gains focus
            if not event.widget.stdheight == None:event.widget.configure(height=int(event.widget.stdheight*event.widget.stdheight_mult))
            if not event.widget.stdwidth == None: event.widget.configure(width=int(event.widget.stdwidth*event.widget.stdwidth_mult))
        else:
            if not event.widget.stdheight == None:event.widget.configure(height=event.widget.stdheight)
            if not event.widget.stdwidth == None: event.widget.configure(width=event.widget.stdwidth)
    

    In this application, where your default entry boxes do not have any sort of overlap when not expanded, this works. This also assumes that you only ever want to cycle through the elements using the event system, rather than clicking on them in a particular order.

    Whenever this isn't the case try this:

    A more dynamic approach

    However, if you have a tighter layout with many overlapping entry boxes (any widget, really), it could cause them to be visually jumbled as their relative layers change. To avoid this, you could place references to your entry box objects into an iterable, and write a function that sorts them in the order laid out in the iterable. In this example I piggybacked your focusOut callback:

    def text_resizer(event):
        if event.widget == event.widget.focus_get():
            event.widget.lift() # Always lift the current widget when it gains focus
            if not event.widget.stdheight == None:event.widget.configure(height=int(event.widget.stdheight*event.widget.stdheight_mult))
            if not event.widget.stdwidth == None: event.widget.configure(width=int(event.widget.stdwidth*event.widget.stdwidth_mult))
        else:
            if not event.widget.stdheight == None:event.widget.configure(height=event.widget.stdheight)
            if not event.widget.stdwidth == None: event.widget.configure(width=event.widget.stdwidth)
            for widget in window.widget_order:
                window.widget_order[widget].lift() # Sort the widgets back into their default order to restore the appearance
    

    Where the widgets are added to the iterable like so:

    window.widget_order[len(window.widget_order)] = entry1 # Add the widget to the widget order list
    

    This will present the same problem you had initially, so it will be of interest to track the currently focused widget and change your method for switching focus away from tkinter's focusNext(), which may have been the solution you preferred in the first place:

    window = Tk()
    window.geometry("300x300")
    window.widget_order = {}    # Store the desired visual ordering of widgets
    window.currentFocus = 0     # Store the current focus number to use as a key for the widget dict
    
    def focus_next_widget(event):
        window.currentFocus = (window.currentFocus + 1) % len(window.widget_order) # Calculate the next focus value, wrapping based on dict length
        window.widget_order[window.currentFocus].focus() # Focus the next widget
        return("break")
    

    The complete code would then look something like:

    from tkinter import Tk, Text
    
    def focus_next_widget(event):
        window.currentFocus = (window.currentFocus + 1) % len(window.widget_order) # Calculate the next focus value, wrapping based on dict length
        window.widget_order[window.currentFocus].focus() # Focus the next widget
        return("break")
    
    class iText(Text):
        def __init__(self, stdwidth_mult=2.5, stdheight_mult=3, **kwargs):
            super(iText, self).__init__(**kwargs)
    
            self.stdwidth = kwargs.get('width')
            self.stdheight = kwargs.get('height')
            self.stdwidth_mult = stdwidth_mult
            self.stdheight_mult = stdheight_mult
    
    def text_resizer(event):
        if event.widget == event.widget.focus_get():
            event.widget.lift() # Always lift the current widget when it gains focus
            if not event.widget.stdheight == None:event.widget.configure(height=int(event.widget.stdheight*event.widget.stdheight_mult))
            if not event.widget.stdwidth == None: event.widget.configure(width=int(event.widget.stdwidth*event.widget.stdwidth_mult))
        else:
            if not event.widget.stdheight == None:event.widget.configure(height=event.widget.stdheight)
            if not event.widget.stdwidth == None: event.widget.configure(width=event.widget.stdwidth)
            for widget in window.widget_order:
                window.widget_order[widget].lift() # Sort the widgets back into their default order to restore the appearance
    
    window = Tk()
    window.geometry("300x300")
    window.widget_order = {}    # Store the desired visual ordering of widgets
    window.currentFocus = 0     # Store the current focus number to use as a key for the widget dict
    
    entry1 = iText(width=4, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
    entry1.insert(1.0, "e1")
    entry1.place(x=6.0, y=68.0, height=43.0)
    entry1.bind("<Tab>", focus_next_widget)
    entry1.bind('<FocusIn>', text_resizer)
    entry1.bind('<FocusOut>', text_resizer)
    window.widget_order[len(window.widget_order)] = entry1 # Add the widget to the widget order list
    
    # First problematic entry
    entry_2_problem_1 = iText(width=4, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
    entry_2_problem_1.place(x=6.0, y=116.0, height=43.0)
    entry_2_problem_1.insert(1.0, "e2")
    entry_2_problem_1.bind("<Tab>", focus_next_widget)
    entry_2_problem_1.bind('<FocusIn>', text_resizer)
    entry_2_problem_1.bind('<FocusOut>', text_resizer)
    window.widget_order[len(window.widget_order)] = entry_2_problem_1 # Add the widget to the widget order list
    
    entry_3 = iText(stdheight_mult=1, height=1, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
    entry_3.place(x=70.0, y=121.0, width=102.0)
    entry_3.insert(1.0, "e3")
    entry_3.bind("<Tab>", focus_next_widget)
    entry_3.bind('<FocusIn>', text_resizer)
    entry_3.bind('<FocusOut>', text_resizer)
    window.widget_order[len(window.widget_order)] = entry_3 # Add the widget to the widget order list
    
    entry_4 = iText(width=4, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
    entry_4.place(x=6.0, y=165.0, height=43.0)
    entry_4.insert(1.0, "e4")
    entry_4.bind("<Tab>", focus_next_widget)
    entry_4.bind('<FocusIn>', text_resizer)
    entry_4.bind('<FocusOut>', text_resizer)
    window.widget_order[len(window.widget_order)] = entry_4 # Add the widget to the widget order list
    
    # Second problematic entry
    entry_5_problem_2 = iText(width=4, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
    entry_5_problem_2.place(x=6.0, y=213.0, height=43.0)
    entry_5_problem_2.insert(1.0, 'e5')
    entry_5_problem_2.bind("<Tab>", focus_next_widget)
    entry_5_problem_2.bind('<FocusIn>', text_resizer)
    entry_5_problem_2.bind('<FocusOut>', text_resizer)
    window.widget_order[len(window.widget_order)] = entry_5_problem_2 # Add the widget to the widget order list
    
    entry_6 = iText(stdheight_mult=1, height=1, bd=2, font='futura', relief='flat', highlightcolor='#3A3A3A', highlightbackground='#3A3A3A', highlightthickness=2, bg="#D9D9D9", fg="#000716")
    entry_6.place(x=70.0, y=218.0, width=102.0, height=34.0)
    entry_6.insert(1.0, 'e6')
    entry_6.bind("<Tab>", focus_next_widget)
    entry_6.bind('<FocusIn>', text_resizer)
    entry_6.bind('<FocusOut>', text_resizer)
    window.widget_order[len(window.widget_order)] = entry_6 # Add the widget to the widget order list
    
    window.mainloop()
    

    This also allows you to section off all your widget order code to make changes to the order a bit more easily; simply insert them into the dictionary in a new order. To me this makes sense, though others may differ.

    I used a dictionary but a list works just as well, I believe.