Syntactic Fructose
Syntactic Fructose

Reputation: 20076

Tkinter: select multiple items in MultiListBox

I'm having trouble modifying an existing MultiListBox implementation to allow me to select multiple items using the shift key. The constructor of the class is:

class MultiListbox(Frame):
    def __init__(self, master, lists):
        Frame.__init__(self, master)
        self.lists=[]
        for l,w in lists:
            frame = Frame(self); frame.pack(side=LEFT, expand=YES, fill=BOTH)
            Label(frame, text=l, borderwidth=1, relief=RAISED).pack(fill=X)
            lb = Listbox(frame, width=w, borderwidth=0, selectborderwidth=0,
                relief=FLAT, exportselection=FALSE, selectmode=EXTENDED)
            lb.pack(expand=YES, fill=BOTH)
            self.lists.append(lb)
            lb.bind('<B1-Motion>', lambda e, s=self: s._select(e.y))
            lb.bind('<Button-1>', lambda e, s=self: s._select(e.y))
            lb.bind('<Leave>', lambda e: 'break')
            lb.bind('<B2-Motion>', lambda e, s=self: s._b2motion(e.x, e.y))
            lb.bind('<Button-2>', lambda e, s=self: s._button2(e.x, e.y))
        frame = Frame(self); frame.pack(side=LEFT, fill=Y)
        Label(frame, borderwidth=1, relief=RAISED).pack(fill=X)
        sb = Scrollbar(frame, orient=VERTICAL, command=self._scroll)
        sb.pack(expand=YES, fill=Y)

        self.lists[0]['yscrollcommand']=sb.set

Even though I set the selectmode=EXTENDED, there is no extended selection.

What function would I need to implement for multilistbox in order to support extended selection?

You can see the entire implementation of multilistbox here.

Upvotes: 1

Views: 3177

Answers (2)

user4171906
user4171906

Reputation:

Unfortunately it looks like you have to roll your own, at least as far as I can tell. The code below is not complete as it will do thing like accept the same selection twice, but it's all I have time for. There is no down-arrow code but that it essentially the same as the up-arrow code. I assume you want a button press to display or use the items selected. Below they are just printed after Tkinter is closed.

import sys
if sys.version_info[0] < 3:
    import Tkinter as tk    ## Python 2.x
    import tkFont
    import ttk
else:
    import tkinter as tk    ## Python 3.x
    import tkinter.font as tkFont
    import tkinter.ttk as ttk

'''

ttk_multicolumn_listbox2.py

Python31 includes the Tkinter Tile extension ttk.

Ttk comes with 17 widgets, 11 of which already exist in Tkinter:
Button, Checkbutton, Entry, Frame, Label, LabelFrame, Menubutton,
PanedWindow, Radiobutton, Scale and Scrollbar

he 6 new widget classes are:
Combobox, Notebook, Progressbar, Separator, Sizegrip and Treeview

For additional info see the Python31 manual:
http://gpolo.ath.cx:81/pydoc/library/ttk.html

Here the TreeView widget is configured as a multi-column listbox
with adjustable column width and column-header-click sorting.

Tested with Python 3.1.1 and Tkinter 8.5
'''

class McListBox(object):
    """use a ttk.TreeView as a multicolumn ListBox"""
    def __init__(self, root):
        self.root=root
        self.tree = None
        self._setup_widgets()
        self._build_tree()
        ttk.Button(self.root, text='Exit',
                   command=self.root.quit).grid(row=20)
        self.selected_offsets=[]

    def _setup_widgets(self):
        container = ttk.Frame(self.root)
        container.grid(sticky="nsew")

        # create a treeview with dual scrollbars
        self.tree = ttk.Treeview(columns=car_header, show="headings",
                                 selectmode="extended")
        vsb = ttk.Scrollbar(orient="vertical",
            command=self.tree.yview)
        hsb = ttk.Scrollbar(orient="horizontal",
            command=self.tree.xview)
        self.tree.configure(yscrollcommand=vsb.set,
            xscrollcommand=hsb.set)
        self.tree.grid(column=0, row=0, sticky='nsew', in_=container)
        vsb.grid(column=1, row=0, sticky='ns', in_=container)
        hsb.grid(column=0, row=1, sticky='ew', in_=container)

        container.grid_columnconfigure(0, weight=1)
        container.grid_rowconfigure(0, weight=1)
        self.tree.bind("<Shift-Up>", self.shift_up_arrow)
        self.tree.bind("<Up>", self.up_arrow)

    def _build_tree(self):
        for col in car_header:
            self.tree.heading(col, text=col.title())
            # adjust the column's width to the header string
            self.tree.column(col,
                width=tkFont.Font().measure(col.title()))

        self.item_id=[]
        for item in car_list:
            self.item_id.append(self.tree.insert('', 'end', values=item))  ## store tkinter id
            # adjust column's width if necessary to fit each value
            for ix, val in enumerate(item):
                col_w = tkFont.Font().measure(val)
                if self.tree.column(car_header[ix],width=None)<col_w:
                    self.tree.column(car_header[ix], width=col_w)

        ## set the focus in the middle for testing
        new_id=self.item_id[5]
        self.tree.focus_set()       ## sets focus to the treeview
        self.tree.selection_set((new_id, new_id))  ## updates background
        self.tree.focus(new_id)     ## sets new id as focus

    def shift_up_arrow(self, event):
        """ gets selected item(s) and stores them in a list
        """
        id_selected=self.tree.focus()
        for offset, id in enumerate(self.item_id):
##            print offset, id
            if id==id_selected:
                self.selected_offsets.append(offset)## save selection
                print offset, car_list[offset]

                ## change background color
                ## you could also give each row a unique tag and
                ## tag_configure the selected row's tag
                self.tree.delete(id_selected)
                self.tree.insert('', offset, id_selected,
                                 values=car_list[offset], tags=("ABC",))
                self.tree.tag_configure("ABC", background='yellow')

                new_id=self.item_id[0]
                if offset > 0:
                    new_id=self.item_id[offset]
                self.tree.focus_set()       ## sets focus to the treeview
                self.tree.selection_set((new_id, new_id))  ## updates background
                self.tree.focus(new_id)     ## sets new id as focus
                return

    def up_arrow(self, event):
        id_selected=self.tree.focus()
        for offset, id in enumerate(self.item_id):
            if id==id_selected and offset > 0:
                new_id=self.item_id[offset]
                self.tree.focus_set()       ## sets focus to the treeview
                self.tree.selection_set((new_id, new_id))  ## updates background
                self.tree.focus(new_id)     ## sets new id as focus

# the test data ...
car_header = ['car', 'repair']
car_list = [
('Hyundai', 'brakes') ,
('Honda', 'light') ,
('Lexus', 'battery') ,
('Benz', 'wiper') ,
('Ford', 'tire') ,
('Chevy', 'air') ,
('Chrysler', 'piston') ,
('Toyota', 'brake pedal') ,
('BMW', 'seat'),
('Audi', 'starter'),
('Fiat', 'shocks'),
('Porsche', 'fuel pump')
]

root = tk.Tk()
root.wm_title("multicolumn ListBox")
mc_listbox = McListBox(root)
root.mainloop()
##
## print selected items
print "Selections %s" %  ("-"*50)
for offset in mc_listbox.selected_offsets:
    print offset, car_list[offset]

Upvotes: 1

user4171906
user4171906

Reputation:

I prefer vegaseat's method using ttk.TreeView because all of the columns are linked to the row, which makes things easier. This slight modification of his example prints the id when any column in the row is selected. You do want to test/print the id's as you will have to convert the id to the row number/list offset, i.e. capture the return from self.tree.insert. Will look into multiple selection more tonight. And will try using keys instead of button.

import sys
if sys.version_info[0] < 3:
    import Tkinter as tk    ## Python 2.x
    import tkFont
    import ttk
else:
    import tkinter as tk    ## Python 3.x
    import tkinter.font as tkFont
    import tkinter.ttk as ttk

'''

ttk_multicolumn_listbox2.py

Python31 includes the Tkinter Tile extension ttk.

Ttk comes with 17 widgets, 11 of which already exist in Tkinter:
Button, Checkbutton, Entry, Frame, Label, LabelFrame, Menubutton,
PanedWindow, Radiobutton, Scale and Scrollbar

he 6 new widget classes are:
Combobox, Notebook, Progressbar, Separator, Sizegrip and Treeview

Here the TreeView widget is configured as a multi-column listbox
with adjustable column width and column-header-click sorting.

Tested with Python 3.1.1 and Tkinter 8.5
'''

class McListBox(object):
    """use a ttk.TreeView as a multicolumn ListBox"""
    def __init__(self, root):
        self.root=root
        self.tree = None
        self._setup_widgets()
        self._build_tree()
        ttk.Button(self.root, text='Exit',
                   command=self.root.quit).grid(row=20)

    def _setup_widgets(self):
        # create a treeview with dual scrollbars
        container = ttk.Frame(self.root)
        container.grid(sticky="nsew")

        self.tree = ttk.Treeview(columns=car_header, show="headings")
        vsb = ttk.Scrollbar(orient="vertical",
            command=self.tree.yview)
        hsb = ttk.Scrollbar(orient="horizontal",
            command=self.tree.xview)
        self.tree.configure(yscrollcommand=vsb.set,
            xscrollcommand=hsb.set)
        self.tree.grid(column=0, row=0, sticky='nsew', in_=container)
        vsb.grid(column=1, row=0, sticky='ns', in_=container)
        hsb.grid(column=0, row=1, sticky='ew', in_=container)

        container.grid_columnconfigure(0, weight=1)
        container.grid_rowconfigure(0, weight=1)
        self.tree.bind("<Button1-Motion>", self.selection)

    def _build_tree(self):
        for col in car_header:
            self.tree.heading(col, text=col.title())
            # adjust the column's width to the header string
            self.tree.column(col,
                width=tkFont.Font().measure(col.title()))

        for item in car_list:
            print self.tree.insert('', 'end', values=item)
            # adjust column's width if necessary to fit each value
            for ix, val in enumerate(item):
                col_w = tkFont.Font().measure(val)
                if self.tree.column(car_header[ix],width=None)<col_w:
                    self.tree.column(car_header[ix], width=col_w)


    def selection(self, event):
        """ gets selected item id and prints them
        """
        id = self.tree.identify_row(event.y)
        print "selection", id


# the test data ...
car_header = ['car', 'repair']
car_list = [
('Hyundai', 'brakes') ,
('Honda', 'light') ,
('Lexus', 'battery') ,
('Benz', 'wiper') ,
('Ford', 'tire') ,
('Chevy', 'air') ,
('Chrysler', 'piston') ,
('Toyota', 'brake pedal') ,
('BMW', 'seat'),
('Audi', 'starter'),
('Fiat', 'shocks'),
('Porsche', 'fuel pump')
]

root = tk.Tk()
root.wm_title("multicolumn ListBox")
mc_listbox = McListBox(root)
root.mainloop()

Upvotes: 2

Related Questions