Dissecting a scrollbar

Duoas There are already various treatments on handling scrollbars both in the Tk documentation and on the Wiki. However, this document aims to get really deep into making your scrollbars behave, such that the new scrollbar-user-wannabe can follow easily. I think this page is entirely warranted due both to my own experiences figuring out the silly thing and for the obvious confusion others express here when first messing with it. I wonder how many give up and conclude that the scrollbar is too mind-boggling a thing to make work the way they want.

If you too are a scrollbar expert, please feel free to add to this page any information you consider relevant. But remember not to jump over young minds.

Contents

  • Basics
  • Really small documents don't need a scrollbar.
  • Really big documents make for really tiny scrollbar handles.
  • Really big documents make things go slow.
  • I'd like to scroll past the end of my document.
  • What's this gap between my document and its scrollbar?
  • Why does my horizontal scrollbar keep changing size?
  • (more?)
  • Comments

Article Format

All the code on this page is intended to be usable in a cut-and-paste format. Hence, cut-and-paste the code out of the Basics section to get started, then cut-and-paste other code sections to play with them. Be aware that all this is example code, so it isn't necessarily exactly what you might want to use. Cut-and-paste it, modify it to your own purposes, and enjoy!


Basics

If you don't already know, a scrollbar is one of the more unwieldly widgets in the GUI world --at least from a programmer's point of view. As a refresher, a scrollbar is composed of multiple components.

http://home.comcast.net/~michaelthomasgreer/scrollbar-fig1.gif

It is entirely possible to have more than one arrow button at each end of the scrollbar, or none, but Tcl currently does not permit you to configure that. (You get what you get from your current widget set.)

The numbers represent the range of the associated document, where 0.0 is the beginning of the document and 1.0 is the end of the document. An associated window displays the document (text, graphics, etc.). Typically the entire document cannot be viewed all at once, so only a portion is visible in the associated window's area. The portion visible is represented by the slider handle. The area used by the slider in the trough represents the visible area of the document in the whole document. This is the genius of the scrollbar widget.

http://home.comcast.net/~michaelthomasgreer/scrollbar-fig5.gif

The slider size and position is controlled using the scrollbar set sub-command [L1 ], which takes two floating-point numbers in the range 0.0 to 1.0, inclusive. The slider (also known as the "handle" or "grip") in the above graphic is positioned at about "0.66 0.77" (That is, first is 0.66 and last is 0.77.) For comparison, the slider in the first image is at "0.07 0.43".

If you set the slider first and last to 0.0 and 1.0, the scrollbar becomes useless. Many scrollbar widgets will disable the arrow buttons and hide the handle to visually indicate that the scrollbar is not usable in any meaningful way. Some don't (particularly older-style scrollbars).

Some more visually pleasing scrollbars will also disable individual arrow buttons if the handle is at either end of the trough area (0.0 or 1.0).


Typically, you link a scrollbar and an associated widget with the usual Tcl commands:

  scrollbar .h -orient horizontal -command {.t xview}
  scrollbar .v -orient vertical   -command {.t yview}
  text      .t -font {courier 12} -width 20 -height 10 -wrap none \
               -yscrollcommand {.v set} -xscrollcommand {.h set}

In this case, we are setting a horizontal as well as a vertical scrollbar on the text widget. This will permit one to have text wider than 20 characters as well as more than 10 lines accessible within the text widget.

If you want to understand better how these commands actually behave, head on over to a Scrollbar tutorial.

For this set of widgets, the grid geometry manager is useful for aligning them in relationship to one another:

  grid .h -row 1 -column 0 -sticky nsew
  grid .v -row 0 -column 1 -sticky nsew
  grid .t -row 0 -column 0 -sticky nsew

  grid columnconfigure . 0 -weight 1
  grid rowconfigure    . 0 -weight 1

So far so good. This stuff you already know.

Now for the messy stuff. The handle is supposed to represent the visible portion of the document, and the entire trough area the whole length of the document. This is the genius of the scrollbar widget. And our bane.

In almost all cases, the issue is the interface between the document and the scrollbar. What we need to do is modify the commands that work between the scrollbar and the associated widget to something a little more intelligent than the simple example given above.


Really small documents don't need a scrollbar

If your scrollbar becomes disabled because the handle range was set to "0.0 1.0", some people consider that to be visual bloat left on the screen (particularly if it doesn't disable but makes the handle fill the whole space <shudder>). "If the scrollbar is unusable," they reason, "then why show it at all?"

See scroll bars that appear only when needed for the full treatment. If you want to skip the gory details and just use something that works, check out tklib's autoscroll package.

The essential idea

The essential idea is to replace the associated widget's -yscrollcommand (or -xscrollcommand, as appropriate) with a new command that does the job of hiding or showing the scrollbar.

  proc sbset {sb first last} {
    if {($first <= 0.0) && ($last >= 1.0)} \
      then {
        # Since I used the grid manager above, I will here too.
        # The grid manager is nice since it remembers geometry when widgets are hidden.
        # Tell the grid manager not to display the scrollbar (for now).
        grid remove $sb
        } \
      else {
        # Restore the scrollbar to its prior status (visible and in the right spot).
        grid $sb
        }
    $sb set $first $last
    }

  .t config -xscrollcommand {sbset .h} -yscrollcommand {sbset .v}

One thing to notice with this arrangement is that the size of the toplevel window changes when a scrollbar appears or disappears. That's because we gridded it that way. There are a number of good ways to handle geometry management, but they are not mentioned here.

However, since it is an effect due to our simple arrangement, let's consider a simple fix:

  grid propagate . off

This tells the grid manager not to try to resize the container window when the grid itself wants to resize. If you do this, there is one other consideration you'll have to handle. In the following picture, I've started wish, cut-and-pasted all the code from the Basic section and this section into the console, and entered a bunch of lines, with one really long one in the middle. Then I arrowed-up until the long line is at the bottom of the text window display.

http://home.comcast.net/~michaelthomasgreer/scrollbar-fig3.gif

If I press the up arrow key once more the long line scrolls down out of view. Since there are no more long lines in view, the horizontal scrollbar disappears. But once it disappears the long line is back in view again! So the horizontal scrollbar reappears. Then disappears. Then reappears. And it cycles. The result is an angry, flashing display.

This is actually a problem that deserves its own section. See Why does my horizontal scrollbar keep changing size?.


Really big documents make for really tiny scrollbar handles

This is the one that bugs me most. If I've got a nice 1000 pixel-high scrollbar, why must I grope for a four pixel-high handle? Granted, some systems allow you to fix the minimum size of the handle. For example, Windows XP scrollbars have this power. Unfortunately, there is no direct way to specify a minimum slider size in Tcl (without writing a platform-specific extension module).

However, you can fix this problem easily enough by once again modifying the commands linking the scrollbar and its associated window.

The essential idea

The essential idea is to replace the associated widget's -yscrollcommand (or -xscrollcommand, as appropriate) with a new command that does the job of keeping the scrollbar handle from shrinking beyond a "specific size".

http://home.comcast.net/~michaelthomasgreer/scrollbar-fig2.gif

The "specific size" of the handle is (last minus first). You can also represent "specific size" in terms of the number of pixels high (or wide) the slider is.

Details

Since percentage is simple we'll start with that.

  proc sbset {sb minsize first last} {
    if {($last -$first) < $minsize} {
      set last [expr {$first +$minsize}]
      if {$last > 1.0} {
        set first [expr {1.0 -$minsize}]
        set last  1.0
        if {$first < 0.0} {set first 0.0}
        }
      }
    $sb set $first $last
    }

If you want to deal with actual pixel sizes, that can be done also. The tricky part is simply that we don't have access to a scrollbar's internals, so we'll have to guess at a couple things. In most cases, errors should be minimal (three or four pixels off).

pseudo-code (real code later):

  • get length of scrollbar widget
  • subtract sum of arrow buttons (usually they are square, so 2*[sb cget -width] is good enough)
  • minsize = minsize-in-pixels / length

There is one other issue this code generates. If you remember, the trough area is portioned linearly from 0.0 to 1.0. If you forbid the slider from shrinking beyond a specific amount, the first location in the slider can never become large enough to scroll to the end of the document. This must also be remedied if you want the people using your program to like you.

http://home.comcast.net/~michaelthomasgreer/scrollbar-fig4.gif

The way to do it is to modify the command that the scrollbar sends to the document to adjust the first location back to an appropriate value. This involves a ratio. Remember, a ratio is just a fraction. To convert 0.42 to 42, multiply by (100 / 1.0). To convert 42 to 0.42, multiply by (1.0 /100). The difference is whether we use the reciprocal (inverting the fraction) or not.

For greater accuracy (every pixel counts here), I've chosen to work with the length of the trough against the length of the invisible portion of the document. (Computers get floating-point arithmetic wrong very easily.)

  (area of document not visible) / (area of trough not occupied by slider)

More specifically, our fraction is:

  (1.0 - (document.last - document.first))  /  (1.0 - (scrollbar.last - scrollbar.first))

This gives me a number greater than or equal to 1.0 that I can multiply with scrollbar.first to get document.first. If the scrollbar is not modified, this value should be 1.0. All I have to do is keep the scalar for each scrollbar. (Since globals are evil, we'll hide the scalars off in our own namespace.)

Putting it all together gets us this:

  namespace eval ::sbset:: {
    variable scalar
    }

  proc scrollbar.set.cmd {sb min_size units first last} {

    # If min_size is in pixels, convert it to a proportion
    if {$units eq {pixels}} {
      if {[string equal -length 1 [$sb cget -orient] v]} \
        then {set trough_length [expr {[winfo height $sb]}]} \
        else {set trough_length [expr {[winfo width  $sb]}]}
      set trough_length [expr {$trough_length -(2 *[$sb cget -width])}]
      set min_size [expr {double( $min_size ) /$trough_length}]
      }

    # Get the size the text widget thinks the slider ought to be
    # (as a proportion)
    set desired_size [expr {$last -$first}]

    set ::sbset::scalar($sb) 1.0

    # Make sure the actual size of the slider never gets too small
    if {$desired_size < $min_size} {
      set last [expr {$first +$min_size}]
      if {$last > 1.0} {
        set first [expr {1.0 -$min_size}]
        set last  1.0
        if {$first < 0.0} {set first 0.0}
        }

      # (but remember how to recover the desired 'first')
      set ::sbset::scalar($sb) [expr {((1.0 -$desired_size) /(1.0 -$min_size))}]
      }

    # This keeps the slider around even if it is too small.
    # That way the scrollbar doesn't automatically disable itself.
    # (Even if the slider can't be used the buttons still can.)
    # Of course, we only do this if we modified the slider size...
    if {($first == 0.0) && ($last == 1.0) && ($desired_size != 1.0)} {
      set first 0.01  ;# Make sure both buttons are still usable
      set last  0.99
      }

    $sb set $first $last
    }

  proc widget.view.cmd {sb widget cmd0 cmd1 number {units units}} {
    # widget xview moveto fraction      <-- what we're interested in fixing
    # widget xview scroll number units  <-- don't mess with this
    if {$cmd1 eq {moveto}} \
      then {$widget $cmd0 moveto [expr {$number *$::sbset::scalar($sb)}]} \
      else {$widget $cmd0 $cmd1 $number $units}
    }

Notice how we have to pass along extra information in the commands, particularly in the widget.view.cmd: we give it the scrollbar name so that we can get the scalar value back out of our ::sbcmd::scalar() array.

  .h config -command        {widget.view.cmd   .h  .t xview}
  .v config -command        {widget.view.cmd   .v  .t yview}
  .t config -xscrollcommand {scrollbar.set.cmd .h 0.3 fractions}
  .t config -yscrollcommand {scrollbar.set.cmd .v  50 pixels}

If you play around with this you'll see it is a very solid solution, lining up the contents of the widget perfectly to the beginning and end.


Really big documents make things go slow

TODO (with simple explanation and links to Mass-widget, etc.


I'd like to scroll past the end of my document

TODO (explain better and give examples)


What's this gap between my document and its scrollbar?

TODO (explain, Tk 8.5 obviates, Internal Scrollbars)


Why does my horizontal scrollbar keep changing size?

TODO


Commentary

wdb The idea of avoid 4-px sliders is really great. I had the same idea. But you have been faster! -- I have not yet tested, but I will!

Duoas Thanks! It is the thing that I've found most obnoxious over the years in using scrollbars and in implementing my own megawidget scrollbars. You remind me also for another reason small sliders are bad: some people do not have mouse-to-pixel resolution: their mouse pointer "jumps" in small (4- to 8-pixel) increments over the display. Now imagine a slider between one of those points. ;->

schlenk I think packages like tklib autoscroll for automatically hiding scrollbars should probably be mentioned here.

Duoas Oh yes. Thank you! As I mentioned at the top, feel free to add information as appropriate. (I've already followed your suggestion and added the autoscroll reference above.) This is very much a work in progress. My intention is to gather all these scrollbar things together in a way that both new and experienced users can find useful.


(from above) LV So, when designing a particular widget, what does the numbers represent?

Duoas Good question. I've updated the section above to more carefully address the issue you raised. Tell me what you think.

Every widget is different. In the scrollbar's, the numbers represent what proportion of the trough (or document) the slider (or visible area) covers.

Also, I appreciate most of the changes someone (I don't know, or care, who) has made above. All improvements in readability are good. But please be careful not to put words in my mouth. Scrollbar widgets are more difficult to use than most normal widgets. Buttons, menus, comboboxes, etc. are all straight-forward and relatively self-contained. A scrollbar, in comparison, often requires that you maintain a lot of additional state information when must be somehow, and intelligently, filtered down into two proportional numbers, both on input and on output from the scrollbar widget. The difficulties this entails, while not the widget's fault, per se, are directly related to the difficulty in using a scrollbar --and evidenced by the massive amount of attention given them in every GUI programming environment. If it were obvious or particularly easy, much fewer people would be confused by it. /me puts soapbox away

HJG On large documents, there is another problem related to the tiny handle: the area of trough1 or trough2 becomes too small to be usable on the first/last few pages.

Duoas Very true. I've no idea how to make that better without radically redesigning the scrollbar itself. The smaller the scrollbar widget itself, no matter the document length, the more pronounced that problem.

Lars H: Isn't that problem just a variation on the size-of-handle problem? You can decide on a "smallest nonzero trough size" and modify the scale accordingly. A difference is perhaps that this modification would be nonlinear (or at least more nonlinear than that you already have), but the idea is the same.