2025, Nov 26 05:00

Fixing hidden Text cursor after Tkinter Listbox selection: focus timing, VirtualEvents, and reliable workarounds

Learn why the Tkinter Text caret disappears after a Listbox selection. VirtualEvent timing and fixes: focus with after(), FocusIn, or ButtonRelease bindings.

Tkinter focus and VirtualEvent timing: why your Text cursor hides after Listbox selection

When a Tkinter Listbox item is selected, it’s tempting to jump the insertion cursor straight into a Text widget and continue the workflow there. But there’s a catch: the caret often doesn’t show up until you press Tab. With a Button this doesn’t happen, which makes the behavior even more puzzling. Let’s reproduce the situation and walk through practical fixes.

Reproducing the issue

The following snippet binds to the Listbox’s selection and tries to focus the Text widget, position the insertion cursor, and scroll to it. The cursor won’t be visible right away.

import tkinter as tk

jump_points = ['2.5', '5.10', '8.15']

def handle_pick(evt):
    idx = lbox.curselection()[0]
    where = jump_points[idx]

    editor.focus_set()
    editor.mark_set('insert', where)
    editor.see(where)
    editor.event_generate('<FocusIn>')

app = tk.Tk()

lbox = tk.Listbox(app)
lbox.pack(side=tk.LEFT, padx=10, pady=10)
for p in jump_points:
    lbox.insert('end', f'Go to {p} in the text')
lbox.bind('<<ListboxSelect>>', handle_pick)

editor = tk.Text(app, height=15, width=50)
editor.pack(side=tk.RIGHT, padx=10, pady=10)
text_blob = ''
for n in range(9):
    text_blob += f"{n+1}:234567890123456789\n"
editor.insert('1.0', text_blob)

app.mainloop()

What’s going on

The event you’re using, <<ListboxSelect>>, is a VirtualEvent. It can behave differently from direct bindings like <ButtonRelease> or a command=. The practical effect here is timing: your callback may run before Tkinter finalizes focus on the Listbox. Even if you direct focus to the Text widget inside the callback, Tkinter may subsequently set it back to the Listbox, which is why the caret becomes visible only after you press Tab.

There’s another subtlety you might encounter afterward. Using Shift with arrow keys can retrigger the selection logic, but at that moment curselection() can be empty, which leads to an IndexError when you read [0]. Adding a guard for the empty selection takes care of it.

Working fixes

One reliable approach is to defer the focus change to a moment after Tkinter completes its own focus work. Scheduling the focus_set() with after() accomplishes this neatly.

import tkinter as tk

jump_points = ['2.5', '5.10', '8.15']

def handle_pick(evt):
    sel = lbox.curselection()
    if not sel:
        return
    mark = jump_points[sel[0]]

    editor.mark_set('insert', mark)
    editor.see(mark)
    root.after(100, focus_editor)

def focus_editor():
    editor.focus_set()

root = tk.Tk()

lbox = tk.Listbox(root)
lbox.pack(side='left', padx=10, pady=10)
for point in jump_points:
    lbox.insert('end', f'Go to {point} in the text')
lbox.bind('<<ListboxSelect>>', handle_pick)

editor = tk.Text(root, height=15, width=50)
editor.pack(side='right', padx=10, pady=10)
content = ''
for i in range(1, 10):
    content += f"{i}:234567890123456789\n"
editor.insert('1.0', content)

root.mainloop()

An alternative is to listen for <FocusIn> on the Listbox and immediately redirect focus to the Text widget. This plays into the framework’s own ordering by acting after the Listbox has formally gained focus.

import tkinter as tk

jump_points = ['2.5', '5.10', '8.15']

def move_caret(evt):
    sel = lbox.curselection()
    if not sel:
        return
    loc = jump_points[sel[0]]
    editor.mark_set('insert', loc)
    editor.see(loc)

def redirect_focus(evt):
    editor.focus_set()

root = tk.Tk()

lbox = tk.Listbox(root)
lbox.pack(side='left', padx=10, pady=10)
for pt in jump_points:
    lbox.insert('end', f'Go to {pt} in the text')
lbox.bind('<<ListboxSelect>>', move_caret)
lbox.bind('<FocusIn>', redirect_focus)

editor = tk.Text(root, height=15, width=50)
editor.pack(side='right', padx=10, pady=10)
blob = ''
for n in range(1, 10):
    blob += f"{n}:234567890123456789\n"
editor.insert('1.0', blob)

root.mainloop()

If you prefer to avoid the VirtualEvent path entirely, you can bind to a mouse release event instead. Replacing <<ListboxSelect>> with <ButtonRelease> or <ButtonRelease-1> is a simple way to ensure the Listbox has settled its state by the time your handler runs.

import tkinter as tk

jump_points = ['2.5', '5.10', '8.15']

def pick_on_release(evt):
    sel = lbox.curselection()
    if not sel:
        return
    pos = jump_points[sel[0]]
    editor.focus_set()
    editor.mark_set('insert', pos)
    editor.see(pos)

root = tk.Tk()

lbox = tk.Listbox(root)
lbox.pack(side='left', padx=10, pady=10)
for p in jump_points:
    lbox.insert('end', f'Go to {p} in the text')
lbox.bind('<ButtonRelease-1>', pick_on_release)

editor = tk.Text(root, height=15, width=50)
editor.pack(side='right', padx=10, pady=10)
text_data = ''
for i in range(1, 10):
    text_data += f"{i}:234567890123456789\n"
editor.insert('1.0', text_data)

root.mainloop()

Why this matters

Focus management sits at the heart of GUI responsiveness. When event order is counterintuitive, users feel it immediately: you click, expect to type, but the insertion cursor isn’t there. Understanding how VirtualEvent timing interacts with widget focus helps you avoid flicker, phantom focus changes, and confusing keyboard behavior. It also highlights a broader theme in Tkinter: binding to the right event can be the difference between smooth UX and unexpected state changes.

Takeaways

If the caret in a Text widget doesn’t appear after a Listbox selection, the issue can stem from when <<ListboxSelect>> fires relative to the framework’s own focus logic. Deferring the focus with after(), redirecting focus on <FocusIn>, or switching to <ButtonRelease> are effective ways to make the UI behave as expected. Add a small guard around curselection() to prevent errors when the selection becomes temporarily empty. With those adjustments in place, the caret reliably shows up where you expect it, right when you need it.