Scrolled frame, canvas, auto scrollbars

Fabricio Rocha - 10 Feb 2011 - Scrollable frames which can hold more widgets than the available space could hold are an old desire of many Tclers. There are some ready-to-use solutions like BWidget ScrolledFrame. But I had read that canvas was often used for this task, and became pretty curious about how could this work, specially with scrollbars that would appear or disappear as needed.

I posted a question about this in comp.lang.tcl but some hours later I got myself a working solution. As I wanted not a ready solution but the logic behind it, some other people might be also interested, so here is an attempt to explain it:

1) The first thing is to create a base frame and, on it, a canvas and its scrollbar(s). Make sure that the canvas can be shrinked or expanded when the same happens to the base frame (for example, with -sticky news option and grid columnconfigure's -weight for the canvas column. Then create the "inner frame" and put it inside the canvas with the command $canvas create window. This command returns a handler (actually, an "itemID") which must be stored in a namespaced or global variable.

2) The canvas, as other scrollable widgets, supports the -yscrollcommand and -xscrollcommand configuration options. These options allow you to define procedures to be called when the visible area changes in the widget. Usually these procedures are the $scrollbar set commands for each respective scrollbar, but these options allow other procedures to be called -- for example, one that will verify if the scrollbars are really needed, before calling the necessary $scrollbar set command. Interception, overloading, call it as you want...

3) Create the proc which will add verification to the scrollbars. The command defined for -yscrollcommand or -xscrollcommand receives as argument a list of two fractional numbers which tell how much of the widget's contents is currently shown in its window and its axis. If the first one is 0 and the second one is 1, it means that all the contents are visible, so the scrollbar can be hidden (by grid remove, for example); otherwise they can be placed back by the geometry manager and have their set command called.

4) Put the widgets as you want in the inner frame, ensure they are gridded or packed to be expanded or reduced, but know that the frame itself will be probably shrinked to wrap the widgets, and this might leave some blank space visible inside the canvas when it is resized -- specially if, for example, you want the inner frame to be scrolled in only one direction, while its widgets should grow or shrink in the other axis. For avoiding this, after adding the widgets to the inner frame, you should call a proc which will update its size. Why a proc? Because the same operation must be done when the canvas itself is resized -- for doing this, use a bind to the canvas' <Configure> event for calling the same resizing procedure.

5) The resizing procedure must initially check again if the scrollbar(s) is/are needed, because their visibility affects the canvas size -- call the same procedure described in item 3 for each axis. Then, retrieve the canvas size with winfo width and/or winfo height and use this information with the command $canvas itemconfigure $innerframeID. I found out that update idletasks is needed in the middle of this procedure for ensuring that the retrieved canvas measures are valid. Finally, the canvas -scrollregion option must be configured for scrolling to work: a canvas has a virtual size of 32k X 32k pixels and this option defines which portion of the area will be "available" in the canvas window. In our case, this portion is exactly the inner frame, so the value for -scrollregion can be the result of $canvas bbox $innerframeID.

As soon as I can clean the code I wrote with this logic, I will post it here. I hope that one of the Tk gurus out there can suggest a more efficient algorithm!


RS 2014-01-27 Here is my solution for decorating a widget with horizontal and vertical scrollbars:

 package require Tk

 proc scrolled w {
    set pa [set pa0 [winfo parent $w]]
    if {$pa eq "."} {set pa ""}
    grid $w [scrollbar $pa.y -command "$w yview"] -sticky news
    $w configure -xscrollcommand "$pa.x set" -yscrollcommand "$pa.y set"
    grid $pa.y -sticky ns
    grid [scrollbar $pa.x -ori hori -command "$w xview"] -sticky ew
    grid columnconfigure $pa0 0 -weight 1
    grid rowconfigure    $pa0 0 -weight 1
 }

#-- Example, with a canvas

 canvas .c -background beige 
 .c create line 10 10 1000 1000
 .c configure -scrollregion [.c bbox all]
 scrolled .c

HE 2014-01-27 RS can you please explain how this should work with a frame to get a 'scrolled frame'?