2025, Oct 01 07:40
Tkinter Listbox ऑटोकम्प्लीट में ListboxSelect के बजाय ButtonRelease-1 बाइंडिंग
यह गाइड Tkinter Listbox ऑटोकम्प्लीट में माउस-क्लिक और तीर कुंजी टकराव की वजह समझाकर ListboxSelect के बजाय ButtonRelease-1 बाइंडिंग का समाधान देता है.
Tkinter की Listbox के साथ ऑटोकम्प्लीट पॉपअप बनाना तब तक सीधा-सादा है, जब तक माउस और कीबोर्ड इंटरैक्शन आपस में नहीं टकराते. एक आम गलती आइटम को माउस से चुनने के बाद सामने आती है: तीर कुंजियाँ दबाते ही चयन किसी अप्रत्याशित आइटम पर चला जाता है—अक्सर ऐसा लगता है कि यह सीधे दूसरी पंक्ति पर कूद गया. नीचे एक न्यूनतम, काम करने वाला सेटअप है जो समस्या को दोहराता है, और उसके बाद उसका सटीक समाधान दिया गया है.
समस्या को पुन: उत्पन्न करना
यह उदाहरण StringVar के जरिए Listbox को Entry से जोड़ता है. तीर कुंजियाँ हाइलाइट को चलाती हैं और Enter से चयन की पुष्टि होती है. माउस से चयन भी काम करता है—लेकिन क्लिक के बाद, दोबारा तीर कुंजियाँ इस्तेमाल करने पर Listbox चौंकाने वाले तरीके से चयन बदल देता है.
import tkinter as tk
people_source = ["Daniel", "Ronel", "Dana", "Elani", "Isabel", "Hercules", "Karien", "Amor", "Piet", "Koos", "Jan", "Johan", "Denise", "Jean", "Petri"]
global lb_nav_index
def refresh_lb_candidates(*args):
global lb_nav_index
lb_nav_index = -1
lb_popup.delete(0, tk.END)
term = query_var.get()
if term == "":
lb_popup.place_forget()
return
else:
for name in people_source:
if term.lower() in name.lower():
lb_popup.insert(tk.END, name)
show_lb_below_entry()
print(f"|refresh_lb_candidates|New dataset applied. Index = {lb_nav_index}")
def show_lb_below_entry():
x = input_entry.winfo_rootx() - app.winfo_rootx()
y = input_entry.winfo_rooty() - app.winfo_rooty() + input_entry.winfo_height()
lb_popup.place(x=x, y=y)
input_entry.focus_set()
def on_lb_click_pick(event=None):
idx = lb_popup.curselection()[0]
val = lb_popup.get(idx)
print("|on_lb_click_pick|Mouse-picked")
render_choice_to_label(val)
def render_choice_to_label(text):
global lb_nav_index
chosen_label.config(text=text)
lb_popup.place_forget()
input_entry.focus_set()
input_entry.delete(0, tk.END)
lb_nav_index = -1
print(f"|render_choice_to_label|Rendered: {text}")
def sync_selection_after_arrows():
global lb_nav_index
lb_popup.selection_clear(0, tk.END)
lb_popup.selection_set(lb_nav_index)
lb_popup.activate(lb_nav_index)
print(f"|sync_selection_after_arrows|Now at = {lb_nav_index}")
def on_input_key_release(event):
global lb_nav_index
if lb_popup.size() > 0:
if event.keysym == "Down":
print(f"|on_input_key_release|Down pressed. Before = {lb_nav_index}")
lb_nav_index += 1
if lb_nav_index >= lb_popup.size():
lb_nav_index = 0
sync_selection_after_arrows()
elif event.keysym == "Up":
print("|on_input_key_release|Up pressed.")
lb_nav_index -= 1
if lb_nav_index < 0:
lb_nav_index = lb_popup.size() - 1
sync_selection_after_arrows()
elif event.keysym == "Return":
print("|on_input_key_release|Return pressed.")
if 0 <= lb_nav_index < lb_popup.size():
render_choice_to_label(lb_popup.get(lb_nav_index))
elif event.keysym == "Escape":
print("|on_input_key_release|Escape pressed.")
lb_popup.delete(0, tk.END)
lb_popup.place_forget()
input_entry.focus_set()
else:
print(f"|on_input_key_release|{event.keysym} pressed.")
lb_nav_index = -1
else:
print(f"|on_input_key_release|{event.keysym} pressed.")
lb_nav_index = -1
print(f"|on_input_key_release|query_var=\"{query_var.get()}\" Focus={app.focus_get()} Index={lb_nav_index} curselection={lb_popup.curselection()}")
print()
app = tk.Tk()
app.title("Listbox appear after typing")
app.geometry("400x200")
query_var = tk.StringVar()
query_var.trace("w", refresh_lb_candidates)
input_entry = tk.Entry(app, textvariable=query_var, width=60)
input_entry.pack(pady=5)
input_entry.focus_set()
input_entry.bind("<KeyRelease>", on_input_key_release)
lb_popup = tk.Listbox(app)
# समस्याग्रस्त बाइंडिंग
lb_popup.bind("<<ListboxSelect>>", on_lb_click_pick)
lb_nav_index = -1
chosen_label = tk.Label(app, text="No option is selected.")
chosen_label.pack(pady=5)
app.mainloop()
असल में क्या गलत होता है
Tkinter की Listbox में एक सक्रिय आइटम (active item) होता है और एक चयन (selection). सक्रिय आइटम वह होता है जिसे आप Up और Down से कीबोर्ड द्वारा नेविगेट करते हैं, और यह दृश्य रूप से हाइलाइट रहता है. चयन वह है जिसे उपयोगकर्ता स्पष्ट रूप से चुनता है, उदाहरण के लिए माउस क्लिक से. इवेंट <<ListboxSelect>> तब फायर होता है जब भी चयन बदलता है. माउस क्लिक चयन सेट करता है, जो हैंडलर को ट्रिगर करता है. बारीकी यह है कि अगर पहले से कोई चयन मौजूद है, तो अगली तीर-कुंजी दबाने पर केवल सक्रिय आइटम ही नहीं, चयन भी समायोजित होता है. इसका मतलब है कि आपका selection-change हैंडलर कीबोर्ड नेविगेशन के दौरान चल पड़ता है और आइटम अप्रत्याशित रूप से कमिट हो जाता है, जो सीधे किसी दूसरी पंक्ति पर छलांग जैसा लगता है.
समाधान
इसके बजाय हैंडलर को बाएँ माउस बटन के रिलीज से बाँधें. इवेंट <ButtonRelease-1> केवल तब फायर होता है जब बाएँ माउस बटन को छोड़ा जाता है, और यह तब ट्रिगर नहीं होता जब तीर कुंजियाँ Listbox में घूमती हैं. माउस से चयन पहले की तरह काम करेगा, जबकि एरो-नेविगेशन अब पिक हैंडलर को नहीं बुलाएगा.
import tkinter as tk
people_source = ["Daniel", "Ronel", "Dana", "Elani", "Isabel", "Hercules", "Karien", "Amor", "Piet", "Koos", "Jan", "Johan", "Denise", "Jean", "Petri"]
global lb_nav_index
def refresh_lb_candidates(*args):
global lb_nav_index
lb_nav_index = -1
lb_popup.delete(0, tk.END)
term = query_var.get()
if term == "":
lb_popup.place_forget()
return
else:
for name in people_source:
if term.lower() in name.lower():
lb_popup.insert(tk.END, name)
show_lb_below_entry()
print(f"|refresh_lb_candidates|New dataset applied. Index = {lb_nav_index}")
def show_lb_below_entry():
x = input_entry.winfo_rootx() - app.winfo_rootx()
y = input_entry.winfo_rooty() - app.winfo_rooty() + input_entry.winfo_height()
lb_popup.place(x=x, y=y)
input_entry.focus_set()
def on_lb_click_pick(event=None):
idx = lb_popup.curselection()[0]
val = lb_popup.get(idx)
print("|on_lb_click_pick|Mouse-picked")
render_choice_to_label(val)
def render_choice_to_label(text):
global lb_nav_index
chosen_label.config(text=text)
lb_popup.place_forget()
input_entry.focus_set()
input_entry.delete(0, tk.END)
lb_nav_index = -1
print(f"|render_choice_to_label|Rendered: {text}")
def sync_selection_after_arrows():
global lb_nav_index
lb_popup.selection_clear(0, tk.END)
lb_popup.selection_set(lb_nav_index)
lb_popup.activate(lb_nav_index)
print(f"|sync_selection_after_arrows|Now at = {lb_nav_index}")
def on_input_key_release(event):
global lb_nav_index
if lb_popup.size() > 0:
if event.keysym == "Down":
print(f"|on_input_key_release|Down pressed. Before = {lb_nav_index}")
lb_nav_index += 1
if lb_nav_index >= lb_popup.size():
lb_nav_index = 0
sync_selection_after_arrows()
elif event.keysym == "Up":
print("|on_input_key_release|Up pressed.")
lb_nav_index -= 1
if lb_nav_index < 0:
lb_nav_index = lb_popup.size() - 1
sync_selection_after_arrows()
elif event.keysym == "Return":
print("|on_input_key_release|Return pressed.")
if 0 <= lb_nav_index < lb_popup.size():
render_choice_to_label(lb_popup.get(lb_nav_index))
elif event.keysym == "Escape":
print("|on_input_key_release|Escape pressed.")
lb_popup.delete(0, tk.END)
lb_popup.place_forget()
input_entry.focus_set()
else:
print(f"|on_input_key_release|{event.keysym} pressed.")
lb_nav_index = -1
else:
print(f"|on_input_key_release|{event.keysym} pressed.")
lb_nav_index = -1
print(f"|on_input_key_release|query_var=\"{query_var.get()}\" Focus={app.focus_get()} Index={lb_nav_index} curselection={lb_popup.curselection()}")
print()
app = tk.Tk()
app.title("Listbox appear after typing")
app.geometry("400x200")
query_var = tk.StringVar()
query_var.trace("w", refresh_lb_candidates)
input_entry = tk.Entry(app, textvariable=query_var, width=60)
input_entry.pack(pady=5)
input_entry.focus_set()
input_entry.bind("<KeyRelease>", on_input_key_release)
lb_popup = tk.Listbox(app)
# सुधारी गई बाइंडिंग
lb_popup.bind("<ButtonRelease-1>", on_lb_click_pick)
lb_nav_index = -1
chosen_label = tk.Label(app, text="No option is selected.")
chosen_label.pack(pady=5)
app.mainloop()
यह क्यों मायने रखता है
ऑटोकम्प्लीट UI पूर्वानुमेय इनपुट हैंडलिंग पर निर्भर होते हैं. जब selection-change इवेंट्स ही कीबोर्ड नेविगेशन के ट्रिगर भी बन जाएँ, तो नियंत्रण प्रवाह उलझ जाता है और उपयोगकर्ताओं को अनचाही पुष्टियाँ या उछाल दिखते हैं. माउस क्रियाओं की मंशा को एरो नेविगेशन से अलग रखने से व्यवहार सुसंगत रहता है और आकस्मिक ऑटोकम्प्लीट से बचाव होता है.
मुख्य निष्कर्ष
जब आपको Tkinter Listbox में माउस-चालित चयन चाहिए और कीबोर्ड traversal में हस्तक्षेप नहीं करना, तो <ButtonRelease-1> पर बाइंडिंग को <<ListboxSelect>> की जगह प्राथमिकता दें. नेविगेशन स्टेट को अलग रखें, क्वेरी बदलते ही इंडेक्स रीसेट करें, और इनपुट वैल्यू के आधार पर पॉपअप दिखाएँ या छिपाएँ—ताकि अनुभव साफ-सुथरा और पूर्वानुमेय बना रहे.