Version 20 of scrollbar

Updated 2006-01-08 13:55:49

Martin Lemburg - 02.09.2002: one question - what can I do to limit a vertical scrollbar to the height of a listbox with the height 1 or 2? The scrollbar is always 3 listbox entries high!

"you want to pack your vertical Scrollbars -fill y and your horizontal Scrollbars -fill x ..."

See also Scroll bars that appear only when needed

cscrollbar is a Tk scrollbar replacement that is compatible with the standard Tk scrollbar protocol.

On news:comp.lang.tcl , Ken Jones writes:

In your example, you created the Text and Scrollbar widgets, but you didn't wire them up to each other. Scrollbars have a -command which they call whenever the user interacts with them. Scrollable widgets also have -xscrollcommand and -yscrollcommand options so that they can communicate with their attached Scrollbars (for example, so the Scrollbar knows how large to make the "elevator" and where to place it in the trough).

An example of hooking up a vertical scrollbar:

 text .t -yscrollcommand {.sbar set}
 scrollbar .sbar -orient vertical -command {.t yview}

... FW: It does, however, warrant noting that -orient vertical is the default and so may be excluded...

An example of hooking up a horizontal scrollbar:

 text .t -xscrollcommand {.sbar set}
 scrollbar .sbar -orient horizontal -command {.t xview}

All scrollable widgets (Canvas, Entry, Listbox, Text) in Tk use the same mechanism. (Although scrolling Canvas widgets is trickier. If you need to scroll a Canvas, check out the Canvas's -scrollregion option as well.)

Also, it looks as though you need to study the pack command more. It's a bit difficult to describe concisely how it works. Here's my best shot at the fundamentals of packing:

  • Tcl lays out widgets in the order you pack them.
  • Each widget is allocated "packing space" (sometimes called a "parcel") in the space that's left over from packing previous widgets, which is called the "cavity."
  • A widget is allocated the entire side of the cavity, as specified by its pack -side option (default is top).
  • The parcel is just wide enough to accommodate the widget when

packing -side left or -side right, or just tall enough to accommodate it when packing -side top or -side bottom.

  • If a widget is smaller than its allocated packing space, pack centers the widget inside of its packing space by default.
  • After packing all the widgets, Tcl "shrink-wraps" the top-level window as tightly as it can around the widgets, squeezing out all the excess space it can.

You can then modify the behavior of the packer through various options like -fill and -anchor, but I'll let you investigate those on your own. But when it comes to Scrollbars, you want to pack your vertical Scrollbars -fill y and your horizontal Scrollbars -fill x, otherwise they'll appear in their default size, which is almost never the behavior you want.

So, putting it all together in your example (vertical scrolling):

 text .xstdin -yscrollcommand {.s set}

 # I'm not sure what you meant to do with this next line.
 # For the sake of illustration, I'm assuming that you
 # just wanted to display the output of some cat
 # command in your Text widget. There are better
 # ways of doing this, but that's another topic.

 .xstdin insert 1.0 [exec cat somefile]
 # or insert just some numbered lines:
 # for { set i 1 } { $i <= 50 } { incr i 1 } {
 #   .xstdin insert end "Line $i\n"
 # }

 scrollbar .s -orient vertical -command {.xstdin yview}
 button .b1 -text Exit -command exit

 # .b1 gets the entire bottom of the window.
 pack .b1 -side bottom

 # Pack .s next, so that if the user decreases the size
 # of the window, the Text widget shrinks instead of
 # the Scrollbar. .s gets the right-hand side of the space
 # left over. It fills it vertically.
 pack .s -side right -fill y

 # Pack .xstdin last. Expand its packing space into
 # any additional space that becomes available if the
 # user resizes the window. Have the Text widget
 # completely fill its packing space.
 pack .xstdin -expand yes -fill both

 # Sleep 5 minutes (300,000 milliseconds) then close window
 after 900000 exit

 # No need to use tkwait to explicitly enter the event loop.
 # wish enters the event loop automatically
 # at the bottom of the script.

escargo 22 Nov 2002 - As near as I can tell, there is no way to control the size of the elevator for the scrollbar. If I want to use a scrollbar to control a widget that tries to minimize the number items that might appear in the graphical part (keeping the rest in a nongraphical structure), it would be desirable to control the elevator size to reflect the total amount of data accessible and not just the portion residing in the graphical part currently. (See Mass-widget.)

I am going to eventually need a tree control that may need to show thousands of entries. Rather than try to show all of them in the tree control, I might want to keep only a couple of pages of them and build the others when needed. I would want the elevator to reflect the virtual size of the tree and not the current size.

Is there any way to do that?

I can see wanting to configure mapping functions and callbacks to the scrollbar so that it could manage a segmented data set, but the hooks do not appear to be there right now. It slickly handles the case where everything is in the scrollable object, but no support for virtualization.

Ken Jones: Actually, you can control the size of the scrollbar "elevator" with the scrollbar set operation. Most of the time, you just hook up the widgets as shown above, and let the "native" scrollbar protocol work its magic. But you're free to call your own procedures instead. In broad strokes, you'd need to do the following:

  • For the scrollbar -command option, call a procedure that would extract a suitable dataset and display it in the listbox/canvas/text widget/whatever.
  • Remember to call the scrollbar's set operation to update the size and location of the elevator.
  • Do some mangling of the scrolled widget's bindings, and/or create some procedure to register for its -xscrollcommand/-yscrollcommand to keep the scrollbar in sync if the user can use arrow keys, etc. to browse through the dataset.

This is basically what's going on in the Mass-widget example mentioned above. But it can be a bit difficult to zero in on that given all the other of the code in that example. So, I hacked together a stripped down example to demonstrate the basic principle. Note that in this example, I have a 1000-element dataset (stored as a list), but display only 10 of these at a time in my listbox.

 # Create a 1000-element dataset, stored in "values"

 for {set i 1} {$i <= 1000} {incr i} {
   lappend values $i

 # scroll(range) will contain only the currently displayed
 # subset of data, 10 items in this example.

 listbox .l -height 10 -listvariable scroll(range)

 # Instead of -command {.l yview}, we're instead calling our
 # updateView routine, to extract an appropriate subset of data
 # to display.

 scrollbar .sb -orient vertical -command updateView

 pack .sb -side right -fill y
 pack .l -side left

 # Initialize our first data subset

 set scroll(start) 0
 set scroll(end) 9
 set scroll(range) [lrange $values $scroll(start) $scroll(end)]

 # updateView --
 # In response to a scrollbar movement, extract an appropriate
 # subset of data, update the listbox to display the data, and
 # update the position of the scrollbar.

 proc updateView {args} {
   global values scroll

   # Parse the scroll protocol

   switch -- [lindex $args 0] {
     moveto {
              # Move to an relative position in the dataset.
              # The next argument is a fraction from 0 to 1
              # representing the beginning of the range to display.

              set scroll(start) [expr {round([llength $values] * [lindex $args 1])}]
     scroll {
              # Jump up or down one or more "units" or "pages."
              # We're free to interpret these in any way we want.
              # I chose a "unit" as a single datum, and "page" as
              # a set of 10 data.

              if {[lindex $args 2] == "units"} {
                set step 1
              } else {
                set step 10
              set step [expr {$step * [lindex $args 1]}]
              incr scroll(start) $step

   # I really should do boundary checking, to make sure that we
   # don't scroll off either end, but I was lazy...

   set scroll(end) [expr {$scroll(start) + 9}]
   set scroll(range) [lrange $values $scroll(start) $scroll(end)]

   # Calculate the new relative position and size for the scrollbar,
   # and set it.

   set start [expr {double($scroll(start))/[llength $values]}]
   set end [expr {double($scroll(end))/[llength $values]}]

   .sb set $start $end

WJP I not infrequently find that I can't easily scroll as quickly as I would like to in windows with a lot of content. Clicking in the trough scrolls only by a small increment, and because there is so much in the window, the scrollbar handle is very small and hard to grasp. I have therefore been adding a binding on ButtonPress-3, which is unused by default, for the following procedure:

 proc ScrollbarMoveBigIncrement {w f x y} {
    set part [$w identify $x $y]
    switch -exact -- $part {
        trough1 {
            set dir -1;
        arrow1 {
            set dir -1;
        trough2 {
            set dir  1;
        arrow2 {
            set dir  1;
        default {
            return ;
    set CurrentFraction [lindex [$w get] 0]
    set NewFraction [expr $CurrentFraction + ($dir * $f)]
    eval [concat [$w cget -command] moveto $NewFraction]

A typical binding looks like this:

 bind $w.sbar <<B3>> "ScrollbarMoveBigIncrement $w.sbar 0.10 %x %y"

Category Widget - Category Command - Tk syntax help - Arts and Crafts of Tcl-Tk Programming