1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74 from Tkinter import *
75
76 MOVE_LINES = 0
77 MOVE_PAGES = 1
78 MOVE_TOEND = 2
79
80 class MultiListbox(Frame):
81 """
82 MultiListbox Class.
83
84 Defines a multi-column listbox. The constructor takes a list of
85 tuples, where each tuple is (column-label, character-width). The
86 list will have as many columns as tuples. Add items to the list by
87 passing tuples or lists of items, one for each column.
88
89 Each column will be given the specified width in character units,
90 with a header of column-label. Also takes many of the normal
91 Listbox options for background, font, etc.
92 """
93 def __init__(self, master, lists, command=None, **options):
94 defaults = {
95 'background': None,
96 'borderwidth': 2,
97 'font': None,
98 'foreground': None,
99 'height': 10,
100 'highlightcolor': None,
101 'highlightthickness': 1,
102 'relief': SUNKEN,
103 'takefocus': 1,
104 }
105
106 aliases = {'bg':'background', 'fg':'foreground', 'bd':'borderwidth'}
107
108 for k in aliases.keys ():
109 if options.has_key (k):
110 options [aliases[k]] = options [k]
111
112 for key in defaults.keys():
113 if not options.has_key (key):
114 options [key] = defaults [key]
115
116 apply (Frame.__init__, (self, master), options)
117 self.lists = []
118
119
120
121 self.colmapping={}
122 self.origData = None
123
124
125
126 self.bind ('<Up>', lambda e, s=self: s._move (-1, MOVE_LINES))
127 self.bind ('<Down>', lambda e, s=self: s._move (+1, MOVE_LINES))
128 self.bind ('<Prior>', lambda e, s=self: s._move (-1, MOVE_PAGES))
129 self.bind ('<Next>', lambda e, s=self: s._move (+1, MOVE_PAGES))
130 self.bind ('<Home>', lambda e, s=self: s._move (-1, MOVE_TOEND))
131 self.bind ('<End>', lambda e, s=self: s._move (+1, MOVE_TOEND))
132 if command:
133 self.bind ('<Return>', command)
134
135
136
137
138
139
140 m = PanedWindow(self, orient=HORIZONTAL, bd=0,
141 background=options['background'], showhandle=0, sashpad=1)
142 m.pack(side=LEFT, fill=BOTH, expand=1)
143
144 for label, width in lists:
145 lbframe = Frame(m)
146 m.add(lbframe, width=width)
147
148
149 b = Label(lbframe, text=label, borderwidth=1, relief=RAISED)
150 b.pack(fill=X)
151 b.bind('<Button-1>', self._sort)
152
153 self.colmapping[b]=(len(self.lists),1)
154
155 lb = Listbox (lbframe,
156 width=width,
157 height=options ['height'],
158 borderwidth=0,
159 font=options ['font'],
160 background=options ['background'],
161 selectborderwidth=0,
162 relief=SUNKEN,
163 takefocus=FALSE,
164 exportselection=FALSE)
165 lb.pack (expand=YES, fill=BOTH)
166 self.lists.append (lb)
167
168
169
170 lb.bind ('<B1-Motion>', lambda e, s=self: s._select (e.y))
171 lb.bind ('<Button-1>', lambda e, s=self: s._select (e.y))
172 lb.bind ('<Leave>', lambda e: 'break')
173 lb.bind ('<B2-Motion>', lambda e, s=self: s._b2motion (e.x, e.y))
174 lb.bind ('<Button-2>', lambda e, s=self: s._button2 (e.x, e.y))
175 if command:
176 lb.bind ('<Double-Button-1>', command)
177
178 sbframe = Frame (self)
179 sbframe.pack (side=LEFT, fill=Y)
180 l = Label (sbframe, borderwidth=1, relief=RAISED)
181 l.bind ('<Button-1>', lambda e, s=self: s.focus_set ())
182 l.pack(fill=X)
183 sb = Scrollbar (sbframe,
184 takefocus=FALSE,
185 orient=VERTICAL,
186 command=self._scroll)
187 sb.pack (expand=YES, fill=Y)
188 self.lists[0]['yscrollcommand']=sb.set
189
190 return
191
192
193
194
195
196
197 def _sort(self, e):
198
199 b=e.widget
200 col, direction = self.colmapping[b]
201
202
203 tableData = self.get(0,END)
204 if self.origData == None:
205 import copy
206 self.origData = copy.deepcopy(tableData)
207
208 rowcount = len(tableData)
209
210
211 for btn in self.colmapping:
212 lab = btn.cget('text')
213 if lab[0]=='[': btn.config(text=lab[4:])
214
215 btnLabel = b.cget('text')
216
217 if direction==0:
218 tableData = self.origData
219 else:
220 if direction==1: b.config(text='[+] ' + btnLabel)
221 else: b.config(text='[-] ' + btnLabel)
222
223 tableData.sort(key=lambda x: x[col], reverse=direction<0)
224
225
226 self.delete(0,END)
227
228
229 for row in range(rowcount):
230 self.insert(END, tableData[row])
231
232
233 if direction==1: direction=-1
234 else: direction += 1
235 self.colmapping[b] = (col, direction)
236
237
238 def _move (self, lines, relative=0):
239 """
240 Move the selection a specified number of lines or pages up or
241 down the list. Used by keyboard navigation.
242 """
243 selected = self.lists [0].curselection ()
244 try:
245 selected = map (int, selected)
246 except ValueError:
247 pass
248
249 try:
250 sel = selected [0]
251 except IndexError:
252 sel = 0
253
254 old = sel
255 size = self.lists [0].size ()
256
257 if relative == MOVE_LINES:
258 sel = sel + lines
259 elif relative == MOVE_PAGES:
260 sel = sel + (lines * int (self.lists [0]['height']))
261 elif relative == MOVE_TOEND:
262 if lines < 0:
263 sel = 0
264 elif lines > 0:
265 sel = size - 1
266 else:
267 print "MultiListbox._move: Unknown move type!"
268
269 if sel < 0:
270 sel = 0
271 elif sel >= size:
272 sel = size - 1
273
274 self.selection_clear (old, old)
275 self.see (sel)
276 self.selection_set (sel)
277 return 'break'
278
279
280 def _select (self, y):
281 """
282 User clicked an item to select it.
283 """
284 row = self.lists[0].nearest (y)
285 self.selection_clear (0, END)
286 self.selection_set (row)
287 self.focus_set ()
288 return 'break'
289
290
291 def _button2 (self, x, y):
292 """
293 User selected with button 2 to start a drag.
294 """
295 for l in self.lists:
296 l.scan_mark (x, y)
297 return 'break'
298
299
300 def _b2motion (self, x, y):
301 """
302 User is dragging with button 2.
303 """
304 for l in self.lists:
305 l.scan_dragto (x, y)
306 return 'break'
307
308
309 def _scroll (self, *args):
310 """
311 Scrolling with the scrollbar.
312 """
313 for l in self.lists:
314 apply(l.yview, args)
315
316 def curselection (self):
317 """
318 Return index of current selection.
319 """
320 return self.lists[0].curselection()
321
322
323 def delete (self, first, last=None):
324 """
325 Delete one or more items from the list.
326 """
327 for l in self.lists:
328 l.delete(first, last)
329
330
331 def get (self, first, last=None):
332 """
333 Get items between two indexes, or one item if second index
334 is not specified.
335 """
336 result = []
337 for l in self.lists:
338 result.append (l.get (first,last))
339 if last:
340 return apply (map, [None] + result)
341 return result
342
343
344 def index (self, index):
345 """
346 Adjust the view so that the given index is at the top.
347 """
348 for l in self.lists:
349 l.index (index)
350
351
352 def insert (self, index, *elements):
353 """
354 Insert list or tuple of items.
355 """
356 for e in elements:
357 i = 0
358 for l in self.lists:
359 l.insert (index, e[i])
360 i = i + 1
361 if self.size () == 1:
362 self.selection_set (0)
363
364
365 def size (self):
366 """
367 Return the total number of items.
368 """
369 return self.lists[0].size ()
370
371
372 def see (self, index):
373 """
374 Make sure given index is visible.
375 """
376 for l in self.lists:
377 l.see (index)
378
379
380 def selection_anchor (self, index):
381 """
382 Set selection anchor to index.
383 """
384 for l in self.lists:
385 l.selection_anchor (index)
386
387
388 def selection_clear (self, first, last=None):
389 """
390 Clear selections between two indexes.
391 """
392 for l in self.lists:
393 l.selection_clear (first, last)
394
395
396 def selection_includes (self, index):
397 """
398 Determine if given index is selected.
399 """
400 return self.lists[0].selection_includes (index)
401
402
403 def selection_set (self, first, last=None):
404 """
405 Select a range of indexes.
406 """
407 for l in self.lists:
408 l.selection_set (first, last)
409
410
411 if __name__ == '__main__':
412 tk = Tk()
413 Label(tk, text='MultiListbox').pack()
414 mlb = MultiListbox (tk,
415 (('Subject', 150),
416 ('Sender', 100),
417 ('Date', 100)),
418 height=20,
419 bg='white')
420 for i in range (100):
421 mlb.insert (END, ('Important Message: %d' % i,
422 'John Doe',
423 '10/10/%04d' % (1900+i)))
424 mlb.pack (expand=YES,fill=BOTH)
425 Button (tk, text="button").pack ()
426 tk.mainloop()
427