Credit: Brent Burley
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 its functionality when necessary. Rather than solving the
problems of just one application, this gives you a reusable 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) 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( )
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 the 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 implementation, only single-selection is allowed, but it 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.
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 is traditional, but if you
share in the widespread dislike for lambda
, note
that lambda
is never truly necessary. In this
case, the easiest way to avoid the lambda
s 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.
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), and various books.
Get Python Cookbook now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.