[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?''' * (more?) ---- ---- '''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 an appropriate command: scrollbar .h -orient horizontal -command {.t xview} scrollbar .v -orient vertical -command {.t yview} text .t -wrap none -yscrollcommand {.v set} -xscrollcommand {.h set} 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 columnconfigure . 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. '''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 pack manager not to display the scrollbar anymore. 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} ---- ---- '''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". 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. 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: [http://home.comcast.net/~michaelthomasgreer/scrollbar-fig2.gif] In this image, the scrollbar first and last have been modified. Hence, they do not match the document first and last. The visible portion of the document is much smaller in relation to the invisible portion than the slider is to the trough (trough1 + trough2). We can create a scalar value (a number to multiply with) that changes the scrollbar ratio into the document ratio. Some simple mathematical properties: 12.0 / 100 = 0.12 0.12 * 100 = 12.0 --> 0.12 * (1.0 / 100) = 12.0 The difference is simply inverting the fraction. So, we can simply create a like 'inverter' fraction. 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.) The scalar value 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. .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. ---- ---- '''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]) ---- ---- '''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!