Next / Previous / Contents / TCC Help System / NM Tech homepage

5. The ScrolledList module

Here is the scrolledlist.py module that defines the ScrolledList widget.

5.1. Module prologue

The module starts with a comment pointing back to this documentation.

scrolledlist.py
"""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.

scrolledlist.py
#================================================================
# Imports
#----------------------------------------------------------------

from Tkinter import *

Next, we define two constants for the default height and width arguments to the widget's constructor.

scrolledlist.py
#================================================================
# Manifest constants
#----------------------------------------------------------------

DEFAULT_WIDTH   =  "40"
DEFAULT_HEIGHT  =  "25"

5.2. class ScrolledList

Here we start the actual class declaration for ScrolledList.

scrolledlist.py
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.

scrolledlist.py
      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 ]
    """

5.3. ScrolledList.__init__(): 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.

scrolledlist.py
    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.

scrolledlist.py
        #-- 1 --
        # [ self  :=  a new Frame widget child of master ]
        Frame.__init__ ( self, master )

Next, we store the various constructor arguments inside the instance.

scrolledlist.py
        #-- 2 --
        self.width     =  width
        self.height    =  height
        self.vscroll   =  vscroll
        self.hscroll   =  hscroll
        self.callback  =  callback

Finally, we lay out the internal widgets.

scrolledlist.py
        #-- 3 --
        # [ self  :=  self with all widgets created and registered ]
        self.__createWidgets()

5.4. ScrolledList.__createWidgets(): Lay out internal widgets

This method creates and grids all our internal widgets.

scrolledlist.py
    def __createWidgets ( self ):
        """Lay out internal widgets.
        """

Here is the grid plan for our internal widgets:

 01
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.

scrolledlist.py
        #-- 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.

scrolledlist.py
        #-- 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.

scrolledlist.py
        #-- 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.

scrolledlist.py
        #-- 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.

scrolledlist.py
        #-- 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 )

5.5. ScrolledList.__clickHandler(): Event handler for button 1

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.

scrolledlist.py
    def __clickHandler ( self, event ):
        """Called when the user clicks on a line in the listbox.
        """

If there is no callback, don't do anything.

scrolledlist.py
        #-- 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.

scrolledlist.py
        #-- 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.

scrolledlist.py
        #-- 3 --
        self.listbox.focus_set()

5.6. ScrolledList.count(): Return the line count

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.

Note

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.

scrolledlist.py
    def count ( self ):
        """Return the number of lines in use in the listbox.
        """
        return self.listbox.size()

5.7. ScrolledList.__getitem__(): Implement the Python index operator

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.

scrolledlist.py
    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 )

5.8. ScrolledList.append(): Add a line of text

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.

scrolledlist.py
    def append ( self, text ):
        """Append a line to the listbox.
        """
        self.listbox.insert ( END, text )

5.9. ScrolledList.insert(): Insert a line of 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.

scrolledlist.py
    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 )

5.10. ScrolledList.delete(): Remove a line of text

This method removes a specified line from the listbox. If the given position is out of range, we do nothing.

scrolledlist.py
    def delete ( self, linex ):
        """Delete a line from the listbox.
        """
        if  0 <= linex < self.count():
            self.listbox.delete ( linex )

5.11. ScrolledList.clear(): Empty the listbox

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.

scrolledlist.py
    def clear ( self ):
        """Remove all lines.
        """
        self.listbox.delete ( 0, END )