2

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)

1 Answers1

2

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.

FawltyPlay
  • 56
  • 7
  • Your assumptions was right, that is what I wanted. Why can't you comment - do I have to enable something? (I am new here, that is why I ask) However, about your "simple answer", I had already tried that before and that does not work, because you are lifting any widget when you get it, so you change it's focus order and it ends broken. Your second solution works fine, despite some minor problems when implementing it to me real GUI as it is composed by multiple toplevel windows so I cannot write "window" in the text-resizer function (as I have many different window names). Anyways, it's fine – diegoperez01 Jul 16 '23 at 11:16
  • Until someone has 50 reputation they cannot comment on just anything, only questions they ask and answers they have submitted. For me, the simple answer allowed me to tab-cycle through all of them in the right order repeatedly. However, I realize now that if you were to click to one of these entry boxes without tabbing to it (skipping a box in the `lift()` cycle, the order would definitely change. I would certainly recommend the second option for a more robust answer. – FawltyPlay Jul 16 '23 at 18:44
  • 1
    As for the issue of having many toplevel windows, the reference to the `window` object could just as easily be something like `entryBoxWindow = tk()`, or you could define your own classes that extend the functionality of the baseline tkinter classes to have a more organized structure. If I understand you correctly, that should be fine? – FawltyPlay Jul 16 '23 at 18:46
  • Yes, Indeed, those are the little modifications I applied to make it work "with elegance". Thx! – diegoperez01 Jul 16 '23 at 19:52