#!/usr/bin/python3 """ ==================================================================================== A simple, customizable, and attachable scrolled listbox component, adopted from the book _Programming Python, 4th Edition_. Modified here to support: (1) separate label/action tables (and thus allow duplicate labels); (2) single and double left-click modes; and (3) right-click events and actions that use the selected or nearest item. Left and right click handlers also receive the click's tk event object for pixel position, if needed. Inputs: labels is [strings], both actions are [one-argument-callables]. In the listbox, left and right clicks run actions in leftactions and rightactions, respectively, which correspond to clicked labels by position. See comments ahead for more on left-click and right-item modes. The caller must sanitize non-BMP Unicode characters in labels for Tk versions through 8.6 if required. ==================================================================================== """ import sys from tkinter import * class ScrolledList(Frame): #---------------------------------------------------------------------- # activate item's left-click callback via single or double left-click; # a single-click in left-double mode just highlights the item, which # may seem pointless, but might be used for right-click item choice; # note: a click in left-single mode still highlights the item (per tk # built-in code), which may matter only if the listbox is persistent; #---------------------------------------------------------------------- leftclickmode = 'single' # or 'double' (single requires right nearest) #---------------------------------------------------------------------- # right-click item choice: selected item, or item nearest to click; # Selected assumes item selected by left-single and requires left-double # mode, but may use either nearest or selected in left-double mode; # right-clicks never highlight the item, per tk's standard behavior; #---------------------------------------------------------------------- rightclickitem = 'nearest' # or 'selected' (selected requires left double) def __init__(self, labels, leftactions, rightactions, parent=None, side=TOP): # sanity checks assert len(labels) == len(leftactions) assert len(labels) == len(rightactions) # these settings can be overridden in subclasses assert self.leftclickmode in ('single', 'double') assert self.rightclickitem in ('nearest', 'selected') # invalid case: single + selected if self.leftclickmode == 'single': assert self.rightclickitem == 'nearest' # but double => nearest or selected if self.rightclickitem == 'selected': assert self.leftclickmode == 'double' # but nearest => double or single # build the widget Frame.__init__(self, parent) self.pack(expand=YES, fill=BOTH, side=side) # make me expandable self.makeWidgets(labels) # caller: self.config(bg=x, bd=y,...) self.leftactions = leftactions # caller: self.listbox.config/itemconfig() self.rightactions = rightactions def handleListLeft(self, tkevent): """ on list single- or double-left-click single mode: use item nearest to click double mode: use item selected (single left-click just selects an item) """ if self.leftclickmode == 'single': # activate item nearest click: index = self.listbox.nearest(tkevent.y) # index of nearest item (rel to widget) elif self.leftclickmode == 'double': # activate item selected by single-left: index = self.listbox.curselection() # get selected item index index = int(index[0]) # index= (digitstring,) tuple, 0..N-1 else: assert False, 'invalid leftclickmode setting' self.leftactions[index](tkevent) # call corresponding action with event def handleListRight(self, tkevent): """ on list single-right-click use item nearest to click, or formerly selected by left-click """ if self.rightclickitem == 'nearest': # activate item nearest click: index = self.listbox.nearest(tkevent.y) # index of nearest item (rel to widget) elif self.rightclickitem == 'selected': # activate item selected by single-left: index = self.listbox.curselection() # get selected item index index = int(index[0]) # index= (digitstring,) tuple, 0..N-1 else: assert False, 'invalid rightclickitem setting' self.rightactions[index](tkevent) # call corresponding action with event def makeWidgets(self, labels): """ build the GUI: listbox, scroll, callbacks; always uses default single selection and resize modes, as in: list.config(selectmode=SINGLE, setgrid=1) """ # crosslink listbox, vertical scrollbar sbar = Scrollbar(self) lbox = Listbox(self, relief=SUNKEN) sbar.config(command=lbox.yview) # xlink sbar and list lbox.config(yscrollcommand=sbar.set) # move one moves other sbar.pack(side=RIGHT, fill=Y) # pack first=clip last lbox.pack(side=LEFT, expand=YES, fill=BOTH) # list clipped first # fill listbox with labels for (pos, label) in enumerate(labels): # add to listbox lbox.insert(pos, label) # or insert(END,label) # set left/right click handlers if self.leftclickmode == 'single': # set left-click event handler lbox.bind('', self.handleListLeft) # single-left activates item else: lbox.bind('', self.handleListLeft) # single-left only selects item lbox.bind('', self.handleListRight) # set right-click event handler # [2.0] on Mac OS X, also allow Control-click as an equivalent for right-click, # and support Mac mice that trigger Button-2 on right button click (on Macs, # right=Button-2 and middle=Button-3; it's the opposite on Windows and Linux!) if sys.platform == 'darwin': lbox.bind('', self.handleListRight) lbox.bind('', self.handleListRight) self.listbox = lbox if __name__ == '__main__': # run to test labels = ('spam', 'toast', 'spam', 'eggs') # duplicates: map by index lactions = [lambda e, i=i: print(i) for i in range(4)] # print(0), print(1), ... (last i!) ractions = [lambda e, i=i: print(i) for i in range(4, 8)] # print(4)...print(7) ScrolledList(labels, lactions, ractions).mainloop()