tkintercomboboxfocusout

tkinter combobox FocusOut


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 ()

Solution

  • Introduction

    I will first clarify some things that are happening (not working as you expected):

    1. 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.

    2. "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.

    3. "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).

    4. I will also briefly explain the structure of the "combobox".

      1. When the dropdown doesn't open, it is just a Combobox widget, which is itself a subclass of the TEntry

      2. 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 a Toplevel widget which has a class ComboboxPopdown)

        .!combobox.popdown.f (a child of the .popdown, this is a Frame widget which has a class TFrame)

        .!combobox.popdown.f.l (a child of the Frame above, this is a Listbox widget which has a class Listbox. In bindtags this widget also has a useful tag ComboboxListbox which can be used as a class binding)

        .!combobox.popdown.f.sb (also a child of the Frame above. I think this is a horizontal Scrollbar of the TScrollbar 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.

    Code Implementation

    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:

    1. You can use ComboboxListbox for <FocusIn> and <FocusOut> events instead of ComboboxPopdown.
    2. The <Map> event occurs when the Listbox is created, when it becomes visible. Need for the <KeyPress> and <KeyRelease> events bindings.
    3. Since 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.