How do I enable multiple selection of values from a combobox?
Asked Answered
D

5

7

Python 3.4.3, Windows 10, Tkinter

I am attempting to create a combobox that allows for multiple selections from the dropdown. I have found similar work for listbox (Python Tkinter multiple selection Listbox), but cannot get it work with the combobox.

Is there a simple way to enable multiple selection from the dropdown of the combobox?

Duston answered 31/12, 2015 at 17:36 Comment(0)
A
8

By design the ttk combobox doesn't support multiple selections. It is designed to allow you to pick one item from a list of choices.

If you need to be able to make multiple choices you can use a menubutton with an associated menu, and add checkbuttons or radiobuttons to the menu.

Here's an example:

import Tkinter as tk

class Example(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        menubutton = tk.Menubutton(self, text="Choose wisely", 
                                   indicatoron=True, borderwidth=1, relief="raised")
        menu = tk.Menu(menubutton, tearoff=False)
        menubutton.configure(menu=menu)
        menubutton.pack(padx=10, pady=10)

        self.choices = {}
        for choice in ("Iron Man", "Superman", "Batman"):
            self.choices[choice] = tk.IntVar(value=0)
            menu.add_checkbutton(label=choice, variable=self.choices[choice], 
                                 onvalue=1, offvalue=0, 
                                 command=self.printValues)
    def printValues(self):
        for name, var in self.choices.items():
            print "%s: %s" % (name, var.get())

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(fill="both", expand=True)
    root.mainloop()
Ad answered 31/12, 2015 at 18:13 Comment(1)
Trying this example... However, on macOS 10.13.6 and Python 3.7.1 Homebrew, clicking on the menu items gives me messy result, the checkmarks appear and disappear not according to the assumed logic as you click on each item.Banna
S
4

Try this maybe...

import tkinter as Tkinter
import tkinter.font as tkFont
import tkinter.ttk as ttk

class Picker(ttk.Frame):

    def __init__(self, master=None,activebackground='#b1dcfb',values=[],entry_wid=None,activeforeground='black', selectbackground='#003eff', selectforeground='white', command=None, borderwidth=1, relief="solid"):

        self._selected_item = None

        self._values = values

        self._entry_wid = entry_wid

        self._sel_bg = selectbackground 
        self._sel_fg = selectforeground

        self._act_bg = activebackground 
        self._act_fg = activeforeground

        self._command = command
        ttk.Frame.__init__(self, master, borderwidth=borderwidth, relief=relief)

        self.bind("<FocusIn>", lambda event:self.event_generate('<<PickerFocusIn>>'))
        self.bind("<FocusOut>", lambda event:self.event_generate('<<PickerFocusOut>>'))

        self._font = tkFont.Font()

        self.dict_checkbutton = {}
        self.dict_checkbutton_var = {}
        self.dict_intvar_item = {}

        for index,item in enumerate(self._values):

            self.dict_intvar_item[item] = Tkinter.IntVar()
            self.dict_checkbutton[item] = ttk.Checkbutton(self, text = item, variable=self.dict_intvar_item[item],command=lambda ITEM = item:self._command(ITEM))
            self.dict_checkbutton[item].grid(row=index, column=0, sticky=Tkinter.NSEW)
            self.dict_intvar_item[item].set(0)


class Combopicker(ttk.Entry, Picker):
    def __init__(self, master, values= [] ,entryvar=None, entrywidth=None, entrystyle=None, onselect=None,activebackground='#b1dcfb', activeforeground='black', selectbackground='#003eff', selectforeground='white', borderwidth=1, relief="solid"):

        if entryvar is not None:
            self.entry_var = entryvar
        else:
            self.entry_var = Tkinter.StringVar()

        entry_config = {}
        if entrywidth is not None:
            entry_config["width"] = entrywidth

        if entrystyle is not None:
            entry_config["style"] = entrystyle

        ttk.Entry.__init__(self, master, textvariable=self.entry_var, **entry_config, state = "readonly")

        self._is_menuoptions_visible = False

        self.picker_frame = Picker(self.winfo_toplevel(), values=values,entry_wid = self.entry_var,activebackground=activebackground, activeforeground=activeforeground, selectbackground=selectbackground, selectforeground=selectforeground, command=self._on_selected_check)

        self.bind_all("<1>", self._on_click, "+")

        self.bind("<Escape>", lambda event: self.hide_picker())

    @property
    def current_value(self):
        try:
            value = self.entry_var.get()
            return value
        except ValueError:
            return None

    @current_value.setter
    def current_value(self, INDEX):
        self.entry_var.set(values.index(INDEX))

    def _on_selected_check(self, SELECTED):

        value = []
        if self.entry_var.get() != "" and self.entry_var.get() != None:
            temp_value = self.entry_var.get()
            value = temp_value.split(",")

        if str(SELECTED) in value:
            value.remove(str(SELECTED))

        else:    
            value.append(str(SELECTED))

        value.sort()

        temp_value = ""
        for index,item in enumerate(value):
            if item!= "":
                if index != 0:
                    temp_value += ","
                temp_value += str(item)

        self.entry_var.set(temp_value)

    def _on_click(self, event):
        str_widget = str(event.widget)

        if str_widget == str(self):
            if not self._is_menuoptions_visible:
                self.show_picker()
        else:
            if not str_widget.startswith(str(self.picker_frame)) and self._is_menuoptions_visible:
                self.hide_picker()

    def show_picker(self):
        if not self._is_menuoptions_visible:
            self.picker_frame.place(in_=self, relx=0, rely=1, relwidth=1 )
            self.picker_frame.lift()

        self._is_menuoptions_visible = True

    def hide_picker(self):
        if self._is_menuoptions_visible:
            self.picker_frame.place_forget()

        self._is_menuoptions_visible = False

if __name__ == "__main__":
    import sys

    try:
        from Tkinter import Tk, Frame, Label
    except ImportError:
        from tkinter import Tk, Frame, Label

    root = Tk()
    root.geometry("500x600")

    main =Frame(root, pady =15, padx=15)
    main.pack(expand=True, fill="both")

    Label(main, justify="left", text=__doc__).pack(anchor="w", pady=(0,15))

    COMBOPICKER1 = Combopicker(main, values = [1, 2, 3, 4])
    COMBOPICKER1.pack(anchor="w")


    if 'win' not in sys.platform:
        style = ttk.Style()
        style.theme_use('clam')

    root.mainloop()
Smaragdite answered 11/8, 2019 at 15:53 Comment(0)
D
1

I used the example of #SilverHalo which I finally improved I think I added the possibility of searching in the combobox and also added a scrollbar because in his example the frame became much too large with many values. I also added the possibility to enlarge the combobox with a right click in the search bar or a right click on the checkbutton you just have to install scrolledframe from pip and see if the result can be useful.

import tkinter as tk
from tkinter import *
from tkinter import ttk 
from tkscrolledframe import ScrolledFrame #https://github.com/bmjcode/tkScrolledFrame
# for installation pip install tkScrolledFrame





class Picker(ttk.Frame):

    def __init__(self, master=None,
                 values=[],
                 entry_wid=None,
                 command=None,
                 entlarge_fonction=None, 
                 borderwidth=1, 
                 relief="solid",
                 font=('Arial, 11')):

        self._selected_item = None
        self._values = values
        self._entry_wid = entry_wid
        self._command = command
        if type(entlarge_fonction).__name__!='NoneType':
            self.entlarge_fonction=entlarge_fonction
        else:
            self.entlarge_fonction= lambda : 1
        style = ttk.Style()
        style.configure('Custom.TFrame', background='#ffffff')
        ttk.Frame.__init__(self, master, borderwidth=borderwidth, relief=relief, style='Custom.TFrame')
        self.bind("<Button-3>", lambda event : self.entlarge_fonction())
        self.bind("<FocusIn>", lambda event:self.event_generate('<<PickerFocusIn>>'))
        self.bind("<FocusOut>", lambda event:self.event_generate('<<PickerFocusOut>>'))
        # search field
        self.searchfield=ttk.Entry(self , font=font)
        self.searchfield.pack(fill=X)
        self.searchfield.insert(END, 'Search')
        self.searchfield.bind("<Button-1>", lambda event : self.clear_search_field())
        self.searchfield.bind("<Button-3>", lambda event : self.entlarge_fonction())
        self.searchfield.bind('<KeyRelease>',lambda event : self.update_checkbutton_list())


        

        self.scrolledframe=ScrolledFrame(self)
        self.scrolledframe.pack(fill=BOTH)
        self.scrolledframe.bind_arrow_keys(self)
        self.scrolledframe.bind_scroll_wheel(self)
        self.frame = self.scrolledframe.display_widget(Frame)
        
        style = ttk.Style()
        style.configure('Custom.TFrame', background='#ffffff')

        #checkbutton frame
        self.check_buttonFrame=ttk.Frame(self.frame, style='Custom.TFrame')
        self.check_buttonFrame.pack(fill=BOTH)



        self.dict_checkbutton = {}
        self.dict_intvar_item = {}
        self.selected_value=[]
        self.create_check_button()



    @property
    def selection(self):
        return self.selected_value
    def set_value(self, new_values):
        if type(new_values).__name__=='list':
            self.selected_value=[]
            self._values=new_values
        else:
            raise('ValueError')


    def create_check_button(self, values=None):
        self.dict_checkbutton = {}
        self.dict_intvar_item = {}
        if type(values).__name__=='NoneType':
            values=self._values

        # first clear my frame
        for widget in self.check_buttonFrame.winfo_children():
            widget.destroy()

        for index,item in enumerate(values):

            self.dict_intvar_item[item] = tk.IntVar()
            self.dict_checkbutton[item] = ttk.Checkbutton(self.check_buttonFrame, 
                                                          text = item, 
                                                          variable=self.dict_intvar_item[item],
                                                          command=lambda selected=item : self.check_box_select(selected))
            self.dict_checkbutton[item].grid(row=index, column=0, sticky=tk.NSEW)
            self.dict_checkbutton[item].bind("<Button-3>", lambda e : self.entlarge_fonction())
            if item not in self.selected_value:
                self.dict_intvar_item[item].set(0)
            else:
                self.dict_intvar_item[item].set(1)

    def check_box_select(self, selected_value=None):
        for keys in self.dict_intvar_item.keys():
            if self.dict_intvar_item[keys].get()!=0:
                if keys not in self.selected_value:
                    self.selected_value.append(keys)
            else:
                if keys in self.selected_value:
                    self.selected_value.remove(keys)
        try:
            self._command(selected_value)
        except Exception as e:
            print(e)



    def clear_search_field(self):
        self.searchfield.delete(0, END)
        self.update_checkbutton_list()
    
    def update_checkbutton_list(self):
        
        search_value=self.searchfield.get()

        update_values=[]

        if search_value!='':
            for item in self._values:
                if search_value.lower() in str(item).lower():
                    update_values.append(item)
        else:
            update_values=self._values.copy()

        self.create_check_button(update_values)


class Combopicker(ttk.Combobox, Picker):
    def __init__(self, master, 
                 values= [] ,
                 entryvar=None, 
                 entrywidth=25, 
                 entrystyle=None, 
                 font=('Arial, 16'),
                 command=None):

        if entryvar is not None:
            self.entry_var = entryvar
        else:
            self.entry_var = tk.StringVar()
        self.entrywidth=entrywidth

        if  type(command).__name__=='NoneType':
            self.command= lambda : 1
        else:
            self.command = command

        entry_config = {}
        if entrywidth is not None:
            entry_config["width"] = entrywidth

        if entrystyle is not None:
            entry_config["style"] = entrystyle

        ttk.Combobox.__init__(self, master, 
                              textvariable=self.entry_var, 
                              **entry_config, 
                              state = "readonly", font=font)
        self.unbind_class("TCombobox", "<Button-1>")
        self.unbind_class("TCombobox", "<Double-1>")
        self.unbind_class("TCombobox", "<Triple-1>")
        self.unbind_class("TCombobox","<<ComboboxSelected>>")


        self._is_menuoptions_visible = False

        self.picker_frame = Picker(self.winfo_toplevel(), 
                                   values=values,
                                   entry_wid = self.entry_var,
                                   entlarge_fonction=self.entlarge,
                                   command=self._on_selected_check)

        self.bind_all("<1>", self._on_click, "+")

        self.bind("<Escape>", lambda event: self.hide_picker())

        # get widget size

        self.expand_var=False




    def set_value(self, new_value):
        self.picker_frame.set_value(new_value)
    
    def get(self):
        return self.picker_frame.selection
    
    def entlarge(self):
        if self.expand_var==False:
            self.config(width=80)
            self.picker_frame.place(in_=self, relx=0, rely=1, relwidth=1, height=300 )
            self.expand_var=True
        else:
            self.config(width=self.entrywidth)
            self.picker_frame.place(in_=self, relx=0, rely=1, relwidth=1, height=150 )
            self.expand_var=False

    def _on_selected_check(self, SELECTED):
        ''' add selected values on entry'''
        values=self.picker_frame.selection.copy()
        if len(values)!=0:
            values=str(values)[1:len(str(values))-1] # transform my list to string and remove []
            self.entry_var.set(values)
        self.command()

    def _on_click(self, event):
        str_widget = str(event.widget)

        if str_widget == str(self):
            if not self._is_menuoptions_visible:
                self.show_picker()
            else:
                try:
                    self.hide_picker()
                except:
                    pass
        else:
            if not str_widget.startswith(str(self.picker_frame)) and self._is_menuoptions_visible:
                self.hide_picker()

    def show_picker(self):
        if not self._is_menuoptions_visible:
            self.picker_frame.place(in_=self, relx=0, rely=1, relwidth=1, height=150 )
            self.picker_frame.lift()
        self._is_menuoptions_visible = True

    def hide_picker(self):
        if self._is_menuoptions_visible:
            self.picker_frame.place_forget()

        self._is_menuoptions_visible = False




if __name__ == "__main__":


    root = Tk()
    root.geometry("500x600")

    main =Frame(root, pady =15, padx=15,)
    main.pack(expand=True, fill="both")

    data = {'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Emma', 'Frank', 'Grace', 'Henry', 'Isabel', 'Jack'],
            'Gender': ['F', 'M', 'M', 'M', 'F', 'M', 'F', 'M', 'F', 'M'],
            'Age': [24, 32, 18, 47, 29, 53, 41, 19, 37, 28]}
    def print_selection():
        print('click')
        for key in combobox.keys():
            print(key, combobox[key].get())
    
    combobox={}
    
    counter=0
    for key in data.keys():
        Label(main, text=key).grid(row=counter, column= 0)
        combobox[key]=Combopicker(main, values=list(set(data[key])) , command=lambda :print_selection())
        combobox[key].grid(row=counter, column= 1)
        counter+=1



    root.mainloop()
Dominga answered 21/4, 2023 at 15:22 Comment(0)
A
0

After searching for a long time I found an answer to the same question: https://mcmap.net/q/737914/-tkinter-drop-down-list-of-check-boxes-combo-boxes

He created a ChecklistCombobox, which looks just like the normal ttk.Combobox but with Checkboxes. You can find his solution on GitHub: https://github.com/hatfullr/ChecklistCombobox

Example use:

from tkinter import *
from checklistcombobox import ChecklistCombobox

master = Tk()

mydropdown = ChecklistCombobox(master, values=[f"Value {a}" for a in range(1,20)])

def output():
    print(mydropdown.get())
btn = Button(master, text="Print Output", command=output)

btn.pack()
mydropdown.pack()

master.mainloop()
Ahmadahmar answered 9/1 at 14:52 Comment(0)
D
0

I managed to do it !

    class Combo(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)

        menubutton = tk.Menubutton(self, text=nom[k][i], 
                                   indicatoron=True, borderwidth=1, relief="raised")
        menu = tk.Menu(menubutton, tearoff=False)
        menubutton.configure(menu=menu)
        menubutton.pack(padx=10, pady=10)

        self.choices = {}
        for choice in data[k][i]:
            self.choices[choice] = tk.IntVar(value=0)
            menu.add_checkbutton(label=choice, variable=self.choices[choice], 
                                 onvalue=1, offvalue=0, 
                                 command=self.printValues)
    def printValues(self):
        value =  []
        for name, var in self.choices.items():
            value.append(var.get())
        return value

    
    
root = tk.Tk()
lenght = len(data)

# Create Canvas
canva = tk.Canvas(root, width = 130*lenght,height = 50*h)
canva.pack(fill = "both", expand = True)


# x = i , y = k
combo_list = []

for k in range(len(data)):

    for i in range(len(data[k])):
        pos = Combo(root)
        combo_list.append(pos)
        pos_canva = canva.create_window(125*k,50*i,anchor = "nw",window = pos)

        
root.mainloop()

for j in range(len(combo_list)):
    print(combo_list[j].printValues())
Deaconess answered 20/6 at 15:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.