Here is the scrolledlist.py module that defines the ScrolledList widget.
The module starts with a comment pointing back to this documentation.
"""scrolledlist.py: A Tkinter widget combining a Listbox with Scrollbar(s).
For details, see:
http://www.nmt.edu/tcc/help/lang/python/examples/scrolledlist/
"""
First we import the Tkinter module into our namespace.
#================================================================ # Imports #---------------------------------------------------------------- from Tkinter import *
Next, we define two constants for the default height and width arguments to the
widget's constructor.
#================================================================ # Manifest constants #---------------------------------------------------------------- DEFAULT_WIDTH = "40" DEFAULT_HEIGHT = "25"
Here we start the actual class declaration for ScrolledList.
class ScrolledList(Frame):
"""A compound widget containing a listbox and up to two scrollbars.
Inside the class's documentation string, we document the public and internal attributes. The scrollbar widgets are technically public, in case anyone wants to configure their attributes.
State/invariants:
.listbox: [ The Listbox widget ]
.vScrollbar:
[ if self has a vertical scrollbar ->
that scrollbar
else -> None ]
.hScrollbar:
[ if self has a vertical scrollbar ->
that scrollbar
else -> None ]
.callback: [ as passed to constructor ]
.vscroll: [ as passed to constructor ]
.hscroll: [ as passed to constructor ]
"""
The constructor defines default values for all the keyword arguments. Note that the vertical scrollbar is on by default, while the horizontal scrollbar is off by default.
def __init__ ( self, master=None, width=DEFAULT_WIDTH,
height=DEFAULT_HEIGHT, vscroll=1, hscroll=0, callback=None ):
"""Constructor for ScrolledList.
"""
The constructor's first job is to call the constructor
for its parent class, Frame.
#-- 1 --
# [ self := a new Frame widget child of master ]
Frame.__init__ ( self, master )
Next, we store the various constructor arguments inside the instance.
#-- 2 --
self.width = width
self.height = height
self.vscroll = vscroll
self.hscroll = hscroll
self.callback = callback
Finally, we lay out the internal widgets.
#-- 3 --
# [ self := self with all widgets created and registered ]
self.__createWidgets()
This method creates and grids all our internal widgets.
def __createWidgets ( self ):
"""Lay out internal widgets.
"""
Here is the grid plan for our internal widgets:
| 0 | 1 | |
| 0 | .listbox | .vScrollbar |
| 1 | .hScrollbar |
First, we create the vertical scrollbar, if there is one.
The sticky=N+S attribute makes the scrollbar
stretch to the full height of grid row 0.
#-- 1 --
# [ if self.vscroll ->
# self := self with a vertical Scrollbar widget added
# self.vScrollbar := that widget ]
# else -> I ]
if self.vscroll:
self.vScrollbar = Scrollbar ( self, orient=VERTICAL )
self.vScrollbar.grid ( row=0, column=1, sticky=N+S )
Next, we create the horizontal scrollbar, if there is
one. The sticky=E+W attribute makes it
stretch to the width of grid column 0.
#-- 2 --
# [ if self.hscroll ->
# self := self with a horizontal Scrollbar widget added
# self.hScrollbar := that widget
# else -> I ]
if self.hscroll:
self.hScrollbar = Scrollbar ( self, orient=HORIZONTAL )
self.hScrollbar.grid ( row=1, column=0, sticky=E+W )
Now we create the Listbox widget. The
relief=SUNKEN attribute makes the
listbox's contents look like they are recessed into the
window. The borderwidth=2 attribute puts
a 2-pixel border around the listbox.
#-- 3 --
# [ self := self with a Listbox widget added
# self.listbox := that widget ]
self.listbox = Listbox ( self, relief=SUNKEN,
width=self.width, height=self.height,
borderwidth=2 )
self.listbox.grid ( row=0, column=0 )
The next step is to create the linkages between the
scrollbars and the Listbox. The command attribute of a vertical Scrollbar widget is a method that is called
whenever the scrollbar is scrolled by the user; the yview method of a Listbox widget
causes its contents to be repositioned. This linkage
allows the scrollbar to move the listbox.
However, the linkage is bidirectional. There are
operations on the Listbox widget that
change its contents' position, and when that happens, the
scrollbar's position should also be adjusted. This
linkage sets the yscrollcommand attribute
of the Listbox to the .set() method of the scrollbar.
The linkages are similar for a horizontal scrollbar.
#-- 4 --
# [ if self.vscroll ->
# self.listbox := self.listbox linked so that
# self.vScrollbar can reposition it ]
# self.vScrollbar := self.vScrollbar linked so that
# self.listbox can reposition it
# else -> I ]
if self.vscroll:
self.listbox["yscrollcommand"] = self.vScrollbar.set
self.vScrollbar["command"] = self.listbox.yview
#-- 5 --
# [ if self.hscroll ->
# self.listbox := self.listbox linked so that
# self.hScrollbar can reposition it ]
# self.hScrollbar := self.hScrollbar linked so that
# self.listbox can reposition it
# else -> I ]
if self.hscroll:
self.listbox["xscrollcommand"] = self.hScrollbar.set
self.hScrollbar["command"] = self.listbox.xview
So that our widget will respond to the user clicking on a
line in the listbox, we use the .bind()
method to set up an event binding that will call our
.__clickHandler method when that happens.
#-- 6 --
# [ self.listbox := self.listbox with an event handler
# for button-1 clicks that causes self.callback
# to be called if there is one ]
self.listbox.bind ( "<Button-1>", self.__clickHandler )
This method is called when the user clicks mouse button 1
(usually the left button, but left-handers can set it up
to be the right-hand button). If the user has provided a
callback procedure, we will figure out
which line of the listbox the user clicked on, and pass
the number of that line to that callback.
def __clickHandler ( self, event ):
"""Called when the user clicks on a line in the listbox.
"""
If there is no callback, don't do anything.
#-- 1 --
if not self.callback:
return
The event argument holds the screen
y-coordinate of the mouse click in its .y
attribute. The .nearest() method on the
Listbox widget converts this coordinate
into a line number.
#-- 2 --
# [ call self.callback(c) where c is the line index
# corresponding to event.y ]
lineNo = self.listbox.nearest ( event.y )
self.callback ( lineNo )
So that the listbox will respond to the PageUp and PageDown keys, we move the keyboard focus to the listbox whenever a family is selected.
#-- 3 --
self.listbox.focus_set()
This method returns the number of lines currently in use
inside the listbox. The .size() method on
the Listbox widget is exactly what we
need.
Originally I wanted to define a .__len__() method so that the user could use the Python len() function on a ScrolledList to get the line
count. However, this caused some bizarre bugs, so
it is now a conventional method.
def count ( self ):
"""Return the number of lines in use in the listbox.
"""
return self.listbox.size()
This method is called when the user indexes a ScrolledList widget
to get the text from a specific line of the listbox. The
.get() method on the Listbox widget does just what we need. We raise an IndexError exception if the index is out of
range.
def __getitem__ ( self, k ):
"""Get the (k)th line from the listbox.
"""
#-- 1 --
if ( 0 <= k < self.count() ):
return self.listbox.get ( k )
else:
raise IndexError, ( "ScrolledList[%d] out of range." % k )
This method appends a new last line to the text in the
listbox. The constant END specifies a
position just after the last existing line.
def append ( self, text ):
"""Append a line to the listbox.
"""
self.listbox.insert ( END, text )
This method inserts a line of text before the line at the specified position. If the position is out of range, we'll just place it at the end.
def insert ( self, linex, text ):
"""Insert a line between two existing lines.
"""
#-- 1 --
if 0 <= linex < self.count():
where = linex
else:
where = END
#-- 2 --
self.listbox.insert ( where, text )
This method removes a specified line from the listbox. If the given position is out of range, we do nothing.
def delete ( self, linex ):
"""Delete a line from the listbox.
"""
if 0 <= linex < self.count():
self.listbox.delete ( linex )
Removes all lines from the listbox. The .delete() method on a Listbox
object takes two arguments, a start position and an end
position. A zero value refers to the start of the
listbox, and the constant END refers to
the position just after the last line.
def clear ( self ):
"""Remove all lines.
"""
self.listbox.delete ( 0, END )