We're back after a server migration that caused effbot.org to fall over a bit harder than expected. Expect some glitches.

Tkinter Scrollbar Patterns

September 20, 2003 | Fredrik Lundh

Tkinter provides a number of scrollable widgets, but unlike many other toolkits, the widgets do not maintain their own scrollbars. To add a scrollbar to a scrollable widget, you have to create separate Scrollbar widget instances, and attach them to the widget. This article explains how the scrollbar interface works, and shows you how to add scrollbars to standard widgets.

The Scrollbar Interface

In Tkinter, the scrollbar is a separate widget that can be attached to any widget that support the standard scrollbar interface. Such widgets include:

To attach a scrollbar to a scrolled widget, set the scrollbar’s command option to point to a method that will be called when the scrollbar is changed, and set the widget’s scroll command option to the scrollbar’s set method. This method is called by the widget when when the view is changed (for example, when new items are added, or the widget is resized).

In standard Tkinter, these methods and options have standard names; the scrollbar should call the widget’s xview or yview method when the scrollbar is changed, and the widgets provide xscrollcommand and yscrollcommand command options that are called when the scrollbar needs to be updated.

In the following example, a listbox containing 100 integers is equipped with a scrollbar:

Example: A listbox with a scrollbar
from Tkinter import *

root = Tk()

scrollbar = Scrollbar(root)
scrollbar.pack(side=RIGHT, fill=Y)

listbox = Listbox(root)
listbox.pack()

for i in range(100):
    listbox.insert(END, i)

# attach listbox to scrollbar
listbox.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=listbox.yview)

mainloop()

To watch the traffic between the listbox and the scrollbar, you can replace the Scrollbar and Listbox classes with versions that log the relevant method calls:

class DebugScrollbar(Scrollbar):
    def set(self, *args):
        print "SCROLLBAR SET", args
        Scrollbar.set(self, *args)

class DebugListbox(Listbox):
    def yview(self, *args):
        print "LISTBOX YVIEW", args
        Listbox.yview(self, *args)

scrollbar = DebugScrollbar()
scrollbar.pack(side=RIGHT, fill=Y)

listbox = DebugListbox(yscrollcommand=scrollbar.set)
listbox.pack()

scrollbar.config(command=listbox.yview)

When you run the example using these widgets, you’ll get a stream of SCROLLBAR and LISTBOX messages in the console window.

When the listbox is first displayed, the listbox calls the scrollbar to inform it about the current view (in this example, 10 out of 100 lines are displayed). The scrollbar calls back, telling the listbox that the scrollbar is in its topmost position:

SCROLLBAR SET ('0', '0.1')
LISTBOX YVIEW ('moveto', '0')

Note that all arguments are strings, and that the values are normalized to fit in the 0.0 to 1.0 range.

When you move the scrollbar thumb, the scrollbar sends moveto messages to the listbox. The listbox updates the view, and calls the scrollbar’s set method with the resulting values:

LISTBOX YVIEW ('moveto', '0.1041')
SCROLLBAR SET ('0.1', '0.2')
LISTBOX YVIEW ('moveto', '0.186')
SCROLLBAR SET ('0.19', '0.29')
LISTBOX YVIEW ('moveto', '0.3124')
SCROLLBAR SET ('0.31', '0.41')
LISTBOX YVIEW ('moveto', '0.4166')
SCROLLBAR SET ('0.42', '0.52')

Note that the listbox rounds the scrollbar value to the nearest full line.

If you click outside the scrollbar thumb, the scrollbar generates scroll events.

LISTBOX YVIEW ('scroll', '1', 'pages')
SCROLLBAR SET ('0.5', '0.6')
LISTBOX YVIEW ('scroll', '1', 'pages')
SCROLLBAR SET ('0.58', '0.68')
LISTBOX YVIEW ('scroll', '1', 'units')
SCROLLBAR SET ('0.59', '0.69')
LISTBOX YVIEW ('scroll', '1', 'units')
SCROLLBAR SET ('0.6', '0.7')

For scroll events, the scrollbar provides both a value and a unit, and it’s up to the listbox to interpret the units in a way that makes sense to the user. The value is usually -1 (scroll up/left) or 1 (scroll down/right), and the unit is either pages or units.

In a listbox, the basic unit is usually a single item, and a page is as many items that fit into the widget’s window. Other widgets may use different definitions.

Patterns

Adding Scrollbars to Listbox Widgets

We’ve already seen how to add scrollbars to a listbox widget, but here’s the relevant part again:

scrollbar = Scrollbar(master)
scrollbar.pack(side=RIGHT, fill=Y)

listbox = Listbox(master, yscrollcommand=scrollbar.set)
listbox.pack()

scrollbar.config(command=listbox.yview)

Note that this example uses pack to add the scrollbar and the listbox to the parent widget. This can get extremely tricky if you plan to add more than just a scrolled listbox to the parent; to solve this, you can either use grid to get the individual widgets in their right location, or pack the widgets into an extra frame, and use pack or grid to put the frame where you want it.

Another problem with the original example is that the scrollbar has no border, but the standard listbox is drawn with a “sunken” appearance. At least on Windows, things look a lot better if the listbox contents and the scrollbar are placed at the same level. In the following example, I’ve removed the border from the listbox (by setting the border width to zero), and added a border to the extra frame widget:

frame = Frame(root, bd=2, relief=SUNKEN)

scrollbar = Scrollbar(frame)
scrollbar.pack(side=RIGHT, fill=Y)

listbox = Listbox(frame, bd=0, yscrollcommand=scrollbar.set)
listbox.pack()

scrollbar.config(command=listbox.yview)

frame.pack()

The Listbox widget also supports horizontal scrolling. For examples, see the next section.

Adding Scrollbars to Text Widgets

The Text widget implements the same scrollbar interface as the listbox; create the scrollbar, and attach it to the text widget by setting the appropriate options:

scrollbar = Scrollbar(master)
scrollbar.pack(side=RIGHT, fill=Y)

text = Text(master, wrap=WORD, yscrollcommand=scrollbar.set)
text.pack()

scrollbar.config(command=text.yview)

The wrap option controls how to handle long lines in the text widget. The default value is CHAR, which tells the widget that it’s okay to add line breaks between individual characters, for lines that are longer than the widget is wide. If you’re displaying text in the widget, it’s usually better to set the wrap option to WORD, like in the example above. This tells the widget to avoid breaking lines inside words.

You can also switch off line wrapping, by setting the wrap option to NONE. When you do, lines that are longer than the widget is wide will be truncated. To allow the user to display wide text, you can add a second, horizontal scrollbar:

xscrollbar = Scrollbar(master, orient=HORIZONTAL)
xscrollbar.pack(side=BOTTOM, fill=X)

yscrollbar = Scrollbar(master)
yscrollbar.pack(side=RIGHT, fill=Y)

text = Text(master, wrap=NONE,
            xscrollcommand=xscrollbar.set,
            yscrollcommand=yscrollbar.set)
text.pack()

xscrollbar.config(command=text.xview)
yscrollbar.config(command=text.yview)

Note that by using pack to display the widgets, you’ll end up with either a horizontal scrollbar that’s wider than the text widget, or a vertical scrollbar that’s taller than the text widget. You can probably get around this by packing a small frame in the lower left corner, and use additional frames to get the right grouping, but it’s easier to put everything in a separate frame, and use the grid manager to display the widgets:

frame = Frame(master, bd=2, relief=SUNKEN)

frame.grid_rowconfigure(0, weight=1)
frame.grid_columnconfigure(0, weight=1)

xscrollbar = Scrollbar(frame, orient=HORIZONTAL)
xscrollbar.grid(row=1, column=0, sticky=E+W)

yscrollbar = Scrollbar(frame)
yscrollbar.grid(row=0, column=1, sticky=N+S)

text = Text(frame, wrap=NONE, bd=0,
            xscrollcommand=xscrollbar.set,
            yscrollcommand=yscrollbar.set)

text.grid(row=0, column=0, sticky=N+S+E+W)

xscrollbar.config(command=text.xview)
yscrollbar.config(command=text.yview)

frame.pack()

Note the use of grid_rowconfigure and grid_columnconfigure. If you leave out those calls, the widget won’t behave properly if you pack the frame into an expanding parent; if the parent is made smaller, the scrollbars may disappear out of sight. If the parent is made larger, you’ll end up with lots of padding.

Adding Scrollbars to Canvas Widgets

As expected, the Canvas widget also implements the standard scrollbar interface, but with a twist. Here’s an example:

frame = Frame(root, bd=2, relief=SUNKEN)

frame.grid_rowconfigure(0, weight=1)
frame.grid_columnconfigure(0, weight=1)

xscrollbar = Scrollbar(frame, orient=HORIZONTAL)
xscrollbar.grid(row=1, column=0, sticky=E+W)

yscrollbar = Scrollbar(frame)
yscrollbar.grid(row=0, column=1, sticky=N+S)

canvas = Canvas(frame, bd=0,
                xscrollcommand=xscrollbar.set,
                yscrollcommand=yscrollbar.set)

canvas.grid(row=0, column=0, sticky=N+S+E+W)

xscrollbar.config(command=canvas.xview)
yscrollbar.config(command=canvas.yview)

frame.pack()

If you leave it like this, you get a nice scrolling canvas, but the scrollbars do not behave like expected; the scrollbar thumbs stay small and centered no matter how you scroll around. The reason is that the canvas coordinate space is unbounded; at any time, you’re only seeing a very small fraction of the available coordinate space.

To fix this, you can tell the canvas to limit the scrolling to a given area. To do this, set the scrollregion option to a rectangle (given as a 4-tuple). For example, if you know that you’re going to draw things in a 1000x1000 area, you can set the scroll region when you create the canvas:

canvas = Canvas(frame, bd=0, scrollregion=(0, 0, 1000, 1000),
                xscrollcommand=xscrollbar.set,
                yscrollcommand=yscrollbar.set)

If you don’t know the scroll region until later, use the config method to update the region:

canvas.config(scrollregion=(left, top, right, bottom))

You can use the bbox method to get a bounding box for a given object, or a group of objects; canvas.bbox(ALL) returns the bounding box for all objects on the canvas:

canvas.config(scrollregion=canvas.bbox(ALL))

Patterns: A Scrolled Widget Helper

To be continued.

def Scrolled(_widget, _master, _mode='y', **options):
    frame = Frame(_master, bd=2, relief=SUNKEN)
    xscrollbar = yscrollbar = None
    if 'x' in _mode: xscrollbar = Scrollbar(frame, orient=HORIZONTAL)
    if 'y' in _mode: yscrollbar = Scrollbar(frame)
    if not options.has_key("bd"):
        options["bd"] = 0
    widget = _widget(frame, **options)
    ... grid the scrollbars and the widget ...
    return widget

widget = Scrolled(Listbox, master, ...options...)
widget = Scrolled(Text, master, ...options...)
widget = Scrolled(Canvas, master, 'xy', ...options...)
def ScrolledListbox(master, _mode='y', **options):
    return Scrolled(Listbox, master, _mode, **options)

def ScrolledText(master, _mode='y', **options):
    return Scrolled(Text, master, _mode, **options)

def ScrolledCanvas(master, _mode='xy', **options):
    return Scrolled(Canvas, master, _mode, **options)

widget = ScrolledListbox(master, ...options...)
widget = ScrolledText(master, ...options...)
widget = ScrolledCanvas(master, ...options...)