Search code examples
python-3.xtkintercomboboxttk

Change the width of an updated listbox in a ttk Combobox


This is an extension to the question and answers here.

I want to create two Comboboxes, with the items in the second Combobox depending on the selection in the first Combobox. Furthermore, I would like the dropdown listbox to resize to fit the text in the list, as in the answer here. However, I'm having some difficulties with this second part. I'd like to solve this problem using Python.

I've had varying results using .pack() and .forget() instead of .place() and .place_forget(), however I haven't been able to create a robust solution. Using .place is preferable to .pack or .grid if possible.

As an MWE, I've extended the code from one of the answers in the previous question. The dropdown listbox of c2 resizes fine, however that of c1 does not.

import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font as tkfont

def on_combo_configure(event):
    combo = event.widget
    style = ttk.Style()
    # check if the combobox already has the "postoffest" property
    current_combo_style = combo.cget('style') or "TCombobox"
    if len(style.lookup(current_combo_style, 'postoffset'))>0:
        return
    combo_values = combo.cget('values')
    if len(combo_values) == 0:
        return
    longest_value = max(combo_values, key=len)
    font = tkfont.nametofont(str(combo.cget('font')))
    width = font.measure(longest_value + "0") - event.width
    if (width<0):
        # no need to make the popdown smaller
        return
    # create an unique style name using widget's id
    unique_name='Combobox{}'.format(combo.winfo_id())
    # the new style must inherit from curret widget style (unless it's our custom style!) 
    if unique_name in current_combo_style:
        style_name = current_combo_style 
    else:
        style_name = "{}.{}".format(unique_name, current_combo_style)

    style.configure(style_name, postoffset=(0,0,width,0))
    combo.configure(style=style_name)

def update_c1_list(event):
    c1.place_forget()
    _ = c.get()
    if _ == "fruit":
        c1['values'] = ('apples are the best', 'bananas are way more better')
    elif _ == "text":
        c1['values'] = ("here's some text", "and here's some much longer text to stretch the list")
    else:
        pass
    c1.place(x=10,y=40)

root = tk.Tk()
root.title("testing the combobox")
root.geometry('300x300+50+50')

c = ttk.Combobox(root, values=['fruit','text'], state="readonly", width=10)
c.bind('<Configure>', on_combo_configure)
c.bind('<<ComboboxSelected>>', update_c1_list)
c.place(x=10,y=10)

c1 = ttk.Combobox(root, state="readonly", width=10)
c1.bind('<Configure>', on_combo_configure)
c1.place(x=10,y=40)

c2 = ttk.Combobox(root, state="readonly", width=10)
c2.bind('<Configure>', on_combo_configure)
c2.place(x=10,y=70)
c2['values']=('this list resizes fine','because it is updated outside of the function')

root.mainloop()

Any help is welcome, thanks.


Solution

  • After playing around I came up with a function which updates the listbox width of a combobox "on the fly". However, it's a bit like fixing a window with a hammer and causes some issues.

    def Change_combo_width_on_the_fly(combo,combo_width):
    style = ttk.Style()
    # check if the combobox already has the "postoffest" property
    current_combo_style = combo.cget('style') or "TCombobox"
    combo_values = combo.cget('values')
    if len(combo_values) == 0:
        return
    longest_value = max(combo_values, key=len)
    font = tkfont.nametofont(str(combo.cget('font')))
    width = font.measure(longest_value + "0") - (combo_width*6+23)
    if (width<0):
        # no need to make the popdown smaller
        return
    # create an unique style name using widget's id
    unique_name='Combobox{}'.format(combo.winfo_id())
    # the new style must inherit from curret widget style (unless it's our custom style!) 
    if unique_name in current_combo_style:
        style_name = current_combo_style 
    else:
        style_name = "{}.{}".format(unique_name, current_combo_style)
    
    style.configure(style_name, postoffset=(0,0,width,0))
    combo.configure(style=style_name)
    

    As a MWE, the code can be used as follows:

    import tkinter as tk
    import tkinter.ttk as ttk
    import tkinter.font as tkfont
    
    root = tk.Tk()
    c1 = ttk.Combobox(root, state="readonly", width=10)
    c1.place(x=10,y=40)
    Change_combo_width_on_the_fly(c1,10)
    root.mainloop()
    

    While the function does the job, it causes problems elsewhere in my code. In particular, it messes with a previously packed widget (scrollbar). I think it is changing the style the last placed widget, but I don't know how to fix this.