Version 40 of scrodget

Updated 2011-11-07 11:14:35 by RLE

ABU 26-jan-2005 - created

ABU 10-jun-2005 - Updated 2.1 -- also with tile support ! ... see below

Scrodget is a generic scrolled widget implemented with snit.

JH: Others may be interested in a snit-based widget set that I have been creating in tklib (the widget module [L1 ]). You will also find it in ActiveTcl. It is designed to use tile throughout.

It is very similar to Bwidget::ScrolledWindow (with the added capability to place (automatic) scrollbars on top/bottom left/right.

Here is a sample:

http://web.tiscali.it/irrational/tcl/scrodget-2.1/demo/demo.gif

Start page and download links at [L2 ]


Remember that Tcl/Tk source script widgets can be submitted to tklib for more general distribution.


PWQ What if I want scroll bars on left and right?


ABU ??? It is a really unusual demand ... 1-feb-2005 Done.


Peter Newman 27 January 2005: Scrodget looks great. I use BWidgets ScrolledWindow a lot - but one thing that annoys me about it is that you can't turn auto-hiding off (and have either or both scrollbars fixed).

Scrodget fixes that. But it would be better if one could enable/disable autohiding, for the horizontal and vertical scrollbars, separately. Any chance of this?

Also PWQ's request for left AND/or right (and top AND/or bottom) is a bit unusual I agree. But a single Scrollbar Manager widget, that can be configured for every conceivable arrangement, would be very useful. And if you were able to do this, being able to enable/disable auto-hiding for the left, right, top and bottom scrollbars, individually, would IMHO be the best way to go.


ABU 1-feb-2005

Scrodget has been updated. First of all, you can download version 1.0.1 which solves some little BUGS.

Second, version 1.1 is available: it allows you to enable/disable autohiding, for the horizontal and vertical scrollbars, separately.

Third, version 2.0 allows you to enable 4 scrollbars (east/west/north/south).

  • Version 1.0.1 [L3 ]
  • Version 1.1 [L4 ]
  • Version 2.0 [L5 ]

Peter Newman 4 February 2005: Version 2 looks good. But I think you've got west and east mixed up. West is left and east is right; you have them the other way round. Specifying the scrollbar sides with news is an excellent idea. But why not specify the auto-hiding the same way?


ABU .. really a stupid error of mine.

Here is an On-fly patch : file "scrodget.tcl" replace

    typevariable GridIdx  -array {
       n { 0 1 }
       s { 2 1 }
       e { 1 0 }
       w { 1 2 }
    }

with

    typevariable GridIdx  -array {
       n { 0 1 }
       s { 2 1 }
       w { 1 0 }
       e { 1 2 }
    }

Peter Newman 5 February 2005: Thanks! I'd already found that. Your code is really excellent - and simple to follow/debug. And so far (after a few days testing,) seems totally bug-free (after the above fix). This is unlike the BWidget ScrolledWindow where the code is more obscure - and appears to have the following bugs:-

  • BWidget ScrolledWindow Auto-hiding Bug Sometimes you get a scrollbar - either with no slider at all - or with a say 95% slider that doesn't actually slide anything - even though the window contents DON'T seem to require a scrollbar.
  • The BWidgets ScrolledWindow supports the -borderwidth, -relief and -background options - but they rarely seem to work. The values of those same options that you specify for the widget you've wrapped the scrollbars around seem to be used instead. (I say rarely, because there's some strange interaction with the related options of the widget being managed - but I've never been able to figure out exactly what the rules are.)

These things are annoying. But the ScrolledWindow code is so obscure, that so far I've found it easier to live with them and/or work-around them, than to fix them.

The good thing with scrodget is that; not only is it more powerful, but it doesn't appear to have these (or any other) bugs. And even if some bugs are found, the code is quite straightforward, and easy to debug.

So if anyone's wondering; ScrolledWindow or scrodget? The answer is definitely scrodget.


LV So which of the Bwidget widgets will next be recoded?


Access To The Internal Widgets

Another excellent thing about scrodget is that you get access to the internal widgets - which makes it very easy to configure those widgets exactly as you want them.

Essentially, a scrodget is a 3x3 grid, like this:

      #  3 x 3 grid ; 
        #  the central cell (1,1) is for the internal widget.
        #  +-----+-----+----+  
        #  |     | n   |    |
        #  +-----+-----+----+  
        #  | w   |inter| e  |
        #  +-----+-----+----+  
        #  |     | s   |    |
        #  +-----+-----+----+  
        # Cells e or w are for horizontal scrollbar 
        # Cells n or s are for vertical   scrollbar
        #  Note that scrollbars may be hidden.  

And you can configure/cget the internal widgets with:-

 pathName _component_ configure ...
 pathName _component_ cget ...

 where _component_ is one of the following:
        frame
        northScroll
        southScroll
        eastScroll
        westScroll

Simply heaven for picky people who want total control of every pixel.

(Getting Rid Of) The Residual Single Line Grid

The only change I've made to ABU's code, is that he grids the internal widget and scrollbars with:-

 grid $internalW -in $win -padx 1 -pady 1 -row 1  -column 1  -sticky news
 grid $sb                 -padx 1 -pady 1 -row $r -column $c -sticky $sticky

which results in a 1 pixel line all around the grid - even if you set the internal and scrollbar widget -borderwidth and -padx/y etc options to zero .

I've changed that -padx/y to 0, ie:-

 grid $internalW -in $win -padx 0 -pady 0 -row 1  -column 1  -sticky news
 grid $sb                 -padx 0 -pady 0 -row $r -column $c -sticky $sticky

so that you don't get that 1 pixel line.


ABU 23-feb-2005

Scrodget has been updated (2.0.1). Many thanks to Peter for his precious comments.


Peter Newman 23 February 2005: Actually, another very useful enhancement to scrodget, would be to make the frame and scrollbar pathnames available - so that one can bind to them, or modify their bindings.

From inspection (of Scrodget version 2.0), the following applies:-

        # ------------------------------------------------------
        # For a `scrodget', the internal pathnames are:-
        #
        # -- The scrodget widget itself - which is actually a
        #    frame that contains the scrollbars and the internal
        #    widget - where the frame's pathname is the same as
        #    that you assigned to the `scrodget'...
        #
        # -- The scrollbars are then:-
        #       ${scrodgetPathname}.northScroll
        #       ${scrodgetPathname}.southScroll
        #       ${scrodgetPathname}.westScroll
        #       ${scrodgetPathname}.eastScroll
        #
        # -- And the internal (managed) widget has whatever name
        #    you gave to the widget you `associated' with the
        #    scrodget.
        #
        # So for example, if you go:-
        #
        #       scrodget .myScrodget ;
        #       ...
        #       canvas .myCanvas ;
        #       ...
        #       .myScrodget associate .myCanvas ;
        #
        # you end up with (potentially,) the following widgets:-
        #
        #       .myScrodget             (`scrodget'/frame)
        #       .myScrodget.northScroll (scrollbar)
        #       .myScrodget.southScroll (scrollbar)
        #       .myScrodget.westScroll  (scrollbar)
        #       .myScrodget.eastScroll  (scrollbar)
        #       .myCanvas               (canvas)
        #
        # ------------------------------------------------------

LV If the author would prefer not to be bound to these names, they could always provide some method of introspection so that one could query the widget to ask for the appropriate info.


ABU 5-apr-2005

I wish to repeat again what it is officially documented in [L6 ] as clearly exposed by Peter:

If you need to query/configure the internal _components_ (the frame and the 4 scrollbars), you can make use of the the supported (official) _components_ names. These are:

  • frame
  • northScroll
  • southScroll
  • eastScroll
  • westScroll

Therefore, you can get/set ALL components properties using something like this:

   # get frame's -padx
  set padx [.myScrodget frame cget -padx]
   # set frame's -padx/y
  .myScrodget frame configure -padx 10 -pady 10
   # change North-scrollbar's width
  .myScrodget northScroll configure -width "3m"

If you need to query/configure the associated-widget (text/canvas/...any scrollable widget), you have total control, since it is not created 'within' scrodget, but it is a widget you've created before and then associated with scrodget.


ABU 10-jun-2005

Scrodget 2.1 is now available. This release allows you to use tile themes for an improved look&feel.


VK 26-may-2007 I've tried using snit scrolled together with ListBox from BWidget package, scrolling works okay but ListBox 1) do not send <<ListboxSelect>> anymore 2) <1> and selection do not work either. Anyone know this problem? TIA.


See also:


JOB scrodget converted to a pure TclOO megawidget (see code below) - might be a starting point for a pure TclOO set of mega-widgets. Any suggestions?

# -------------------------------------------------------------------------
# --- scrolledwidget.tcl
# -------------------------------------------------------------------------
# Revision history of this code:
#
# Scrodget:
#     Scrodget enables user to create easily a widget with its scrollbar.
#     Scrollbars are created by Scrodget and scroll commands are automatically
#     associated to a scrollable widget with Scrodget::associate.
#
#    scrodget was inspired by ScrolledWidget (BWidget)
#
#  Copyright (c) 2005 <Irrational Numbers> : <[email protected]>
#
#  This program is free software; you can redistibute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation.
#
#  See the file "license" for information on usage and
#  redistribution of this program, and for a disclaimer of all warranties.
#
# -------------------------------------------------------------------------
# scrolledwidget:
#   07.11.2011: Johann [dot] Oberdorfer [at] googlemail [dot] com
#
#   A TclOO approach, just to see how difficult it would be to convert a
#   snit widget to tcloo. In fact it was an easy one!
#  -enjoy-
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------


package require TclOO
package require tile

package provide scrolledwidget 0.2

namespace eval ::scrolledwidget {
        
        namespace export scrolledwidget
        
        # this is a tk-like wrapper around the class,
        # so that object creation works like other Tk widgets

        proc scrolledwidget {path args} {
                set obj [ScrolledWidget create tmp $path {*}$args]
                rename $obj ::$path
                return $path
        }
        
        oo::class create ScrolledWidget {
                
                variable widgetOptions
                variable widgetCompounds
            variable auto
                variable isHidden
                variable GridIdx
                
                constructor {path args} {

                        my variable widgetOptions
                        my variable widgetCompounds
                        my variable isHidden
                        my variable GridIdx
            
            set widgetCompounds(internalW) {}

                        array set widgetOptions {
                                -scrollsides  se
                                -autohide 0
                        }
                        
                        # for each 'side' (n,s,w,e)
                        #  define the position {row,col} within the 3x3 grid
                        array set GridIdx {
                                n { 0 1 }
                                s { 2 1 }
                                w { 1 0 }
                                e { 1 2 }
                        }
                        
                        # attributes for south/north (or east/west) are paired
                        set isHidden(ns)  0
                        set isHidden(ew)  0
                        
                        set win [ttk::frame $path -class ScrolledWidget]
                        
            if { [tk windowingsystem] eq "aqua" } {
                        set sb "scrollbar"
            } else {    set sb ttk::scrollbar }

                        set widgetCompounds(frame) $win
                        set widgetCompounds(northScroll) [$sb $win.northScroll -orient horizontal]
                        set widgetCompounds(southScroll) [$sb $win.southScroll -orient horizontal]
                        set widgetCompounds(eastScroll)  [$sb $win.eastScroll]
                        set widgetCompounds(westScroll)  [$sb $win.westScroll]
                        
                        # fix against deprecated scrollbar set/get syntax
                        $widgetCompounds(northScroll) set 0 1
                        $widgetCompounds(southScroll) set 0 1
                        $widgetCompounds(eastScroll)  set 0 1
                        $widgetCompounds(westScroll)  set 0 1
                        
                        # 3 x 3 grid ;
                        #  the central cell (1,1) is for the internal widget.
                        #  +-----+-----+----+
                        #  |     | n   |    |
                        #  +-----+-----+----+
                        #  | w   |inter| e  |
                        #  +-----+-----+----+
                        #  |     | s   |    |
                        #  +-----+-----+----+
                        # Cells e or w are for vertical scrollbars
                        # Cells n or s are for horizontal scrollbars
                        # Note that scrollbars may be hidden.
                        
                        grid rowconfig    $win 1 -weight 1 -minsize 0
                        grid columnconfig $win 1 -weight 1 -minsize 0
                        
                        # we must rename the widget command since it clashes with
                        # the object being created
                        
                        set win ${path}_
                        rename $path $win
                        
                        my SetAutohide $widgetOptions(-autohide)
                        my SetScrollsides $widgetOptions(-scrollsides)
                        
                        my configure {*}$args
                }

        # public methods starts with lower case declaration names,
        # whereas private methods starts with uppercase naming
                
                method cget { {opt "" }  } {
                        my variable widgetOptions
                        
                        if { [string length $opt] == 0 } {
                                return [array get widgetOptions]
                        }
                        if { [info exists widgetOptions($opt) ] } {
                                return $widgetOptions($opt)
                        }
                        return -code error "# unknown option"
                }
                
                method configure { args } {
                        my variable widget
                        my variable widgetOptions
                        
                        if {[llength $args] == 0}  {
                                
                                # return all custom options
                                return [array get widgetOptions]
                                
                        } elseif {[llength $args] == 1}  {
                                
                                # return configuration value for this option
                                set opt $args
                                if { [info exists widgetOptions($opt) ] } {
                                        return $widgetOptions($opt)
                                }
                                return [$widget cget $opt]
                        }
                        
                        # error checking
                        if {[expr {[llength $args]%2}] == 1}  {
                                return -code error "value for \"[lindex $args end]\" missing"
                        }
                        
                        # process the new configuration options...
                        array set opts $args
                        
                        foreach opt [array names opts] {
                                set val $opts($opt)
                                
                                # overwrite with new value
                                if { [info exists widgetOptions($opt)] } {
                                        set widgetOptions($opt) $val
                                }
                                
                                # some options need action from the widgets side...
                                switch -- $opt {
                                        -scrollsides {
                                                if { ! [regexp -expanded  {^[nesw]*$} $val] } {
                                                        return -code error "# bad scrollsides \"$opt\": only n,s,w,e allowed"
                                                }
                                                my SetScrollsides $val
                                        }
                                        -autohide {
                                                my SetAutohide $val
                                        }
                                        default {
                                                return -code error "unknown configuration option: \"$opt\" specified"
                                        }
                                }
                        }
                }
                
                method associate { args } {
                        my variable widgetCompounds
                        
                        switch -- [llength $args] {
                                0 { return $widgetCompounds(internalW) }
                                1 { set w [lindex $args 0] }
                                default {
                                        return -code error \
                        "wrong # args: should be \"$ $widgetCompounds(frame) associate ?widget?\""
                                }
                        }
                        
                        if { $w != {}  &&  ! [winfo exists $w] } {
                                return -code error "error: widget \"$w\" does not exist"
                        }
                        
                        # detach previously associated-widget (if any)
                        catch {
                                grid forget $widgetCompounds(internalW)
                                $widgetCompounds(internalW) configure -xscrollcommand {} -yscrollcommand {}
                        }
                        
                        set widgetCompounds(internalW) $w
                                
                        $widgetCompounds(eastScroll) configure  -command "$w yview"
                        $widgetCompounds(westScroll) configure  -command "$w yview"
                        $widgetCompounds(northScroll) configure -command "$w xview"
                        $widgetCompounds(southScroll) configure -command "$w xview"

                        $w configure \
                                        -xscrollcommand "[self] auto_setScrollbar $widgetCompounds(northScroll) $widgetCompounds(southScroll)" \
                                        -yscrollcommand "[self] auto_setScrollbar $widgetCompounds(eastScroll) $widgetCompounds(westScroll)"
                                
                        catch {raise $w $widgetCompounds(frame)}
                        grid $w -in $widgetCompounds(frame) -row 1 -column 1  -sticky news
                 }

                
                method auto_setScrollbar { sbA sbB {first 0} {last 1} }  {
                        my variable widgetOptions
                        my variable auto
                        my variable isHidden
                        
                        set sideA  [my WhichSide $sbA]
                        set orient [my WhichOrient $sbA]
                        
                        if { $auto($orient) } {
                                if { $first == 0 && $last == 1 } {
                                        if { ! $isHidden($orient) } {
                                                grid forget $sbA
                                                grid forget $sbB
                                                set isHidden($orient) 1
                                        }
                                } else {
                                        if { $isHidden($orient) } {
                                                my ShowScrollbar $sbA $widgetOptions(-scrollsides)
                                                my ShowScrollbar $sbB $widgetOptions(-scrollsides)
                                                set isHidden($orient) 0
                                        }
                                }
                        }
                        $sbA set $first $last
                        $sbB set $first $last
                }

 
                   # from the scrollbar's name, derive its 'side'
                #   i.e.  [WhichSide $widgetCompounds(westScroll])  returns 'w'

                method WhichSide { sb } {
                        return [string index [winfo name $sb] 0]
                }

                
                # from the scrollbar's name, derive its 'orientation'
                # return values are: "ns" or "ew"
                # note: [my WhichSide $widgetCompounds(northScroll)] returns 'ew' (i.e. horizontal)
                
                method WhichOrient { sb } {
                        set side [my WhichSide $sb]
                        if { [string first $side "ns"] >= 0 } {
                                set orient ew
                        } else {
                                set orient ns
                        }
                        return $orient
                }
                

                method SetScrollsides {sides} {
                        my variable widgetOptions
                        my variable widgetCompounds
                        my variable isHidden
           
                        set widgetOptions(-scrollsides) $sides
                        if { ! $isHidden(ew) } {
                                my ShowScrollbar $widgetCompounds(northScroll)  $widgetOptions(-scrollsides)
                                my ShowScrollbar $widgetCompounds(southScroll)  $widgetOptions(-scrollsides)
                        }
                        if { ! $isHidden(ns) } {
                                my ShowScrollbar $widgetCompounds(eastScroll)   $widgetOptions(-scrollsides)
                                my ShowScrollbar $widgetCompounds(westScroll)   $widgetOptions(-scrollsides)
                        }
                }
                
                
                # note: both scrollbars have the same orientation

                method HandleAutohide { sbA sbB } {
                        my variable widgetOptions
                        my variable auto
                        my variable isHidden
                        
                        set sideA  [my WhichSide $sbA]
                        set orient [my WhichOrient $sbA]
                        
                        if { $auto($orient) } {
                                # 1/true : check if scrollbar should be hidden
                                #          (based on the scrollbar's visible range)
                                my auto_setScrollbar $sbA $sbB {*}[$sbA get]
                        } else {
                                # 0/false : if scrollbars are hidden, then show them
                                if { $isHidden($orient) } {
                                        my ShowScrollbar $sbA  $widgetOptions(-scrollsides)
                                        my ShowScrollbar $sbB  $widgetOptions(-scrollsides)
                                        set isHidden($orient) 0
                                }
                        }
                }

                
                method BoolValue {x} {
                         if { [string is boolean $x] } {
                                if { "$x" } { return 1 } else { return 0 }
                        }
                        return -code error "# not a boolean value"
                }

        
                method SetAutohide {value} {
                        my variable widgetOptions
                        my variable widgetCompounds
                        my variable auto
                        
                        set value1 $value
                        # normalize boolean value (if boolean)
                        catch { set value1 [my BoolValue $value1] }
                        
                        switch -- $value1 {
                                0 -
                                none       { set auto(ew) 0 ; set auto(ns) 0 }
                                vertical   { set auto(ew) 0 ; set auto(ns) 1 }
                                horizontal { set auto(ew) 1 ; set auto(ns) 0 }
                                1 -
                                both       { set auto(ew) 1 ; set auto(ns) 1 }
                                default    { return -code error \
                    "# bad autohide \"$value\": must be none,vertical,horizontal,both or a boolean value"
                                }
                        }
                        set widgetOptions(-autohide) $value
                        
                        my HandleAutohide $widgetCompounds(northScroll) $widgetCompounds(southScroll)
                        my HandleAutohide $widgetCompounds(eastScroll)  $widgetCompounds(westScroll)
                }
                
                
                method ShowScrollbar {sb validSides} {
                        my variable GridIdx
                        
                        set side [my WhichSide $sb]
                        if { [string first $side $validSides] != -1 } {
                                set r [lindex $GridIdx($side) 0]
                                set c [lindex $GridIdx($side) 1]
                                set sticky [my WhichOrient $sb]
                                grid $sb -row $r -column $c -sticky $sticky
                        } else {
                                grid forget $sb
                        }
                }
        }

}