[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 ming-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?) 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 the programmer's point of view. For 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 scrollbar '''set''' sub-command takes two floating-point numbers in the range 0.0 to 1.0, inclusive. The handle (or grip or "slider") in the above graphic is positioned at about "0.07 0.43". If you set the handle to range from 0.0 to 1.0, the scrollbar becomes useless and 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. 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). An '''associated window''' displays a '''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. 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} If you want to understand better how these commands actually behave, head on over to a [Scrollbar tutorial]. For this kind of arrangement, the grid geometry manager is useful: 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 ). "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 it not mentioned here. However, since it is an effect due to our scrollbars/document interaction, 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 on my keyboard the long line scrolls down out of view. Since there are no more long lines, the horizontal scrollbar disappears. But, once it disappears, the long line is back in view again! So the horizontal scrollbar 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] By "specific size" you can mean in percentage of the range between 0.0 and 1.0, or you can mean number of pixels high. '''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 units first last} { 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 [expr {double( $min ) /$trough_length}] } set range [expr {$last -$first}] set ::sbset::scalar($sb) 1.0 if {$range < $min} { set ::sbset::scalar($sb) [expr {((1.0 -$range) /(1.0 -$min))}] set last [expr {$first +$min}] if {$last > 1.0} { set first [expr {1.0 -$min}] set last 1.0 if {$first < 0.0} {set first 0.0} } } # 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) && ($range != 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.