I have searched for days for a solution, but nowhere in the documentation and in no forum I found a fitting answer. Seems like I'm the only one with this problem (?). Problem: I need some pre- and post-processing for my combobox. Therefore I bound focus_in- and focus-out-functions. But focus_out is triggered everytime I click on the button of the popdown-list. This is of course not the wanted behaviour. And after selecting an item or go back with ESC there is triggered again the focus-in-action (what is also not wanted). If the popdown is not open, focusout works as expected.
I tried several workarounds but this is like a never ending story: everytime you solve a problem there occur two others.
Some examples:
But all these workarounds shouldn't be necessary. I know that a combobox is a combination of a textentry and a droptdown-list and I think that the focus-out at the click means that the textentry loses focus. But it should not be the problem of the user of this widget to get it working correctly but the problem of the creator of it. This person should handle the events in a proper way (and please don't tell me "it's not a bug, it's a feature").
Is there a (simple) way to avoid this strange focus-out-behaviour? Is it possible to determine which one of the two widgets of the combobox (entry, popdown) is active when the focusout occurs? focus_get / nametowidget is not working. How can I react to keytab like ESC while popdown is open (a bound function does not work)? And last but not least: should I better not waste more time with this sh#*@t and create an own combobox from scratch?
BTW: I use Python 3.11 on Windows 11.
Some code-examples (this is not my "real" code, but the problems are the same): 1: added focus_get() in the combo_test_focus_out method. As you can see, focus_get runs on error. 2. bound KeyPress and KeyRelease to the combobox. If you open the popdown and press a key you can see that you see nothing (except at ESC and RETURN)...
import tkinter as tk
from tkinter import ttk
class test_combo():
def __init__(self, master = None):
self.master = master
my_textvar = tk.StringVar()
blabla = ttk.Entry()
combo_test = ttk.Combobox ( textvariable = my_textvar )
combo_test.bind ( "<FocusIn>", self.combo_test_focus_in )
combo_test.bind ( "<FocusOut>", self.combo_test_focus_out )
combo_test.bind ( "<KeyRelease>", self.combo_test_key_release )
combo_test.bind ( "<KeyPress>", self.combo_test_key_press )
combo_test ['values'] = [1,2,3]
blabla.pack()
combo_test.pack()
blabla.focus_set()
def combo_test_focus_in( self, event ):
print ("I'm in focus_in")
def combo_test_focus_out( self, event ):
print ("I'm in focus_out")
object_with_focus = None
try:
object_with_focus = root.focus_get ()
except Exception as e:
print ( f'focus_get Error: {e}' )
print ("object_with_focus = ", object_with_focus)
def combo_test_key_release( self, event ):
print ("I'm in combo_test_key_release")
print ( f"Key released: {event.keysym}" )
def combo_test_key_press( self, event ):
print ("I'm in combo_test_key_press")
print ( f"Key pressed: {event.keysym}" )
root = tk.Tk()
root.geometry ("%dx%d+%d+%d" % (200, 100, 1000, 300 ))
app = test_combo (master = root)
root.mainloop ()
The following example is 1:1 from ChatGPT. Besides the problem that he cannot get the focused object, there seem to be some logical errors inside. Put it just her to demonstrate the popdown-problem (see in check_dropdown_open).
import tkinter as tk
from tkinter import ttk
class CustomCombobox(ttk.Combobox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._is_dropdown_open = False
self.bind('<<ComboboxSelected>>', self.on_selection)
self.bind('<FocusOut>', self.on_focus_out)
self.bind('<Button-1>', self.on_button_click, add='+')
def on_button_click(self, event):
if not self._is_dropdown_open:
self._is_dropdown_open = True
self.after(1, self.check_dropdown_open)
else:
self._is_dropdown_open = False
def check_dropdown_open(self):
try:
my_popdown = self.tk.call("ttk::combobox::PopdownWindow", self)
self.nametowidget(my_popdown).bind('<FocusOut>', self.on_dropdown_focus_out, add='+')
except Exception as e:
print(f'Error: {e}')
def on_dropdown_focus_out(self, event):
if self._is_dropdown_open:
self._is_dropdown_open = False
print("Dropdown focus lost")
def on_selection(self, event):
self._is_dropdown_open = False
def on_focus_out(self, event):
if not self._is_dropdown_open:
print("Combobox focus lost")
root = tk.Tk()
root.geometry ("%dx%d+%d+%d" % (200, 100, 1000, 300 ))
blabla = ttk.Entry (master = root)
combo = CustomCombobox(master = root)
combo ['values'] = [1, 2, 3]
blabla.pack ()
combo.pack ()
root.mainloop ()
I will first clarify some things that are happening (not working as you expected):
Combobox
itself is nothing more than just an entry
widget
(subclass). If you bind something to a Combobox
, you bind it all to an
Entry
and not to dropdown list
. Now the Combobox
(Entry
) focus will be removed when you open the dropdown list
. The focus will be moved to the dropdown list
. Hence, when you open it,
the FocusOut
event is triggered when focus leaves the entry and moves to the
dropdown list
. The opposite situation occurs when you close (select an item) from the dropdown list
, the focus moves back to the Combobox
(Entry
). This is why it may look like strange behavior.
"focus_get
produces an Error". In fact, it's most likely a bug (or a Tkinter feature?). It happens because the dropdown list
you see is
not "registered" in Tkinter when it is created. Tkinter thinks that it
doesn't exist at all. In the error:
w = w.children[n]
KeyError: 'popdown'
This indicates that this dropdown list
is not saved in the tkinter dictionary children
. There may be a way to manually add the dropdown list
to it, but it probably won't be easy. To "fix" this issue, you can call the tk/tcl command instead, like this: object_with_focus = root.call('focus')
This will not raise any error. Note that this command will return the tcl
object of your widget, not a regular tkinter class. But you can still use it using tk/tcl commands instead.
"KeyPress
and KeyRelease
don't work". This events only trigger for the widget that currently has focus. When you open the dropdown list
, your focus will be removed from Combobox
(Entry
) and moved to dropdown list
(Which you can check with root.call('focus')
, will return something like .!combobox.popdown.f.l
). Therefore, your key events can only be triggered if you bind them to the .!combobox.popdown.f.l
, not to Combobox
(Entry
) (for the moment when the dropdown list
is opened).
I will also briefly explain the structure of the "combobox
".
When the dropdown
doesn't open, it is just a Combobox
widget, which is itself a subclass of the TEntry
When clicked, a Listbox
will be created if this is the first time you press the combobox
"button". In fact, it is much more complex than just a Listbox
. The structure of the it looks like this:
.!combobox (
Entry
widget).!combobox.popdown (a child of the
.!combobox
, this is aToplevel widget
which has a classComboboxPopdown
).!combobox.popdown.f (a child of the .popdown, this is a
Frame
widget which has a classTFrame
).!combobox.popdown.f.l (a child of the
Frame
above, this is aListbox
widget which has a classListbox
. Inbindtags
this widget also has a useful tagComboboxListbox
which can be used as a class binding).!combobox.popdown.f.sb (also a child of the
Frame
above. I think this is a horizontalScrollbar
of theTScrollbar
class)
When you first click the Combobox
"button", a popdown
(Toplevel
) window will be created. After that, each click will simply withdraw
(hide) the popdown
(Toplevel
) or deiconify
and raise
(show) it, but it will not be re-created and will exist until the end of the program.
Now you can add all this to your code to get the following code:
import tkinter as tk
from tkinter import ttk
class test_combo():
def __init__(self, master=None):
self.master = master
my_textvar = tk.StringVar()
blabla = ttk.Entry()
combo_test = ttk.Combobox(textvariable=my_textvar)
combo_test.bind_class("ComboboxListbox", "<Map>", self.dropdown_list_created, add='+')
combo_test.bind_class("ComboboxPopdown", "<FocusIn>", self.combo_test_focus_in, add='+')
combo_test.bind_class("ComboboxPopdown", "<FocusOut>", self.combo_test_focus_out, add='+')
combo_test['values'] = [1, 2, 3]
blabla.pack()
combo_test.pack()
blabla.focus_set()
def dropdown_list_created(self, event):
root.bind_class(event.widget, "<KeyPress>", self.combo_test_key_press)
root.bind_class(event.widget, "<KeyRelease>", self.combo_test_key_release)
def combo_test_focus_in(self, event):
print(root.call('focus'), " in focus_in")
def combo_test_focus_out(self, event):
print("I'm in focus_out")
object_with_focus_string_name = root.call('focus')
print("object_with_focus = ", object_with_focus_string_name)
def combo_test_key_release(self, event):
print("I'm in combo_test_key_release")
print(f"Key released: {event.keysym}")
def combo_test_key_press(self, event):
print("I'm in combo_test_key_press")
print(f"Key pressed: {event.keysym}")
root = tk.Tk()
root.geometry("%dx%d+%d+%d" % (200, 100, 1000, 300))
app = test_combo(master=root)
root.mainloop()
Code notes:
ComboboxListbox
for <FocusIn>
and <FocusOut>
events instead of ComboboxPopdown
.<Map>
event occurs when the Listbox
is created, when it becomes visible. Need for the <KeyPress>
and <KeyRelease>
events bindings.ComboboxListbox
and ComboboxPopdown
are sort of "class" bindings, they will be called for all your Comboboxes
. For this reason, if you want your functions to only trigger on a specific Combobox
, you will need to add a check to the functions, something like this:
if combo_test in event.widget:
# do something
pass
There may be other ways to do this check.
I hope this was at least a little bit helpful. If you have any questions or something is not working in the way you wanted, feel free to ask.