<!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
 "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd"
  [
    <!ENTITY sl   "<code>ScrolledList</code>">
    <!ENTITY slpy "<filename>scrolledlist.py</filename>">
    <!ENTITY slt  "<code>scrolledlisttest</code>">
    <!ENTITY DEFAULT-WIDTH   "40">
    <!ENTITY DEFAULT-HEIGHT  "25">
    <!ENTITY selfURL
      "http://www.nmt.edu/tcc/help/lang/python/examples/scrolledlist/">
  ]
>
<article>
  <articleinfo>
    <title>&sl;: A Tkinter scrollable list widget</title>
    <titleabbrev>&sl;</titleabbrev>
    <authorgroup>
      <author>
        <firstname>John W.</firstname>
        <surname>Shipman</surname>
      </author>
    </authorgroup>
    <address><email>tcc-doc@nmt.edu</email>
    </address>
    <revhistory>
      <revision>
        <revnumber>$Revision: 1.6 $</revnumber>
        <date>$Date: 2008/01/09 05:50:05 $</date>
      </revision>
    </revhistory>
    <abstract>
      <para>
        Describes a graphical user interface widget that combines
        a list box and scroll bars, for the Python programming
        language's Tkinter widget set.
      </para>
      <para>
        This publication is available in <ulink url="&selfURL;"
        >Web form</ulink > and also as a <ulink
        url="&selfURL;scrolledlist.pdf" >PDF document</ulink >.
        Please forward any comments to <userinput
        >tcc-doc@nmt.edu</userinput >.
      </para>
    </abstract>
  </articleinfo>
  <section id='intro'>
    <title>Introduction</title>
    <para>
      This document describes the usage and implementation of a
      graphical user interface widget called a &sl;, whose
      purpose is to display a list of lines of text.  This widget
      combines a <firstterm >listbox</firstterm > with one or
      more <firstterm >scrollbars</firstterm >, so the user can
      use the scrollbars to see all the text even if it doesn't
      all fit in the available screen space.
    </para>
    <para>
      This widget works with the Tkinter graphical user interface
      for the Python programming language.
    </para>
    <para>
      The document also contains the actual code of the widget,
      explained in a lightweight literate programming style,
      along with a small test driver script named &slt;.
    </para>
    <para>
      Relevant documents:
    </para>
    <itemizedlist>
      <listitem>
        <para>
          <ulink url='http://www.nmt.edu/tcc/help/pubs/python22/'
          ><citetitle >Python 2.2 quick reference</citetitle
          ></ulink > describes the Python language.
        </para>
      </listitem>
      <listitem>
        <para>
          <ulink url='http://www.nmt.edu/tcc/help/pubs/tkinter/'
          ><citetitle >Tkinter reference: a GUI for
          Python</citetitle ></ulink > describes the Tkinter
          widget set.
        </para>
      </listitem>
      <listitem>
        <para>
          See <ulink
          url='http://www.nmt.edu/~shipman/soft/litprog/' >the
          author's Lightweight Literate Programming page</ulink >
          for an explanation of the technique of embedding the
          program in its internal documentation.
        </para>
      </listitem>
      <listitem>
        <para>
          See also <ulink
          url='http://www.nmt.edu/~shipman/soft/clean/' >the
          author's page on the Cleanroom software
          methodology</ulink > for a discussion of the author's 
          preferred style of implementation.
        </para>
      </listitem>
    </itemizedlist>
    <para>
      Files generated from this document are available online:
    </para>
    <itemizedlist>
      <listitem>
        <para>
          <ulink
            url='http://www.nmt.edu/tcc/help/lang/python/examples/scrolledlist/scrolledlist.py'
          >&slpy;</ulink >, the module defining the &sl;
          widget.
        </para>
      </listitem>
      <listitem>
        <para>
          <ulink
url='http://www.nmt.edu/tcc/help/lang/python/examples/scrolledlist/scrolledlisttest'
          >&slt;</ulink >, a small test driver.
        </para>
      </listitem>
      <listitem>
        <para>
          <ulink
            url='http://www.nmt.edu/tcc/help/lang/python/examples/scrolledlist/scrolledlist.xml'
          ><filename >scrolledlist.xml</filename ></ulink >,
          the DocBook source file for this document.
        </para>
      </listitem>
    </itemizedlist>
  </section> <!--intro-->
  <section id='layout'>
    <title>Layout of the &sl; widget</title>
    <para>
      The parts of a &sl; widget visible to the user are:
    </para>
    <itemizedlist>
      <listitem>
        <para>
          A Tkinter <code >Listbox</code > widget that displays
          the lines of text.
        </para>
      </listitem>
      <listitem>
        <para>
          An optional vertical <code >Scrollbar</code > widget
          that allows the user to scroll up and down through the
          lines of text.
        </para>
      </listitem>
      <listitem>
        <para>
          An optional horizontal <code >Scrollbar</code > that
          allows the user to shift the text in the <code
          >Listbox</code > to the left or right when the lines
          are too long to fit in the window.
        </para>
      </listitem>
    </itemizedlist>
    <para>
      Here is a diagram showing the layout of these components in
      the most general case, with both vertical and horizontal
      scrollbars:
    </para>
    <informalfigure>
      <!--This artwork was created using xfig; the source file
       !  is "mockup.fig".
       !    - HTML: Export as PNG at 100%.
       !    - PDF: Export as PDF at 100%.
       !-->
      <mediaobject>
        <imageobject role="html">
          <imagedata fileref="mockup.png"/>
        </imageobject>
        <imageobject role="fo">
          <imagedata fileref="mockup.pdf"/>
        </imageobject>
      </mediaobject>
    </informalfigure>
  </section> <!--layout-->
  <section id='using'>
    <title>Using a &sl; widget in your Tkinter application</title>
    <para>
      Here is how you call the constructor for a &sl; widget.
      <programlisting
><replaceable >s</replaceable > = &sl; ( <replaceable
                >master</replaceable
                >, width=<replaceable >W</replaceable
                >, height=<replaceable >H</replaceable
                >, vscroll=<replaceable >VS</replaceable
                >,
                   hscroll=<replaceable >HS</replaceable
                >, callback=<replaceable >c</replaceable > )
</programlisting>
      where:
    </para>
    <variablelist>
      <varlistentry>
        <term>
          <code ><replaceable >s</replaceable ></code >
        </term>
        <listitem>
          <para>
            The constructor returns a new &sl; widget that
            has <emphasis >not</emphasis > been registered.  Be
            sure to register it with the <code >.grid()</code >
            method or it will not appear in your application.
          </para>
        </listitem>
      </varlistentry>
      <varlistentry>
        <term>
          <code >master</code >
        </term>
        <listitem>
          <para>
            The parent <code >Frame</code > widget in which the
            new &sl; widget is to be mastered.
          </para>
        </listitem>
      </varlistentry>
      <varlistentry>
        <term>
          <code >width=<replaceable >W</replaceable ></code >
        </term>
        <listitem>
          <para>
            The width of the <code >Listbox</code > in
            characters.  The default value is &DEFAULT-WIDTH;.
          </para>
        </listitem>
      </varlistentry>
      <varlistentry>
        <term>
          <code >height=<replaceable >H</replaceable ></code >
        </term>
        <listitem>
          <para>
            The height of the <code >Listbox</code > in lines.
            The default value is &DEFAULT-HEIGHT;.
          </para>
        </listitem>
      </varlistentry>
      <varlistentry>
        <term>
          <code >vscroll=<replaceable >VS</replaceable ></code >
        </term>
        <listitem>
          <para>
            By default, you will get a vertical scrollbar.  If
            you don't want one, use &#x201c;<code
            >vscroll=0</code >&#x201d;.
          </para>
        </listitem>
      </varlistentry>
      <varlistentry>
        <term>
          <code >hscroll=<replaceable >HS</replaceable ></code >
        </term>
        <listitem>
          <para>
            By default, you will <emphasis >not</emphasis > get a
            horizontal scrollbar.  If you <emphasis >do</emphasis
            > want one, use &#x201c;<code >hscroll=1</code
            >&#x201d;.
          </para>
        </listitem>
      </varlistentry>
      <varlistentry>
        <term>
          <code >callback=<replaceable >c</replaceable ></code >
        </term>
        <listitem>
          <para>
            If you want your application to react whenever a user
            clicks on a line in the <code >Listbox</code >,
            use this keyword argument to supply a function <code
            >c</code >, and that function will be called whenever
            the user clicks on a line.  You should define your
            function like this:
            <programlisting
>def <replaceable >c</replaceable > ( lineNo ):
    ...
</programlisting>
            where the <code >lineNo</code > argument will be the
            index (starting at 0) of the line on which the user
            clicked.  For example, if the user clicks on the
            third line, the &sl; widget will call your procedure
            with an argument 2.
          </para>
        </listitem>
      </varlistentry>
    </variablelist>
    <section id='attributes'>
      <title>Public attributes of a &sl;</title>
      <para>
        These attributes of a &sl; widget are visible.
      </para>
      <variablelist>
        <varlistentry>
          <term>
            <code >.width</code >
          </term>
          <listitem>
            <para>
              The width of the listbox in characters when the
              widget was constructed.
            </para>
          </listitem>
        </varlistentry>
        <varlistentry>
          <term>
            <code >.height</code >
          </term>
          <listitem>
            <para>
              The height of the listbox in lines when the
              widget was constructed.
            </para>
          </listitem>
        </varlistentry>
        <varlistentry>
          <term>
            <code >.listbox</code >
          </term>
          <listitem>
            <para>
              The Tkinter <code >Listbox</code > widget inside
              the &sl; widget.  If you want to change the
              appearance of the listbox, use this attribute's
              <code >.configure()</code > method.
            </para>
            <para>
              For example, if you have a font object (<code
              >tkFont.Font</code >) named <code >ttFont</code >,
              and you would like to apply it to a &sl; widget
              named <code >optionBox</code >, this would do it:
              <programlisting
>    optionBox.listbox.configure(font=ttFont)
</programlisting>
            </para>
          </listitem>
        </varlistentry>
      </variablelist>
    </section> <!--attributes-->
    <section id='methods'>
      <title>Methods on a &sl;</title>
      <para>
        These methods are defined on a &sl; widget.
      </para>
      <variablelist>
        <varlistentry>
          <term>
            <code >.count()</code >
          </term>
          <listitem>
            <para>
              Returns the number of lines currently contained in
              the listbox.
            </para>
          </listitem>
        </varlistentry>
        <varlistentry>
          <term>
            <code >.__getitem__(self, i)</code >
          </term>
          <listitem>
            <para>
              To retrieve the text of the <code
              >i<superscript>th</superscript></code > line in the
              listbox, you can use the regular Python index
              operator.  For example, if you have a &sl; object
              named <code >optionBox</code >, the expression
              &#x201c;<code >optionBox[2]</code >&#x201d; would
              return the contents of the third line.
            </para>
          </listitem>
        </varlistentry>
        <varlistentry>
          <term>
            <code >.append(<replaceable >s</replaceable >)</code >
          </term>
          <listitem>
            <para>
              Adds a string <code ><replaceable >s</replaceable
              ></code > as the next line in the listbox.
            </para>
          </listitem>
        </varlistentry>
        <varlistentry>
          <term>
            <code >.insert(<replaceable >linex</replaceable >,
            <replaceable >s</replaceable >)</code >
          </term>
          <listitem>
            <para>
              Inserts a string <code ><replaceable
              >s</replaceable ></code > as a new line before
              position <code ><replaceable >linex</replaceable
              ></code >.  For example, to add a new first line
              containing &#x201c;<code >Merlin</code >&#x201d; to
              a &sl; widget named <code >s</code >:
              <programlisting > s.insert(0, 'Merlin')
</programlisting>
            </para>
          </listitem>
        </varlistentry>
        <varlistentry>
          <term>
            <code >.delete(<replaceable >linex</replaceable
            >)</code >
          </term>
          <listitem>
            <para>
              Deletes a line from the listbox; <code
              ><replaceable >linex</replaceable ></code > is the
              index of the line, starting from 0.
            </para>
          </listitem>
        </varlistentry>
        <varlistentry>
          <term>
            <code >.clear()</code >
          </term>
          <listitem>
            <para>
              Removes all lines from the listbox.
            </para>
          </listitem>
        </varlistentry>
      </variablelist>
    </section> <!--methods-->
  </section> <!--using-->
  <section id='driver'>
    <title>&slt;: A small test driver</title>
    <para>
      Here is a small, complete Tkinter application that
      exercises the functions of the &sl; widget.  This is
      what it looks like:
    </para>
    <informalfigure>
      <!--Screen shot of scrolledlisttest taken by Gimp: File->Acquire.
       !-->
      <mediaobject>
        <imageobject>
          <imagedata fileref="screenshot.png"/>
        </imageobject>
      </mediaobject>
    </informalfigure>
    <para>
      We start out with the usual prologue: a comment pointing
      back at this documentation.  We import the <code
      >Tkinter</code > module, then we import the module under
      test: &sl;.
    </para>
    <programlisting role='outFile:scrolledlisttest'
>#!/usr/bin/env python
#================================================================
# &slt;: Test driver for &sl;
#
# For documentation, see:
#   &selfURL;
#----------------------------------------------------------------

from Tkinter import *
import scrolledlist
</programlisting>
    <section id='test-Application'>
      <title>The <code >Application</code > class</title>
      <para>
        Next we start declaring the <code >Application</code >
        class, which embodies the entire graphical user
        interface.
      </para>
      <programlisting role='outFile:scrolledlisttest'>
class Application(Frame):
    """GUI for &slt;
    """
</programlisting>
    </section> <!--test-Application-->
    <section id='Application-init'>
      <title><code >Application.__init__()</code >: Constructor</title>
      <para>
        The constructor is quite pro forma: call the parent class
        constructor, register the application with its <code
        >.grid()</code > method, create the widgets, and run some
        initial tests.
      </para>
      <programlisting role='outFile:scrolledlisttest'>
    def __init__ ( self ):
        """Constructor for the Application class.
        """
        Frame.__init__ ( self, None )
        self.grid()
        self.__createWidgets()
        self.__tests()
</programlisting>
    </section> <!--Application-init-->
    <section id='Application-createWidgets'>
      <title><code >Application.__createWidgets()</code >: Widget
      layout</title>
      <para>
        There are only two widgets: the &sl; widget we are
        testing, and a quit button below it.
      </para>
      <programlisting role='outFile:scrolledlisttest'>
    def __createWidgets ( self ):
        """Lay out the widgets.
        """
        self.sbox = scrolledlist.ScrolledList ( self,
            width=20, height=10, hscroll=1,
            callback=self.__pickHandler )
        self.sbox.grid ( row=0, column=0 )

        self.quitButton  =  Button ( self, text="Quit",
            command=self.quit )
        self.quitButton.grid ( row=1, column=0, columnspan=99,
            sticky=E+W, ipadx=5, ipady=5 )
</programlisting>
    </section> <!--Application-createWidgets-->
    <section id='Application-pickHandler'>
      <title><code >Application.__pickHandler()</code >: Handler
      for clicking on the listbox</title>
      <para>
        This method is the callback function that gets called
        whenever the user clicks on a line in the listbox.  It
        displays the line number and the text on that line.
      </para>
      <programlisting role='outFile:scrolledlisttest'>
    def __pickHandler ( self, linex ):
        """Handler for user clicks on lines in the listbox.
        """
        print "Click on line %d, '%s'" % (linex, self.sbox[linex])
</programlisting>
    </section> <!--Application-pickHandler-->
    <section id='Application-tests'>
      <title><code >Application.__tests__()</code >: Run initial
      tests</title>
      <para>
        This method adds some lines to the listbox using both the
        <code >.append()</code > and <code >.insert()</code >
        methods, exercises the <code >.delete()</code > method,
        and then tests the <code >.count()</code > method.
      </para>
      <programlisting role='outFile:scrolledlisttest'>
    def __tests ( self ):
        """Initial testing of the &sl; widget.
        """
</programlisting>
      <para>
        First we insure that the initial list length is zero.
        Then we add three lines and again check the length.
      </para>
      <programlisting role='outFile:scrolledlisttest'
>        print "Initial size is", self.sbox.count()
        print "Add alpaca, buffalo, eagle:"
        self.sbox.append ( "alpaca" )
        self.sbox.append ( "buffalo" )
        self.sbox.append ( "eagle" )
        print "Size is now", self.sbox.count()
</programlisting>
      <para>
        Next we test the <code >.clear()</code > method.
        The size after clearing should be zero.
      </para>
      <programlisting role='outFile:scrolledlisttest'
>        print "Clear listbox:"
        self.sbox.clear()
        print "Size is now", self.sbox.count()
</programlisting>
      <para>
        We add the same three lines back in, then test insertion.
      </para>
      <programlisting role='outFile:scrolledlisttest'
>        print "Add alpaca, buffalo, eagle:"
        self.sbox.append ( "alpaca" )
        self.sbox.append ( "buffalo" )
        self.sbox.append ( "eagle" )
        print "Insert cachalot"
        self.sbox.insert ( 2, "cachalot" )
        print "Size is now", self.sbox.count()
</programlisting>
      <para>
        Next we test deletion.
      </para>
      <programlisting role='outFile:scrolledlisttest'
>        print "Delete buffalo:"
        self.sbox.delete ( 1 )
        print "Size is now", self.sbox.count()
</programlisting>
      <para>
        Finally, we insert enough lines, and one long line, to
        make it possible to test the scrollbars.
      </para>
      <programlisting role='outFile:scrolledlisttest'
>        print "Insert bunches o stuff"
        self.sbox.append ( "finch" )
        self.sbox.append ( "goshawk" )
        self.sbox.append ( "harrier" )
        self.sbox.append ( "indigobird" )
        self.sbox.append ( "jabiru" )
        self.sbox.append ( "kingfisher" )
        self.sbox.append ( "Middendorff's grasshopper-warbler" )
        self.sbox.append ( "merlin" )
        self.sbox.append ( "northern flicker" )
        self.sbox.append ( "ovenbird" )
        self.sbox.append ( "parula" )
        print "Size is now", self.sbox.count()
</programlisting>
    </section> <!--Application-tests-->
    <section id='driver-main'>
      <title>Main program</title>
      <para>
        This is the usual main program for a Tkinter application:
        instantiate an <code >Application</code > object, set up
        the application's title in the window frame, and wait
        for events.
      </para>
      <programlisting role='outFile:scrolledlisttest'
>#================================================================
# Main program
#----------------------------------------------------------------

app  =  Application()
app.master.title ( "&slt;" )
app.mainloop()
</programlisting>
    </section> <!--driver-main-->
  </section> <!--driver-->
  <section id='module-scrolledlist'>
    <title>The &sl; module</title>
    <para>
      Here is the &slpy; module that defines the &sl; widget.
    </para>
    <section id='sl-prologue'>
      <title>Module prologue</title>
      <para>
        The module starts with a comment pointing back to this
        documentation.
      </para>
      <programlisting role='outFile:scrolledlist.py'
>"""&slpy;: A Tkinter widget combining a Listbox with Scrollbar(s).

  For details, see:
    &selfURL;
"""
</programlisting>
      <para>
        First we import the Tkinter module into our namespace.
      </para>
      <programlisting role='outFile:scrolledlist.py'>
#================================================================
# Imports
#----------------------------------------------------------------

from Tkinter import *
</programlisting>
      <para>
        Next, we define two constants for the default <code
        >height</code > and <code >width</code > arguments to the
        widget's constructor.
      </para>
      <programlisting role='outFile:scrolledlist.py'>
#================================================================
# Manifest constants
#----------------------------------------------------------------

DEFAULT_WIDTH   =  "&DEFAULT-WIDTH;"
DEFAULT_HEIGHT  =  "&DEFAULT-HEIGHT;"
</programlisting>
    </section> <!--sl-prologue-->
    <section id='class-ScrolledList'>
      <title><code >class ScrolledList</code ></title>
      <para>
        Here we start the actual class declaration for &sl;.
      </para>
      <programlisting role='outFile:scrolledlist.py'>
class ScrolledList(Frame):
    """A compound widget containing a listbox and up to two scrollbars.
</programlisting>
      <para>
        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.
      </para>
      <programlisting role='outFile: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 ]
    """
</programlisting>
    </section> <!--class-ScrolledList-->
    <section id='ScrolledList-init'>
      <title><code >ScrolledList.__init__()</code >: Constructor</title>
      <para>
        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.
      </para>
      <programlisting role='outFile:scrolledlist.py'>
    def __init__ ( self, master=None, width=DEFAULT_WIDTH,
        height=DEFAULT_HEIGHT, vscroll=1, hscroll=0, callback=None ):
        """Constructor for &sl;.
        """
</programlisting>
      <para>
        The constructor's first job is to call the constructor
        for its parent class, <code >Frame</code >.
      </para>
      <programlisting role='outFile:scrolledlist.py'
>        #-- 1 --
        # [ self  :=  a new Frame widget child of master ]
        Frame.__init__ ( self, master )
</programlisting>
      <para>
        Next, we store the various constructor arguments inside
        the instance.
      </para>
      <programlisting role='outFile:scrolledlist.py'
>        #-- 2 --
        self.width     =  width
        self.height    =  height
        self.vscroll   =  vscroll
        self.hscroll   =  hscroll
        self.callback  =  callback
</programlisting>
      <para>
        Finally, we lay out the internal widgets.
      </para>
      <programlisting role='outFile:scrolledlist.py'
>        #-- 3 --
        # [ self  :=  self with all widgets created and registered ]
        self.__createWidgets()
</programlisting>
    </section> <!--ScrolledList-init-->
    <section id='ScrolledList-createWidgets'>
      <title><code >ScrolledList.__createWidgets()</code >: Lay
      out internal widgets</title>
      <para>
        This method creates and grids all our internal widgets.
      </para>
      <programlisting role='outFile:scrolledlist.py'>
    def __createWidgets ( self ):
        """Lay out internal widgets.
        """
</programlisting>
      <para>
        Here is the grid plan for our internal widgets:
      </para>
      <informaltable pgwide="0">
        <tgroup cols="3">
          <colspec colwidth="1em" align="left"/>
          <colspec colwidth="8em" align="left"/>
          <colspec colwidth="8em" align="left"/>
          <tbody>
            <row>
              <entry></entry>
              <entry>0</entry>
              <entry>1</entry>
            </row>
            <row>
              <entry>0</entry>
              <entry><code >.listbox</code ></entry>
              <entry><code >.vScrollbar</code ></entry>
            </row>
            <row>
              <entry>1</entry>
              <entry><code>.hScrollbar</code></entry>
              <entry></entry>
            </row>
          </tbody>
        </tgroup>
      </informaltable>
      <para>
        First, we create the vertical scrollbar, if there is one.
        The <code >sticky=N+S</code > attribute makes the scrollbar
        stretch to the full height of grid row 0.
      </para>
      <programlisting role='outFile: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 )
</programlisting>
      <para>
        Next, we create the horizontal scrollbar, if there is
        one.  The <code >sticky=E+W</code > attribute makes it
        stretch to the width of grid column 0.
      </para>
      <programlisting role='outFile: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 )
</programlisting>
      <para>
        Now we create the <code >Listbox</code > widget.  The
        <code >relief=SUNKEN</code > attribute makes the
        listbox's contents look like they are recessed into the
        window.  The <code >borderwidth=2</code > attribute puts
        a 2-pixel border around the listbox.
      </para>
      <programlisting role='outFile: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 )
</programlisting>
      <para>
        The next step is to create the linkages between the
        scrollbars and the <code >Listbox</code >.  The <code
        >command</code > attribute of a vertical <code
        >Scrollbar</code > widget is a method that is called
        whenever the scrollbar is scrolled by the user; the <code
        >yview</code > method of a <code >Listbox</code > widget
        causes its contents to be repositioned.  This linkage
        allows the scrollbar to move the listbox.
      </para>
      <para>
        However, the linkage is bidirectional.  There are
        operations on the <code >Listbox</code > widget that
        change its contents' position, and when that happens, the
        scrollbar's position should also be adjusted.  This
        linkage sets the <code >yscrollcommand</code > attribute
        of the <code >Listbox</code > to the <code >.set()</code
        > method of the scrollbar.
      </para>
      <para>
        The linkages are similar for a horizontal scrollbar.
      </para>
      <programlisting role='outFile: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
</programlisting>
      <para>
        So that our widget will respond to the user clicking on a
        line in the listbox, we use the <code >.bind()</code >
        method to set up an event binding that will call our
        <code >.__clickHandler</code > method when that happens.
      </para>
      <programlisting role='outFile: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 ( "&lt;Button-1&gt;", self.__clickHandler )
</programlisting>
    </section> <!--ScrolledList-createWidgets-->
    <section id='ScrolledList-clickHandler'>
      <title><code >ScrolledList.__clickHandler()</code >: Event
      handler for button 1</title>
      <para>
        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
        <code >callback</code > procedure, we will figure out
        which line of the listbox the user clicked on, and pass
        the number of that line to that callback.
      </para>
      <programlisting role='outFile:scrolledlist.py'>
    def __clickHandler ( self, event ):
        """Called when the user clicks on a line in the listbox.
        """
</programlisting>
      <para>
        If there is no callback, don't do anything.
      </para>
      <programlisting role='outFile:scrolledlist.py'
>        #-- 1 --
        if  not self.callback:
            return
</programlisting>
      <para>
        The <code >event</code > argument holds the screen
        y-coordinate of the mouse click in its <code >.y</code >
        attribute.  The <code >.nearest()</code > method on the
        <code >Listbox</code > widget converts this coordinate
        into a line number.
      </para>
      <programlisting role='outFile: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 )
</programlisting>
      <para>
        So that the listbox will respond to the <keysym
        >PageUp</keysym > and <keysym >PageDown</keysym > keys,
        we move the keyboard focus to the listbox whenever
        a family is selected.
      </para>
      <programlisting role='outFile:scrolledlist.py'
>        #-- 3 --
        self.listbox.focus_set()
</programlisting>
    </section> <!--ScrolledList-clickHandler-->
    <section id='ScrolledList-count'>
      <title><code >ScrolledList.count()</code >: Return the
      line count</title>
      <para>
        This method returns the number of lines currently in use
        inside the listbox.  The <code >.size()</code > method on
        the <code >Listbox</code > widget is exactly what we
        need.
      </para>
      <note>
        <para>
          Originally I wanted to define a <code >.__len__()</code
          > method so that the user could use the Python <code
          >len()</code > function on a &sl; to get the line
          count.  However, this caused some bizarre bugs, so
          it is now a conventional method.
        </para>
      </note>
      <programlisting role='outFile:scrolledlist.py'>
    def count ( self ):
        """Return the number of lines in use in the listbox.
        """
        return self.listbox.size()
</programlisting>
    </section> <!--ScrolledList-count-->
    <section id='ScrolledList-getitem'>
      <title><code >ScrolledList.__getitem__()</code >: Implement
      the Python index operator</title>
      <para>
        This method is called when the user indexes a &sl; widget
        to get the text from a specific line of the listbox.  The
        <code >.get()</code > method on the <code >Listbox</code
        > widget does just what we need.  We raise an <code
        >IndexError</code > exception if the index is out of
        range.
      </para>
      <programlisting role='outFile:scrolledlist.py'>
    def __getitem__ ( self, k ):
        """Get the (k)th line from the listbox.
        """

        #-- 1 --
        if  ( 0 &lt;= k &lt; self.count() ):
            return self.listbox.get ( k )
        else:
            raise IndexError, ( "ScrolledList[%d] out of range." % k )
</programlisting>
    </section> <!--ScrolledList-getitem-->
    <section id='ScrolledList-append'>
      <title><code >ScrolledList.append()</code >: Add a line of
      text</title>
      <para>
        This method appends a new last line to the text in the
        listbox.  The constant <code >END</code > specifies a
        position just after the last existing line.
      </para>
      <programlisting role='outFile:scrolledlist.py'>
    def append ( self, text ):
        """Append a line to the listbox.
        """
        self.listbox.insert ( END, text )
</programlisting>
    </section> <!--ScrolledList-append-->
    <section id='ScrolledList-insert'>
      <title><code >ScrolledList.insert()</code >: Insert a line
      of text</title>
      <para>
        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.
      </para>
      <programlisting role='outFile:scrolledlist.py'>
    def insert ( self, linex, text ):
        """Insert a line between two existing lines.
        """

        #-- 1 --
        if  0 &lt;= linex &lt; self.count():
            where  =  linex
        else:
            where  =  END

        #-- 2 --
        self.listbox.insert ( where, text )
</programlisting>
    </section> <!--ScrolledList-insert-->
    <section id='ScrolledList-delete'>
      <title><code >ScrolledList.delete()</code >: Remove a line
      of text</title>
      <para>
        This method removes a specified line from the listbox.
        If the given position is out of range, we do nothing.
      </para>
      <programlisting role='outFile:scrolledlist.py'>
    def delete ( self, linex ):
        """Delete a line from the listbox.
        """
        if  0 &lt;= linex &lt; self.count():
            self.listbox.delete ( linex )
</programlisting>
    </section> <!--ScrolledList-delete-->
    <section id='ScrolledList-clear'>
      <title><code >ScrolledList.clear()</code >: Empty the
      listbox</title>
      <para>
        Removes all lines from the listbox.  The <code
        >.delete()</code > method on a <code >Listbox</code >
        object takes two arguments, a start position and an end
        position.  A zero value refers to the start of the
        listbox, and the constant <code >END</code > refers to
        the position just after the last line.
      </para>
      <programlisting role='outFile:scrolledlist.py'>
    def clear ( self ):
        """Remove all lines.
        """
        self.listbox.delete ( 0, END )
</programlisting>
    </section> <!--ScrolledList-clear-->
  </section> <!--module-scrolledlist-->
</article>
