mhMultiListBox


   1 ######################################################################
   2 #
   3 #    File:       mhMultiListBox.py
   4 #
   5 #    Purpose:    Multi-column list box.  Acts mostly like a regular
   6 #                tkinter listbox + scrollbar, but supports multiple
   7 #                columns with heading labels.
   8 #
   9 #                o Enhanced version of MultiListBox.py by Bob Hauck
  10 #                  which can be found at <http://www.haucks.org/>
  11 #                o Based on work by Brent Burley found at:
  12 #                  
  13 #<http://aspn.activestate.com/ASPN/Python/Cookbook/Recipe/52266>
  14 #                o Uses code of SortedTable by Rick Lawson found at
  15 #                  <http://tkinter.unpythonic.net/wiki/SortableTable>
  16 #
  17 #                Features:
  18 #                  o Can call a command on <return> or double-click.
  19 #                  o Grabs focus when clicked.
  20 #                  o Columns are resizable.
  21 #                  o Properly handles keyboard navigation.
  22 #                  o Sorts when solumn labels are clicked.
  23 #                  o Includes a vertical scrollbar.
  24 #                  o No external libraries except tkinter.
  25 #                   
  26 #                Limitations:
  27 #                  o Only single selection.
  28 #                  o No horizontal scrolling.
  29 #                  o Vertical scrollbar is always present.
  30 #
  31 #    Language:   Python 2.3
  32 #
  33 #    Author:     Bob Hauck
  34 #    Copyright 2001, Codem Systems Inc.,  All rights reserved.
  35 #
  36 #    Codem Systems, Inc.
  37 #    7 Executive Park Drive, 
  38 #    Merrimack, NH  03054
  39 #
  40 # License
  41 # -------
  42 #
  43 # Redistribution and use in source and binary forms, with or without
  44 # modification, are permitted provided that the following conditions
  45 # are met:
  46 #
  47 # 1. Redistributions of source code must retain the above copyright
  48 #    notice, this list of conditions and the following disclaimer.
  49 # 
  50 # 2. Redistributions in binary form must reproduce the above copyright
  51 #    notice, this list of conditions and the following disclaimer in the
  52 #    documentation and/or other materials provided with the distribution.
  53 #
  54 # 3. Neither the name of the author nor the names of any contributors
  55 #    may be used to endorse or promote products derived from this software
  56 #    without specific prior written permission.
  57 #
  58 # Warranty
  59 # --------
  60 #
  61 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  62 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  63 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  64 # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  65 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  66 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  67 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  68 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  69 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  70 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  71 # SUCH DAMAGE.
  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         # MH (05/20049
 120         # These are needed for sorting
 121         self.colmapping={}
 122         self.origData = None
 123 
 124         #  Keyboard navigation.
 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         # Columns are a frame with listbox and label in it.
 136         
 137         # MH (05/2004):
 138         # Introduced a PanedWindow to make the columns resizable
 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             # MH (05/2004)
 148             # modified this, click to sort
 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             # Mouse features
 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     # MH (05/2004)
 194     # Sort function, adopted from Rick Lawson 
 195     # http://tkinter.unpythonic.net/wiki/SortableTable
 196     
 197     def _sort(self, e):
 198         # get the listbox to sort by (mapped by the header button)
 199         b=e.widget
 200         col, direction = self.colmapping[b]
 201 
 202         # get the entire table data into mem
 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         #remove old sort indicators if it exists
 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         #sort data based on direction
 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             # sort by col
 223             tableData.sort(key=lambda x: x[col], reverse=direction<0)
 224 
 225         #clear widget
 226         self.delete(0,END)
 227 
 228         # refill widget
 229         for row in range(rowcount):
 230             self.insert(END, tableData[row])
 231 
 232         # toggle direction flag 
 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 

tkinter: mhMultiListBox (last edited 2012-04-20 00:49:50 by newacct)