Recipe11.11.Supporting Multiple Values per Row in a Tkinter Listbox


Recipe 11.11. Supporting Multiple Values per Row in a Tkinter Listbox

Credit: Brent Burley, Pedro Werneck, Eric Rose

Problem

You need a Tkinter widget that works just like a normal Listbox but with multiple values per row.

Solution

When you find a functional limitation in Tkinter, most often the best solution is to build your own widget as a Python class, subclassing an appropriate existing Tkinter widget (often Frame, so you can easily aggregate several native Tkinter widgets into your own compound widget) and extending and tweaking the widget's functionality as necessary. Rather than solving a problem for just one application, this approach gives you a component that you can reuse in many applications. For example, here's a way to make a multicolumn equivalent of a Tkinter Listbox:

from Tkinter import * 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)             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     def _select(self, y):         row = self.lists[0].nearest(y)         self.selection_clear(0, END)         self.selection_set(row)         return 'break'     def _button2(self, x, y):         for l in self.lists:             l.scan_mark(x, y)         return 'break'     def _b2motion(self, x, y):         for l in self.lists             l.scan_dragto(x, y)         return 'break'     def _scroll(self, *args):         for l in self.lists:             apply(l.yview, args)         return 'break'     def curselection(self):         return self.lists[0].curselection( )     def delete(self, first, last=None):         for l in self.lists:             l.delete(first, last)     def get(self, first, last=None):         result = [  ]         for l in self.lists:             result.append(l.get(first,last))         if last: return apply(map, [None] + result)         return result     def index(self, index):         self.lists[0].index(index)     def insert(self, index, *elements):         for e in elements:             i = 0             for l in self.lists:                 l.insert(index, e[i])                 i = i + 1     def size(self):         return self.lists[0].size( )     def see(self, index):         for l in self.lists:             l.see(index)     def selection_anchor(self, index):         for l in self.lists:             l.selection_anchor(index)     def selection_clear(self, first, last=None):         for l in self.lists:             l.selection_clear(first, last)     def selection_includes(self, index):         return self.lists[0].selection_includes(index)     def selection_set(self, first, last=None):         for l in self.lists:             l.selection_set(first, last) if _ _name_ _ == '_ _main_ _':     tk = Tk( )     Label(tk, text='MultiListbox').pack( )     mlb = MultiListbox(tk, (('Subject', 40), ('Sender', 20), ('Date', 10)))     for i in range(1000):       mlb.insert(END,            ('Important Message: %d' % i, 'John Doe', '10/10/%04d' % (1900+i)))     mlb.pack(expand=YES, fill=BOTH)     tk.mainloop( )

Discussion

This recipe shows a compound widget that gangs multiple Tk Listbox widgets to a single scrollbar to achieve a simple multicolumn scrolled listbox. Most of the Listbox API is mirrored, to make the widget act like normal Listbox, but with multiple values per row. The resulting widget is lightweight, fast, and easy to use. The main drawback is that only text is supported, which is a fundamental limitation of the underlying Listbox widget.

In this recipe's implementation, only single selection is allowed, but the same idea could be extended to multiple selection. User-resizable columns and auto-sorting by clicking on the column label should also be possible. Auto-scrolling while dragging Button-1 was disabled because it broke the synchronization between the lists. However, scrolling with Button-2 works fine. Mice with scroll wheels appear to behave in different ways depending on the platform. For example, while things appear to work fine with the preceding code on some platforms (such as Windows/XP), on other platforms using X11 (such as Linux), I've observed that mouse scroll wheel events correspond to Button-4 and Button-5, so you could deal with them just by adding at the end of the for loop in method _ _init_ _ the following two statements:

    lb.bind('<Button-4>', lambda e, s=self: s._scroll(SCROLL, -1, UNITS))     lb.bind('<Button-5>', lambda e, s=self: s._scroll(SCROLL, +1, UNITS))

This addition should be innocuous on platforms such as Windows/XP. You should check this issue on all platforms on which you need to support mouse scroll wheels.

If you need to support sorting by column-header clicking, you can obtain the hook needed for that functionality with a fairly modest change to this recipe's code. Specifically, within the for loop in method _ _init_ _, you can change the current start:

        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)

to the following richer code:

        for l, w, sort_command in lists:             frame = Frame(self)             frame.pack(side=LEFT, expand=YES, fill=BOTH)             Button(frame, text=l, borderwidth=1, relief=RAISED,                    command=sort_command).pack(fill=X)

To take advantage of this hook, you then need to pass as the lists' argument, rather than one tuple of pairs, a list of three tuples, the third item of each tuple being an object callable with no arguments to perform the appropriate kind of sorting. In my applications, I've generally found this specific refinement to be more trouble than it's worth, but I'm presenting it anyway (although not in the actual "Solution" of this recipe!) just in case your applications differ in this respect. Maybe sorting by column header clicking is something that's absolutely invaluable to you.

One note about the implementation: in the MultiListbox._ _init_ _ method, several lambda forms are used as the callable second arguments (callbacks) of the bind method calls on the contained Listbox widgets. This approach is traditional, but if you share the widespread dislike for lambda, you should know that lambda is never truly necessary. In this case, the easiest way to avoid the lambdas is to redefine all the relevant methods (_select, _button2, etc.) as taking two formal arguments (self, e) and extract the data they need from argument e. Then in the bind calls, you can simply pass the bound self._select method, and so on.

See Also

Information about Tkinter can be obtained from a variety of sources, such as Pythonware's An Introduction to Tkinter, by Fredrik Lundh (http://www.pythonware.com/library), New Mexico Tech's Tkinter Reference (http://www.nmt.edu/tcc/help/lang/python/docs.html), Python in a Nutshell, and various other books.



Python Cookbook
Python Cookbook
ISBN: 0596007973
EAN: 2147483647
Year: 2004
Pages: 420

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net