10.3. Listboxes and Scrollbars
Listbox widgets allow you to display a list of items for selection, and Scrollbars are designed for navigating through the contents of other widgets. Because it is common to use these widgets together, let's study them both at once. Example 10-9 builds both a Listbox and a Scrollbar, as a packaged set.
Example 10-9. PP3E\Gui\Tour\scrolledlist.py
This module can be run standalone to experiment with these widgets, but is also designed to be useful as a library object. By passing in different selection lists to the options argument and redefining the runCommand method in a subclass, the ScrolledList component class defined here can be reused anytime you need to display a scrollable list. With just a little forethought, it's easy to extend the Tkinter library with Python classes.
When run standalone, this script generates the window shown in Figure 10-12. It's a Frame, with a Listbox on its left containing 20 generated entries (the 5th has been clicked), along with an associated Scrollbar on its right for moving through the list. If you move the scroll, the list moves, and vice versa.
Figure 10-12. scrolledlist at the top
10.3.1. Programming Listboxes
Listboxes are straightforward to use, but they are populated and processed in somewhat unique ways compared to the widgets we've seen so far. Many listbox calls accept a passed-in index to refer to an entry in the list. Indexes start at integer 0 and grow higher, but Tkinter also accepts special name strings in place of integer offsets: end to refer to the end of the list, active to denote the line selected, and more. This generally yields more than one way to code listbox calls.
For instance, this script adds items to the listbox in this window by calling its insert method, with successive offsets (starting at zero):
list.insert(pos, label) pos += 1
But you can also fill a list by simply adding items at the end without keeping a position counter at all, with either of these statements:
list.insert('end', label) # add at end: no need to count positions list.insert(END, label) # END is preset to 'end' inside Tkinter
The listbox widget doesn't have anything like the command option we use to register callback handlers for button presses, so you either need to fetch listbox selections while processing other widgets' events (e.g., a button press elsewhere in the GUI) or tap into other event protocols to process user selections. To fetch a selected value, this script binds the <Double-1> left mouse button double-click event to a callback handler method with bind (seen earlier on this tour).
In the double-click handler, this script grabs the selected item out of the listbox with this pair of listbox method calls:
index = self.listbox.curselection( ) # get selection index label = self.listbox.get(index) # fetch text by its index
Here, too, you can code this differently. Either of the following lines has the same effect; they get the contents of the line at index 'active'the one selected:
label = self.listbox.get('active') # fetch from active index label = self.listbox.get(ACTIVE) # ACTIVE='active' in Tkinter
For illustration purposes, the class's default runCommand method prints the value selected each time you double-click an entry in the listas fetched by this script, it comes back as a string reflecting the text in the selected entry:
C:\...\PP3E\Gui\Tour>python scrolledlist.py You selected: Lumberjack-2 You selected: Lumberjack-19 You selected: Lumberjack-4 You selected: Lumberjack-12
Listboxes can also be useful input devices even without attached scroll bars; they accept color, font, and relief configuration options. They also support both single and multiple selection modes. The default mode allows only a single item to be selected, but the selectmode argument supports four settings: SINGLE, BROWSE, MULTIPLE, and EXTENDED (the default is BROWSE). Of these, the first two are single selection modes, and the last two allow multiple items to be selected.
These modes vary is subtle ways. For instance, BROWSE is like SINGLE, but it also allows the selection to be dragged. Clicking an item in MULTIPLE mode toggles its state without affecting other selected items. And the EXTENDED mode allows for multiple selections and works like the Windows file explorer GUIyou select one item with a simple click, multiple items with a Ctrl-click combination, and ranges of items with Shift-clicks. Multiple selections can be programmed with code of this sort:
listbox = Listbox(window, bg='white', font=('courier', fontsz)) listbox.config(selectmode=EXTENDED) listbox.bind('<Double-1>', (lambda event: onDoubleClick( ))) # get messages selected in listbox selections = listbox.curselection( ) # tuple of digit strs, 0..N-1 selections = [int(x)+1 for x in selections] # convert to ints, make 1..N
When multiple selections are enabled, the curselection method returns a list of digit strings giving the relative numbers of the items selected, or it returns an empty tuple if none is selected. Really, this method always returns a tuple of digit strings, even in single selection mode (we don't care in Example 10-9, because the get method does the right thing for a one-item tuple, when fetching a value out of the listbox).
You can experiment with the selection alternatives on your own by uncommenting the selectmode setting in Example 10-9 and changing its value. You may get an error on double-clicks in multiple selection modes, though, because the get method will be passed a tuple of more than one selection index (print it out to see for yourself). We'll see multiple selections in action in the PyMailGUI example later in this book.
10.3.2. Programming Scroll Bars
The deepest magic in the Example 10-9 script, though, boils down to two lines of code:
sbar.config(command=list.yview) # call list.yview when I move list.config(yscrollcommand=sbar.set) # call sbar.set when I move
The scroll bar and listbox are effectively cross-linked to each other through these configuration options; their values simply refer to bound widget methods of the other. By linking like this, Tkinter automatically keeps the two widgets in sync with each other as they move. Here's how this works:
In other words, moving one automatically moves the other. It turns out that every scrollable object in TkinterListbox, Entry, Text, and Canvashas built-in yview and xview methods to process incoming vertical and horizontal scroll callbacks, as well as yscrollcommand and xscrollcommand options for specifying an associated scroll bar's callback handler to invoke. All scroll bars have a command option, to name an associated widget's handler to be called on moves. Internally, Tkinter passes information to all of these methods, and that information specifies their new position (e.g., "go 10 percent down from the top"), but your scripts need never deal with that level of detail.
Because the scroll bar and listbox have been cross-linked in their option settings, moving the scroll bar automatically moves the list, and moving the list automatically moves the scroll bar. To move the scroll bar, either drag the solid part or click on its arrows or empty areas. To move the list, click on the list and move the mouse pointer above or below the listbox without releasing the mouse button. In both cases, the list and scroll bar move in unison. Figure 10-13 shows the scene after moving down a few entries in the list, one way or another.
Figure 10-13. scrolledlist in the middle
10.3.3. Packing Scroll Bars
Finally, remember that widgets packed last are always clipped first when a window is shrunk. Because of that, it's important to pack scroll bars in a display as soon as possible so that they are the last to go when the window becomes too small for everything. You can generally make due with less than complete listbox text, but the scroll bar is crucial for navigating through the list. As Figure 10-14 shows, shrinking this script's window cuts out part of the list, but retains the scroll bar.
Figure 10-14. scrolledlist gets small
At the same time, you don't generally want a scroll bar to expand with a window, so be sure to pack it with just a fill=Y (or fill=X for a horizontal scroll) and not an expand=YES. Expanding this example's window, for instance, makes the listbox grow along with the window, but it keeps the scroll bar attached to the right and it keeps it the same size.
We'll see both scroll bars and listboxes repeatedly in later examples in this and later chapters (flip ahead to examples for PyEdit, PyForm, PyTree, and ShellGui, later in this chapter, for more). And although the example script in this section captures the fundamentals, I should point out that there is more to both scroll bars and listboxes than meets the eye here.
For example, it's just as easy to add horizontal scroll bars to scrollable widgets. They are programmed almost exactly like the vertical one implemented here, but callback handler names start with "x," not "y" (e.g., xscrollcommand), and an orient='horizontal' configuration option is set for the scroll bar object. For example, to add both vertical and horizontal scrolls and to crosslink their motions, you would use the following sort of code:
window = Frame(self) vscroll = Scrollbar(window) hscroll = Scrollbar(window, orient='horizontal') listbox = Listbox(window) # move listbox when scroll moved vscroll.config(command=listbox.yview, relief=SUNKEN) hscroll.config(command=listbox.xview, relief=SUNKEN) # move scroll when listbox moved listbox.config(yscrollcommand=vscroll.set, relief=SUNKEN) listbox.config(xscrollcommand=hscroll.set)
See the PyEdit, PyTree, and PyMailGUI programs later in this book for examples of horizontal scroll bars at work. Scroll bars see more kinds of GUI action toothey can be associated with other kinds of widgets in the Tkinter library. For instance, it is common to attach one to the Text widget. This brings us to the next point of interest on this tour.