Windows Application Framework

EKB 2005-07-08:

I have now Snitified this package. The code samples have been replaced with the new Snit versions.''

I've been working towards making a package that will automatically take care of some tricks for using Tk under Windows that I learned once and don't want to have to learn again. For example:

  • Binding the "x" button to the exit procedure
  • Linking to an .ico file
  • Creating a resizer/gripper in the bottom-right corner (using code from the sizer control page on this Wiki)
  • Creating tooltips (also known as balloon help) using the code from the Snit Tooltips page
  • Creating a toolbar and finding button images for it (I embedded some inline images, but you might want to use ICONS instead!)
  • Adding a status bar, and connecting it to the toolbar buttons
  • Saving preferences in the user's directory
  • ... and so on ...

I've put some of these things together into a simple application framework. It uses some of my own code, as well as code from other pages on the Tcler's Wiki and the web. (These are documented in the code.)

Here's an example of an application created using the Application Framework code (This is actually the program I used to create the base64-encoded button strings that appear in the Framework, so it is hopefully useful by itself.)

http://www.kb-creative.net/screenshots/AppFrameExample.gif

To use the "Transparent Color" feature in the program, first fill any areas in the image that you want to make transparent with some color, for example, pure green (R = 0, G = 255, B = 0). Then type those numbers into the R, G, B entry widgets. When the image is loaded, any pixels that are set to (0,255,0) will be made transparent. The screenshot above shows this image loaded in the program: http://www.kb-creative.net/images/find.gif.

And here's the code to make the example, using the Application Framework (which is farther down on the page) A couple of things to notice in the code:

  • Any values assigned to the "globalPrefs" array or set using the "myapp setpref" method are automatically saved on exit and retrieved when the program is restarted
  • Balloon help is available for all widgets, including toolbar buttons

Example

package require base64
 
## -- Do this before making the GUI
source "AppFrame.tcl"
WinApp::appframe myapp -title "Translate Gif" -statusrelief groove
 
## -- Set any preferences
myapp setpref folder $WinApp::appdir
 
## -- Init will load preferences and set the focus
myapp init

## -- Create the GUI
[myapp menu] add cascade -label "File" -menu [myapp menu].file
menu [myapp menu].file -tearoff false
[myapp menu].file add command -label "Open" -command FindFile
[myapp menu].file add command -label "Exit" -command {myapp exitproc} \
    -underline 1 -accelerator "Ctrl+Q"

set openBtn [myapp addButton -imagedata open -descr "Browse for a GIF file" \
    -shortdescr "Find file" -command FindFile]
set copyBtn [myapp addButton -imagedata copy -descr "Copy base64-encoded string" \
    -shortdescr "Copy encoded image" -command CopyAll]

# Interface
text [myapp main].t -height 1
pack [myapp main].t -side top -expand yes -fill both
frame [myapp main].f
pack [myapp main].f -fill x -side top
label [myapp main].f.l -text "File: "
pack [myapp main].f.l -side left
entry [myapp main].f.e -textvariable filename
pack [myapp main].f.e -side left -expand yes -fill x
button [myapp main].f.b -text "Browse..." -command FindFile
pack [myapp main].f.b
 
# A button to put the image preview in
image create photo sampleImg

frame [myapp main].fi
pack [myapp main].fi -fill x -padx 2 -pady 4

label [myapp main].fi.trans -text "Transparent color: "
pack [myapp main].fi.trans -side left
label [myapp main].fi.rl -text " R"
pack [myapp main].fi.rl -side left
entry [myapp main].fi.r -width 4 -textvariable WinApp::globalPrefs(red)
pack [myapp main].fi.r -side left
label [myapp main].fi.gl -text " G"
pack [myapp main].fi.gl -side left
entry [myapp main].fi.g -width 4 -textvariable WinApp::globalPrefs(green)
pack [myapp main].fi.g -side left
label [myapp main].fi.bl -text " B"
pack [myapp main].fi.bl -side left
entry [myapp main].fi.b -width 4 -textvariable WinApp::globalPrefs(blue)
pack [myapp main].fi.b -side left
 
button [myapp main].fi.i -image sampleImg -activebackground red -width 22 -height 22
pack [myapp main].fi.i -side right -anchor e
WinApp::tooltip register [myapp main].fi.i "Press to check transparency"
label [myapp main].fi.preview -text "Preview: "
pack [myapp main].fi.preview -side right -anchor e
 
## -- Commands
proc CopyAll {} {
    [myapp main].t tag remove sel 1.0 end
    [myapp main].t tag add sel 1.0 end
    tk_textCopy [myapp main].t
}
 
proc FindFile {} {
    global filename
 
    set filename [tk_getOpenFile -initialdir [myapp getpref folder] \
        -filetypes {{{GIF files} {.gif} } {{All files} * }}]
    
    if {$filename == ""} {return}
    
    myapp setpref folder [file dirname $filename]
 
    # Open the file
    sampleImg config -file $filename

    # Add transparency, if need be
    set maxX [image width sampleImg]
    set maxY [image height sampleImg]
    for {set x 0} {$x < $maxX} {incr x} {
        for {set y 0} {$y < $maxY} {incr y} {
            set curcolor [sampleImg get $x $y]
            set istcolor true
            if {[lindex $curcolor 0] != [[myapp main].fi.r get]} {
                set istcolor false
            } elseif {[lindex $curcolor 1] != [[myapp main].fi.g get]} {
                set istcolor false
            } elseif {[lindex $curcolor 2] != [[myapp main].fi.b get]} {
                set istcolor false
            }
            if $istcolor {sampleImg transparency set $x $y true}
        }
    }
    
    sampleImg write "_temp.gif" -format GIF
    
    set fileID [open "_temp.gif" RDONLY]
    fconfigure $fileID -translation binary
    set rawData [read $fileID]
    close $fileID
    set encodedData [base64::encode $rawData]

    sampleImg config -data $encodedData
    
    [myapp main].t delete 1.0 end
    [myapp main].t insert end $encodedData
}

If this application and/or the framework seem useful, please add to them, modify them and improve them!

Framework

Here's the framework as it currently stands (last modified 8 July 2005). It includes a lot of embedded image data with standard button images (open, save, etc.) -- use [WinApp::buttonlist] to see the pre-defined images.

package require snit

namespace eval WinApp {

    ## -- Utility proc to easily insert a stub
    proc stub {msg} {
        tk_messageBox -message $msg -title "STUB"
    }
    
    # An array of buttons, defined for convenience (see below)
    variable btn
    
    # An array of prefs that apply to all appframes
    variable globalPrefs
        
    # Save the directory from which the program is called & info about user dirs
    variable appdir [file dirname $argv0]
    snit::type appframe {
        
        ##
        ## Options
        ##
        option -title "AppFrame Application"
        option -exitproc ""
        option -statusrelief flat
        option -icon ""
        option -toplevel "."


        ##
        ## Variables
        ##

        typevariable USERDIR
        typevariable USERPREFS


        # Array with various window components -- main, menu, statusbar, toolbar
        variable component

        # Array with prefs to save and restore
        variable prefs
        
        ## Keep track of numbers of separators and buttons on the toolbar
        variable tbSepNum 0
        variable tbBtnNum 0
        
        # This is accessed via the selfns
        variable status
        
        constructor {args} {
            $self configurelist $args
            
            # Generic prefs
            set prefs(geometry) 300x200
            set prefs(isMaximized) false

            ##
            ## Create the menu
            ##
            # Take care of case where toplevel is just "." -- don't have multiple "."'s
            if {$options(-toplevel) == "."} {
                set tlvl ""
            } else {
                set tlvl "$options(-toplevel)"
            }
            
            $options(-toplevel) configure -menu $tlvl.appframeMenu
            set component(menu) [menu $tlvl.appframeMenu]
            
            if {$options(-icon) != ""} {
                wm iconbitmap $options(-toplevel) -default $options(-icon)
            }
            
            wm title $options(-toplevel) $options(-title)
            
            # Put the exit procedure action in 2 places -- "x" button and keyboard shortcut
            wm protocol . WM_DELETE_WINDOW [mymethod exitproc]
            bind . <Control-q> [mymethod exitproc]
            
            ## Toolbar
            set component(toolbar) [frame $tlvl.appFrameToolbar]
            grid $tlvl.appFrameToolbar -row 0 -sticky ew -columnspan 2
            
            ## Status bar & resizer
            set component(statusbar) [frame $tlvl.appFrameStatus -height 10]
            grid $tlvl.appFrameStatus -row 2 -sticky ew
            
            label $tlvl.appFrameStatus.announce -textvariable [myvar status] -anchor w  \
                -relief $options(-statusrelief)
            pack $tlvl.appFrameStatus.announce -side left -fill x -expand yes
            
            # Make this the same size as the resizer to give it room
            frame $tlvl.appFrameStatus.resizer -width 16 -height 16
            pack $tlvl.appFrameStatus.resizer -side right
            
            WinApp::sizer::sizer $options(-toplevel)
            
            ## Main app space
            set component(main) [frame $tlvl.appFrameMain]
            grid $tlvl.appFrameMain -row 1 -sticky nsew
            
            ## Application grid
            grid rowconfig $options(-toplevel) 1 -weight 1
            grid columnconfig $options(-toplevel) 0 -weight 1
            
        }
        
        ##
        ## Methods
        ##
        method init {} {
            $self LoadPrefs $options(-title) "prefs.tcl"
            # Have to do this, at least in Windows, to ensure app has focus when started
            focus -force .
        }
        method getpref {id} {
            return $prefs($id)
        }
        method getprefids {} {
            return [array names prefs]
        }
        method setpref {id val} {
            set prefs($id) $val
        }
        method component {item} {
            if {![info exists component($item)]} {
                error "\"$item\" is not a recognized AppFrame component"
             }
            return $component($item)
        }
        # Shortcut for "$self component main"
        method main {} {
            return $component(main)
        }
        # Shortcut for "$self component menu"
        method menu {} {
            return $component(menu)
        }
        
        ## -- An exit procedure that saves preferences and exits
        method exitproc {} {
            # handler is the user's exit handler
            if {$options(-exitproc) != ""} {
                eval $options(-exitproc)
            }
            $self SavePrefs
            exit
        }
        
        method setStatus {msg} {
            set status $msg
        }
        method clearStatus {} {
            set status ""
        }
        
        ## -- Proc to add variables to the toolbar
        #
        # Switches:
        #   -image          User-defined image
        #   -imagedata      ID for a pre-defined inline button image
        #   -descr          Text that appears in the status bar when over button
        #   -shortdescr     Text that appears in balloon help when over button
        #   -command        The command for the button
        #
        # NOTE: An image is required for toolbar buttons
        #
        method addButton {args} {

            set button afBtn$tbBtnNum
            incr tbBtnNum
            
            set retval [button $component(toolbar).$button -relief flat -borderwidth 1]
            pack $component(toolbar).$button -side left
            set descr ""
            set shortdescr ""
            set command "WinApp::stub $button"
            set image ""
            
            foreach {opt val} $args {
                switch -exact -- $opt {
                    -imagedata {set image [image create photo -data $WinApp::btn($val)]}
                    -image {set image $val}
                    -descr {set descr $val}
                    -shortdescr {set shortdescr $val}
                    -command {set command $val}
                }
            }
            
            if {$image == ""} {
                error "No image for button \"$button\""
                return
            }
            $component(toolbar).$button config -image $image
            $component(toolbar).$button config -command $command
            bind $component(toolbar).$button <Enter> "set [myvar status] \"$descr\"; if {\[%W cget -state\] == \"normal\"} {%W configure -relief raised}"
            bind $component(toolbar).$button <Leave> "set [myvar status] \"\"; %W configure -relief flat"
            if {$shortdescr != ""} {
               WinApp::tooltip register $component(toolbar).$button "$shortdescr"
            }
            
            return $retval
        }
        
        ## -- Proc to add a separator to the toolbar
        method addSep {} {            
            frame $component(toolbar).sep$tbSepNum -width 5 -borderwidth 0
            pack $component(toolbar).sep$tbSepNum -side left -fill y -padx 4
            incr tbSepNum
        }
                
        ##
        ## Manage Prefs
        ##
        method LoadPrefs {progname prefsfile} {            
            # Get current user's home directory: If environment vars not available,
            # default to subfolder of the installation folder
            set USERDIR $WinApp::appdir
            if {$::tcl_platform(os) == "Windows NT"} {
                if {[info exists ::env(USERPROFILE)]} {
                    set USERDIR $::env(USERPROFILE)
                }
            }
            if {$::tcl_platform(os) == "Windows 95"} {
                if {[info exists ::env(windir)] && [info exists ::env(USERNAME)]} {
                    set USERDIR [file join $::env(windir) Profiles $::env(USERNAME)]
                }
            }
            set USERDIR [file join $USERDIR "Application Data" $progname]
            set USERPREFS [file join $USERDIR $prefsfile]
            
            if {[file exists $USERPREFS]} {
                source $USERPREFS
            }
            
            wm geometry . $prefs(geometry)
            if {$prefs(isMaximized)} {
                wm state . zoomed
            }
        }
        
        method SavePrefs {} {
            if {![file exists $USERDIR]} {
                file mkdir $USERDIR
            }
            
            # Find out if the window is zoomed
            if {[wm state .] == "zoomed"} {
                set prefs(isMaximized) true
            } else {
                set prefs(isMaximized) false
                # Store the current window geometry
                set prefs(geometry) [wm geometry .]
            }
            
            # Don't bother about errors. If can't open, then can't save prefs. That's OK.
            if {![catch {open $USERPREFS w} fileID]} {
                foreach item [array names prefs] {
                    puts $fileID "set prefs($item) \"$prefs($item)\""
                }
                foreach item [array names WinApp::globalPrefs] {
                    puts $fileID "set WinApp::globalPrefs($item) \"$WinApp::globalPrefs($item)\""
                }
                close $fileID
            }
        }
    }
    
    namespace eval sizer {
        ######################################################################
        #
        # From [email protected] (Tcler's Wiki)
        # Modified by EKB to give the glyph more "breathing room"
        #
        ######################################################################
        namespace export sizer
    }
    
    proc sizer::sizer {win} {
        variable config
        variable f
        if {$win=="."} {
            set config($win-widget) .sizer
        } else {
            set config($win-widget) $win.sizer
        }
        canvas $config($win-widget) -width 16 -height 16 -cursor "size_nw_se" -bg SystemButtonFace
        foreach i {0 4 8} {
            # -width 2 means 2point on win98 and 2pixel on w2k
            $config($win-widget) create line [expr $i+3] 13 14 [expr $i+2] -width 1 -fill SystemButtonShadow
            $config($win-widget) create line [expr $i+2] 13 14 [expr $i+1] -width 1 -fill SystemButtonShadow
            $config($win-widget) create line [expr $i+1] 13 14       $i     -width 1 -fill SystemButtonHighlight
        }
        
        set config($win-zoomed) 2 ;# not 0/1
        bind $config($win-widget) <Button-1>  [namespace code [list sizer_start $win %X %Y]]
        bind $config($win-widget) <B1-Motion> [namespace code [list sizer_move $win %X %Y]]
        bind $win                 <Configure> [namespace code [list sizer_update $win]]
    }
    
    proc sizer::sizer_update {win} {
        variable config
        set zoomed [string equal [wm state $win] "zoomed"]
        if {$zoomed!=$config($win-zoomed)} {
            set config($win-zoomed) $zoomed
            if {$zoomed} {
                place forget $config($win-widget)
            } else {
                set x [expr {-16+[$win cget -padx]}]
                set y [expr {-16+[$win cget -pady]}]
                place $config($win-widget) -relx 1.0 -rely 1.0 -x $x -y $y
            }
        }
    }
    
    proc sizer::sizer_start {win x y} {
        variable config
        set config($win-x) $x
        set config($win-y) $y
        scan [wm geometry $win] "%dx%d" config($win-width) config($win-height)
    }
    
    proc sizer::sizer_move {win x y} {
        variable config
        set width  [expr $config($win-width) +$x-$config($win-x)]
        set height [expr $config($win-height)+$y-$config($win-y)]
        catch {wm geometry $win ${width}x${height} }
    }
    
    ##
    ## Button images
    ##
    
    proc buttonlist {} {
        variable btn
        return [lsort [array names btn]]
    }
    
    ## -- File operations
    set btn(save) {
    R0lGODlhEwASAJEAANnZ2QAAAICAAMDAwCH5BAEAAAAALAAAAAATABIAAAJi
    hI+py0jxEQl2SNB8iwmCHRI03yKCYIcEzbeQINghQfMtJAh2SNB8CwmCHRI0
    30KCYIcIxTeRINhB8tGCYIcIxT8Jgh0iFH8mJAh2iFD8mZAg2CFC8WdCguBT
    fDyCj6nLSQUAOw==  
    }
    set btn(open) {
    R0lGODlhEwASAKIAANnZ2QAAAP//AP///4CAAP///////////yH5BAEAAAAA
    LAAAAAATABIAAANnCLrc/jAyFIqgyx0IGhAIutyKEQiqEYGgqxEYAACBMoKh
    Gwi6GoMyMjIoEwi6GoIjIyM4Egi6GoMygqHLAQgQMoKBpLsBCAAxgoGku4Gg
    GhJIuhsIuhFIuhsIuoChy4Ggy+0Po4QoAQA7
    }
    set btn(new) {
    R0lGODlhFwAWAJEAANnZ2U1NTf///////yH5BAEAAAAALAAAAAAXABYAAAJa
    hI+py+0PoyLxjeBjBsmPIPiIQfJDguDjBcmPiCD4aEHyLwg+WpD8C4KPFiT/
    guCjBcm/IPhoQfIvCD5akPwLgo8WJP+C4KMFyb8g+GgUH4PgY+py+8MopyEF
    ADs=  
    }
    set btn(print) {
    R0lGODlhFwAWAKIAANnZ2U1NTf///9PT06ampv//Tf///////yH5BAEAAAAA
    LAAAAAAXABYAAAN5CLrc/jDKSWtEobsIuryBoquBoMsbKBiqEgi6vIGiq4Gg
    yxsoGKqygaDLgaKrMRgTCLqKocsTOBEIuhA4uhyDMYGgCxi6vIMTCLoQOLpE
    hDMxgaCrgaNbVTgTEQi6iqHLOxgIuhs4uhuDgaDLgKHLgaDL7Q+jnLRClAA7  
    }
    set btn(printpreview) {
    R0lGODlhFwAWAKIAANnZ2U1NTf///9PT06ampk3//////////yH5BAEAAAAA
    LAAAAAAXABYAAAOMCLrc/jDK6VDoLoIuc6DoSgSCLm+g6EoMBoIua6DoSgRG
    IOgyBIouB4IuQ6DoSgRGIOgyBIpu0OBQIOhyoKgGFdYQBYLuBopqTOEMTSDo
    bqCoxgzO0ASC7gaKatDgFFEg6G6g6AYNDgUh6G6g6EoERlAEgq4Gii4HYCiC
    LmDosgJCBIIutz+MctLaUAIAOw==  
    }
    
    ## -- Formatting
    set btn(bold) {
    R0lGODlhFwAWAJEAANnZ2U1NTf///////yH5BAEAAAAALAAAAAAXABYAAAJG
    hI+py+0Po5yUkfhG8DEhIghIBMHHjAijmPAxI8IoJnxMih8EHxMigoBEEHzM
    iDCKCR8zIoxiwkek+EbwMXW5/WGUk9ZKCgA7
    }
    set btn(italic) {
    R0lGODlhFwAWAJEAANnZ2U1NTaampv///yH5BAEAAAAALAAAAAAXABYAAAI8
    hI+py+0Po5wUktgEHxMjhOBjokQQfMyMEIKPiRJB8DEzQgg+JkoEwcfMCCH4
    mEfxCT6mLrc/jHLSagEpADs=
    }
    set btn(underline) {
    R0lGODlhFwAWAJEAANnZ2U1NTaampv///yH5BAEAAAAALAAAAAAXABYAAAJL
    hI+py+0Po5yUkThwQfAxIy4ohI8JEUcJ4WNCxFFC+JgQcZQQPiZEHCWEjwkR
    RwnhY0KEEJQg+JhGsQk+pi4fxT+Cj6nL7Q+jnIcUADs=
    }
    set btn(justcenter) {
    R0lGODlhFwAWAJEAANnZ2U1NTf///////yH5BAEAAAAALAAAAAAXABYAAAI2
    hI+py+0Po4wkPiLBx9RVio8EH1NXKT4iwcfUVYqPBB9TVyk+IsHH1FWKjwQf
    U5fbH0Y5qSMFADs=
    }
    set btn(justright) {
    R0lGODlhFwAWAJEAANnZ2U1NTf///////yH5BAEAAAAALAAAAAAXABYAAAI2
    hI+py+0Po4wkPiLBx9Rlio8EH1MXKT4iwcfUZYqPBB9TFyk+IsHH1GWKjwQf
    U5fbH0Y5KSMFADs=
    }
    set btn(justleft) {
    R0lGODlhFwAWAJEAANnZ2U1NTf///////yH5BAEAAAAALAAAAAAXABYAAAI2
    hI+py+0Po4wkPiLBx9RFio8EH1OXKT4iwcfURYqPBB9Tlyk+IsHH1EWKjwQf
    U5fbH0Y5KSQFADs=
    }
    set btn(justfull) {
    R0lGODlhFwAWAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAXABYAAAI3
    hI+py+0Po5xUkfiIBB9TFyk+IsHH1EWKj0jwMXWR4iMSfExdpPiIBB9TFyk+
    IsHH1OX2h1FOUgA7  
    }
    set btn(bullets) {
    R0lGODlhFwAWAJEAANnZ2QAAgAAAAP///yH5BAEAAAAALAAAAAAXABYAAAJD
    hI+py+0Po5QkJnzMjAgCKP8I/kUEwcfU5fYnMeFjZkQQQPlH8C8iCD6mLrc/
    iQkfMyOCAMo/gn8RQfAxdbn9YZSHFAA7  
    }
    set btn(numbering) {
    R0lGODlhFwAWAJEAANnZ2QAAgAAAAP///yH5BAEAAAAALAAAAAAXABYAAAJF
    hI+py+0PoyIhfEyNOJJ/BB8DERGBxISPqctLCeFjqoWR/CP4GIiICCQmfExd
    XooJH1PjSP4RfITIzKSE8DF1uf1hlIgUADs= 
    }
    set btn(decindent) {
    R0lGODlhFwAWAJEAANnZ2QAAAAAAgP///yH5BAEAAAAALAAAAAAXABYAAAJG
    hI+py+0PoyLxEQk+pi5SfESCj6mLQjAo/hF8E0GKfwQfSDbBx0QRpPhH8E+Q
    4h/Bx9RFio9I8DF1keIjEnxMXW5/GKUkBQA7  
    }
    set btn(incindent) {
    R0lGODlhFwAWAJEAANnZ2QAAAAAAgP///yH5BAEAAAAALAAAAAAXABYAAAJG
    hI+py+0PoyLxEQk+pi5SfESCj6mLQjAo/hH8EzmKfwQfSDbBx0yRo/hH8E+Q
    4h/Bx9RFio9I8DF1keIjEnxMXW5/GKUkBQA7
    }
    
    ## -- Editing
    set btn(copy) {
    R0lGODlhFwAWAJEAANnZ2U1NTf///01NpiH5BAEAAAAALAAAAAAXABYAAAJp
    hI+py+0PIyLxCT6mhQhJCYKPCSFCUiQIPmZIBCmcT/ARgmTPyQzBx5CIoEwi
    MjIE/4Jkj0TI0DaCbyERQRnlyxB8C5I9QrFlCL5R/CH5MgQfcYRiyxB8xCH5
    MgQfkeYfwcfU5faHUU5SADs=
    }
    set btn(cut) {
    R0lGODlhFwAWAJEAANnZ2U1NTU1Npv///yH5BAEAAAAALAAAAAAXABYAAAJY
    hI+py+0PYyMBIAg+5sUFwce8uCD4mBcBlBA+JgYEwcfMiCD4mBqqekQRwcdM
    ASGJ8DFBRAgCCMFHFBMgAULwEcUESIAQfEQxMZIIHxNECD6mLrc/jBKQAgA7
    }
    set btn(delete) {
    R0lGODlhGAAWANUAANnZ2dQNN9ZkgtsUPN8BKtVgfud9ldVTc9EoTtE2WuzH
    1twONtdFZ9QPON4BKt2Zr9crT9hBY+rZ5tcHMNwBKuGtwNU1WdM8X9oDLdoC
    LObH19EnTefQ39ROcNwCK9kDLeiOpevi7dAhSNQLNd8CK+iIn+fJ2dMMNtgF
    LtMQOd6csdkELdUKM9MTPOmUqueAmNRffdEYQN8DLO3q9NJJa+IzVf//////
    /////////////////////////////////yH5BAEAAAAALAAAAAAYABYAAAaE
    QIBwSCwaj8ikcslsLgMCJ3JAKDiPBgLhYEQkmooFgUFsOB5OAIQQAUgmlIpz
    aCFcMBmNs7ghZIAcIEA4JBaJHc+HADIaiyFRZhQCkUpG49B0QqVCQhCpZDQC
    VCtWK0R0kV5GI6wRCxldspexOKPNjEIgVw0IEA6JReMRmVQumU3nsxgEADs=  
    }
    set btn(paste) {
    R0lGODlhFQAWAKIAANnZ2U1NTf//TaamTaamptPT001Npv///yH5BAEAAAAA
    LAAAAAAVABYAAAOKCLrc/jDKSStAoYGgy4uhKrKKQFdjkCYkAiVoKBB0IQgp
    sHSDBgNBF2KQMHSVCANBF4JwaGgIh4YmEHQhBmlo0HQtEHQhCIeGDE/VDEEX
    YpCGxvBU7QxBN2iQhuwMzc7M0BBUYwiHxvB01RBUgwZpyA5N9wxBF0PV8HTV
    EHQZ0HQZQZfbH0Y5KUQJADs=
    }
    set btn(undo) {
    R0lGODlhFwAWAJEAANnZ2U1Npqampv///yH5BAEAAAAALAAAAAAXABYAAAI+
    hI+py+0Po5y0WkDiCMFHi4sgGCF3Z5QgCD4E4gUlhG+BeEFJCD4E4lFsgh1y
    d3dniEL4mLrc/jDKSau9gBQAOw==  
    }
    set btn(redo) {
    R0lGODlhFwAWAJEAANnZ2aampk1Npv///yH5BAEAAAAALAAAAAAXABYAAAI9
    hI+py+0Po5y0ThJJhOBjWgiSBAAh+AiSJgSJ4CNInpBE+AiSJiSJ4COEJMkm
    +GgSBB9Tl9sfRjlptZeRAgA7  
    }
    
    ## -- Tools
    set btn(checkspelling) {
    R0lGODlhFwAWAJEAANnZ2U1NTU1Npv///yH5BAEAAAAALAAAAAAXABYAAAJh
    hI+py+0PoyIBIgAgKISPFhBAQSAIPmJEACWAAAAgEf4FBAQhCAAQgo8BAUEB
    IEIIPqaKqh4R4WOqqB6JsOXu7kgGgBF8TBARgkTwMU9ESISPmSJC8DFVCD6m
    Lrc/jBKQAgA7  
    }
    set btn(find) {
    R0lGODlhFwAWAKIAANnZ2QAAAOzp2Kyomf///wD//wAAgP///yH5BAEAAAAA
    LAAAAAAXABYAAAOCCLrc/jDKKVFoIOhyK0aIiGAEgi5z4BCRCMoEgi5vIFGV
    CIoEgi5roFCh6Eog6DIEChWKrgSCLkOg6HIg6DIEii4Hgi5roOgKBYIub+Cg
    qNIEgi5zYIgIDZoi6HIHRoSJoRmCLjejiZkh6HIzmpgZgi43o4kZgi53oxmC
    Lrc/jHJGlAA7  
    }
    set btn(replace) {
    R0lGODlhFwAWAKIAANnZ2YAAAAAAAKyomezp2P///wD//wAAgCH5BAEAAAAA
    LAAAAAAXABYAAAN9CLrc/jDKSR0KBF1uBxSBoMvNGKoIutyKARCBoMu9GACB
    oMsqKCIAgIGgyyw4RCMIuhq8SKaKCEcAADAAAkEXhLCMiARBIzQQdEGQVEsA
    AUMVQVdlkLRGAAEiEHQZBIdq5BAUYncXEpW7Q9DldjxF0OV2vEPQ5faHUU5a
    J0oAOw==  
    }
    set btn(paint) {
    R0lGODlhFwAWALMAANnZ2YCAgAAAAP///wCAgMDAwP8AAIAAAAD//wAAgICA
    AAAA/4AAgP//AP///////yH5BAEAAAAALAAAAAAXABYAAAS3EMhJq704671D
    gCIEAYGcVAYxgxBBCAikkFLKEIQMQ0BSBAAiCAjkBGEEAcwRBMEE1BACAjll
    KEuYIwhKUJUgIJCTJrLYESSoBpWAQE4qE2FHENUUFBDISSUMctIgIJCTgjDG
    GKWUAkMQEMhJZSgwyCkEBHLSGcYoJYQQoIBATjrDGKWEEAIUEMhJZxhjlBJC
    gAICOekMY4xSQghQQCAnnWGMUUoIAQoI5KQUCjklBHLSai/Oeu8IADs=
    }
    set btn(help) {
    R0lGODlhGAAWAPcAANnZ2djh9NHc8dDb79Hb79Hc79Db8dPc6+Pn7+Tr+NPc
    8eTo9f38/f////37/ODm89Ld8c3S3Ovv+OLq99bf8Pv//8bz/6jd/pjP/JHJ
    /6nU//j+/////NTf8bq/x+nt9tTe8c7p/3Gy/4ev7N/i7PTw6+Dk8Xmk9k6M
    /sre/////dLd8L3Ax+3x+tfg8+/x9tvq/0qK/12H3v//7v38/qG55///+v75
    8z1z4DZx5u7z/+Dn9JOZpdLW3WuY9Stn4m6LyOPq8FJ/1wExvX+c12aM2hVU
    0Xea4v79/r/I26Gjqdfg8ebt+zNr2zRp0ylfyiJZyRpVyRFMwcbQ4kBx0CFb
    zTlrzvv7/NPe72Vnatnh8srY8yNczTNnzzBlzCxhzCBWxKi1z3uc4BtUxyxh
    zSpfxu3x9dXf8E5PUtjh89be8h9XxTNnzjJmzCJbzGqGwpGt6BBLxC5jzClg
    yyxfw/Hy89bg8kpLTvv6+DNjxSddyjRoziRcy2qJyt/n9zls0CRbyjJmzRxU
    w1F5x/38+MXP4UxNUNPc8Pn6+pCo2A5JvS9jzSphzTtsy2mO2CpfyipeyRFI
    ub3J4Ojt9pCXpFxeYdrg6tfh8v//+Fl+xw1GuxRPxpWkwnCS2RBLwQxGuIGa
    zlJUV5CSmOzw+cfP3+Lq9f//9IGazxZNtz1luZqv2SRZwCVXuZ+v1f//99ni
    9GtweGxucs/T2+Hl7bW9ztjj9OLi5KKxz5ar1LvG3fDx7PLz9tbh8np/iK+y
    uNjb46Glr8bP49fh89nj9Nrj9Nji87C6yVRWWmlqbqeqsOLm7t/j67W4v2Zn
    a0tMT0hJTHV3e8THz+fq8///////////////////////////////////////
    ////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////
    /////////////////////yH5BAEAAAAALAAAAAAYABYAAAj/AAEIHEiwoMGD
    CBMqXMhQYAABAwgUMHAAQUMACRQsYNCgQQMHDyBEkLBwAoUGFSxcwJBBwwYO
    HTx8UAiiQQgRI0iUMHECRQoVK1i0QOjiBYwYMmbQqGHjBo4cOnbw6IEQRAMf
    P4AEETKESIMiRo4gSaIE4RImTZw8gRJFypQGVKpYuYIlC0ItW7h08fIFTJgG
    YsaQKWPmDBqEadSsYdPGzZsGcOLImUOnjp07CF3gyaNnD58+fv4ACiRoEKFC
    hhAeQpRI0SJGjRw9kgMpkqRJlCohtHQJUyZNmzg16OTpEyhMdkKJQjiKVClT
    p1ClUrWKVStXr2DFkpVwFq1aeGzdOMKVS9cuXr2y+Pqg8BewYMKGESsmzNgx
    ZMmUMVzGrJkzgM8EooEWTRpAAAIHEixo8CDChAoXMgwIADs=
    }
    set btn(zoomin) {
    R0lGODlhGAAWAOYAANnZ2dvj9ay33Zqr2put3Z6u2q2439bd8c3W756t2rvO
    6sXp+b7w/rjl+aPE65yt3MzV7pqr2d7q9v///8nr/7jo/6/i/77x/6za+pqq
    2brE56fB5OX9/9ju/5HR9C10xcf5/8Dy/8b4/6C74q653qSz37nk9r7r/6nd
    9L/u+s7//77n+Z2u3r/x/q7h/wFnzNr//9H9/qa14LLT8bns/5vN8p7Q8c3c
    6uf//8To9J+v277J6Zqt3cbz/7bo/7fm8vj///7///3//6K32bjF5fDu8NnV
    1pyq0pm15MDw/9H//9j//+D//+7//9Dd7KKx2/zEc/ipT5eUoKGv2Jqt2J+9
    4L3d7b7b7aa226Sx3NHZ8PqlRcF1TKSAh9PZ78DM66Wz3Z2v3qe24KR+hbu4
    1uDb1fCiSNTX3oBxaKB7gby20ubp8dPV4f//////////////////////////
    /////////////////////////////////////////////////yH5BAEAAAAA
    LAAAAAAYABYAAAfQgACCg4SFhoeIiYqLjIUBAgMEBQYHjYMICQoLDA0ODxCN
    AAEREhMUFRYXGBkBjRobHB0eHyAhIiMkjSUmJx4eHygpKisDjSwtLi8fHx8f
    MDEEjTIzNDU2Hzc3ODk6jTs8PT4/H0BBQkNEjEVGR0hJSktMTU5PAYtFUFFS
    U1RVVoBXWFlagACCg4QARVBbXF1eX2BhYl8BhYUARVBbXGNkhYWFgkVQW1xj
    ZIWFhYNlZlxjZIWFhYRnaGlqhYWFhQBrbIAAgoOEhYaHiImKi4yLgQA7  
    }
    set btn(zoomout) {
    R0lGODlhGAAWAOYAANnZ2dvj9b3I6KOy3Zut3Zqr2pqp1dHZ8Ke13qCv17LT
    68Hy/rjg9p64456u28zV7qi237/M5f///+D3/7fn/7Pl/77x/6jU9Zyr2sDM
    66e64e/9//b8/8Tm/7zu/7Di/7Hk/8b2/5265Km02qe34LLO7Mr1/7Hd+c3+
    /7zn+Z2v3rLY8rTn/4Gz5gFnzN3//838/qe24LDO7rvu/8LR4+j//8nw+aCz
    38Xx/7Tm/8///9////L///3///r//7bR6qGv2qPG8Mf1/8n7/9r//+b///H/
    /+7y+p+v29Xb8PXauvG2ZKSntZup1Jmz37vi+dD7/rjT68LK5v+3UuOKP6Bz
    asjN37nF5p6t27C64axyZaSSsN/i86SRrejg2/+4U9TX3qCAYK1yY8PFy7m2
    w///////////////////////////////////////////////////////////
    /////////////////////////////////////////////////yH5BAEAAAAA
    LAAAAAAYABYAAAfZgACCg4SFhoeIiYqLjIUBAgMEBQYBjYMHCAkKCwwNDg+N
    AAEQERITFBUWFxgBjRkaGxwdHh8gISIjjSQlJieAKIKCKQWAAIKDhIMqKywt
    Li4uLi8wBIWFgzEyM4A0goI1NgWAAIKDhIMZNzg5Ojs8PT4/I4WFgwFAQUJD
    REVGR0hJhYWCSktMTU5PUDZRSFKFhYJKU1RVVldYBAVZAYWFgkpTVFpbXIWF
    hYJKU1RaXVyFhYWCXl9UWl1chYWFg2BhYl1chYWFhWNkXIAAgoOEhYaHiImK
    i4yKgQA7  
    }
    
    ## -- Objects
    set btn(clipart) {
    R0lGODlhFwAWALMAANnZ2QAAAICAgP///4AAAIAAgMDAwP//AICAAP//////
    /////////////////////yH5BAEAAAAALAAAAAAXABYAAATMEMhJq704622D
    gEEEEUQQQQQRIJBTCjjkpFNAIKcMY4whCCEEijFGgEBOKcYYAxI5xxgCAjll
    GGOMIkohkIgxAgRySjHGGMGEQyARYwgI5JRhjIHKKUNAIsYIEMgpxRhilDCO
    gKKMISCQU4YxRghFjADLGCNAIKcUY4yBwjABDjkEBHLKMMYYYhwU4JAjQCCn
    FGOMIQoSAg45BARyyjCGECOEUmAIYwQI5JRiiDJGKafAIsQQEMgpgwgiiCCC
    gEEEESCQk1Z7cdabdwsjADs=  
    }
    set btn(table) {
    R0lGODlhFwAWAKIAANnZ2QAAAAAAgICAgP///////////////yH5BAEAAAAA
    LAAAAAAXABYAAAN8CLrc/jDKSStDocsdCLoQKLrMgaALgaLLHAi6EDi6zIGg
    C0FIQ0SDpKMUCLoQhDRENEg6SoGgC4GjyxwIuhCENEQ0SDpKgaALQUhDRIOk
    oxQIuhA4usyBoAtBSENEg6SjFAi6EIQ0RDRIOkqBoAsYutyBoMvtD6OctFqL
    EgA7
    }
    set btn(picture) {
    R0lGODlhFwAWAKIAANnZ2QAAAP//AP///4CAgMDAwP///////yH5BAEAAAAA
    LAAAAAAXABYAAAOACLrc/jDKSau9KHRZEXQZAmVkZFBGJhB0GQJHRkaQgiQQ
    dBkCZWRkMComEHQZAkdGRpCCJBB0GQJlZGRQRiYQdBkCRyZGcGQkEHQZAmWC
    YjBCJhB0GQIniCiwKiQQdBkCgyiIMKoiEHQZAkkpNLAqEHQZMHRZEXS5/WGU
    k1Z7AUoAOw==  
    }
    
    ## -- VCR controls
    set btn(vcrback) {
    R0lGODlhFQAVAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAVABUAAAI6
    hI+py+0Po3wkhI+JEapKYsLHtIigED4mUGyCjwkUm+BjWkRQCB/zIoLgY2KE
    qhoSwsfU5faHUc5JCgA7  
    }
    set btn(vcrgoend) {
    R0lGODlhFgAWAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAWABYAAAJY
    hI+py+0Po5wUkAAAAQCEhOBjxEUQnDAzM4oBFgBG8DEiAigOGMFHio9G8JHi
    oxF8jIgAigNG8DEiLCgBRBB8jLgIghNmZkYBAAIACAnBx9Tl9odRTgpIAQA7  
    }
    set btn(vcrff) {
    R0lGODlhFgAWAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAWABYAAAJK
    hI+py+0Po5zUkAAAQfAxIeIoIXzMiDCKCR8xIoKQRBB8PIqPBB+N4iPBR4uI
    ICQRBB8vIoxiwkeMuKAQPmZgEHxMXW5/GOWkihQAOw==  
    }
    set btn(vcrforward) {
    R0lGODlhFQAVAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAVABUAAAI7
    hI+py+0Po5yThPAxM0JVAYkJH/MigkL4mEaxCT4mUGyCjwkRQSF8TIsIgo95
    EaoKSAgfU5fbH0b5SAEAOw==  
    }
    set btn(vcrpause) {
    R0lGODlhEgASAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAASABIAAAJB
    hI+py+0PSQIwgo8WYUEhfLQICwrho0VYUAgfLcKCQvhoERYUwkeLsKAQPlqE
    BYXw0SIsKISPFmFBIXxMXW5/SAoAOw==  
    }
    set btn(vcrrewind) {
    R0lGODlhFgAWAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAWABYAAAJK
    hI+py+0Po5xUkQAAQfAxIy4ohI8YEUYx4eNFRBCSCIKPRvGR4KNRfCT4eBER
    hCSC4CNGhFFM+JgRFxTCx4TAIPiYutz+MMpJDSkAOw==  
    }
    set btn(vcrgostart) {
    R0lGODlhFgAWAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAWABYAAAJa
    hI+py+0Po5wUkAQAEAAEhOBjxEUQnDAzM0oAFgAWBB8jIIISREQQfKT4aAQf
    KT4awccIiKAEEREEHyMsggJEBMHHiIsgOGFmZpQAAAKAgBB8TF1ufxjlpIAU
    ADs=  
    }
    set btn(vcrstopcircle) {
    R0lGODlhEgASAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAASABIAAAI0
    hI+py+0PAYlN8NEofhB8pPhH8I/iH8E/in8E/yj+Efyj+EfwkeIHwUej2AQf
    U5fbH8ZECgA7  
    }
    set btn(vcrstopsquare) {
    R0lGODlhFQAVAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAVABUAAAI7
    hI+py+0Po5yQxD+Cj0bxj+CjUfwj+GgU/wg+GsU/go9G8Y/go1H8I/hoFP8I
    PhrFP4KPqcvtD6OckBQAOw==  
    }
    
    ## -- Directions
    set btn(down) {
    R0lGODlhFAASAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAUABIAAAI/
    hI+py+1M4uMRfKP4GAT/KD4GwUeKfwQfgeIfwUej+EHwESk2wcek2AQfEyKC
    4GNaRBB8zAtVBRLCx9Tl9kekADs=  
    }
    set btn(downleft) {
    R0lGODlhFQATAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAVABMAAAJD
    hI+py+1vSAgfMyNUFZCY8DEvIiiEj2kUm+BjAsUn+JgUPwg+IsU3go9H8Y/g
    o1F8JPgIFB+D4CPFRyP4mLrc/tCRAgA7  
    }
    set btn(downright) {
    R0lGODlhFQATAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAVABMAAAJB
    hI+py+0PSQgfEyNUlcSEj2kRQSF8TKDYBB+T4hN8RIofBB+P4hvBR6P4R/AR
    KD4SfKT4GAT/KD4awcfU5faHjxQAOw==  
    }
    set btn(left) {
    R0lGODlhEgAUAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAASABQAAAI/
    hI+py+1vSAgfMyIyj8Qm+GgUn+AjxTeCbxQfCT7FRyP4RvGR4CPFN4KPRvEJ
    Ph7FJviYEZGZhoTwMXW5/RspADs=  
    }
    set btn(right) {
    R0lGODlhEgAUAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAASABQAAAJA
    hI+py+1GQviYFhEEH5NiE3w8ik/w0Si+EXyk+EjwjeKjEXyKjwTfKL4RfKT4
    BB+NYhN8vIjITBLCx9Tl9oeMFAA7  
    }
    set btn(up) {
    R0lGODlhFAASAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAUABIAAAI+
    hI+py+1/SAgfE0NVKSZ8TIsIgo8JFJvgY1Jsgo9I8YPgo1H8I/gIFP8IPlJ8
    DIJ/FB+D4BvFxyP4mLrc3qQAOw==  
    }
    set btn(upleft) {
    R0lGODlhFQATAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAVABMAAAJC
    hI+py+0PH4mPRvCP4mMQfKT4SPARKP4RfDSKbwQfj+IHwUek+AQfk2ITfEyI
    CArhY1pEEHzMi1BVQEL4mLrc/pAUADs=  
    }
    set btn(upright) {
    R0lGODlhFQATAJEAANnZ2QAAAP///////yH5BAEAAAAALAAAAAAVABMAAAJD
    hI+py+0PHYmPRvCR4mMQfASKjwQfjeIfwcej+EbwESl+EHxMik/wMYFiE3xM
    iwgK4WNeRBB8TIxQVUNC+Ji63P6GFAA7  
    }

#-----------------------------------------------------------------------
# Tooltip type
#
# The tooltip command is an instance of TooltipType, so that we can
# have options.
#
# Code posted by William Duquette to the Tcler's Wiki on page
#   "Snit Tooltips"

snit::type TooltipType {
    #-------------------------------------------------------------------
    # Options

    option -font {Helvetica 8}
    option -background "#FFFFC0"
    option -topbackground black
    option -foreground black
    option -delay 600

    #-------------------------------------------------------------------
    # Variables

    # Tool tip text.  An array, indexed by window name
    variable tiptext

    # Tool tip timeout, or {}
    variable timeout {}

    # Tool tip window, or {}
    variable tipwin {}

    #-------------------------------------------------------------------
    # Constructor

    # Implicit

    #-------------------------------------------------------------------
    # Public methods

    method register {window text} {
        set tiptext($window) $text

        # Add "+" so other actions bound to these events will fire
        bind $window <Enter> "+[mymethod Enter $window]"
        bind $window <Leave> "+[mymethod Leave $window]"
    }

    method unregister {window} {
        unset tiptext($window)
    }

    #-------------------------------------------------------------------
    # Private Methods

    # When the mouse pointer enters the window, set the timer.
    method Enter {window} {
        set timeout [after $options(-delay) [mymethod Popup $window]]
    }

    # Pop up the tooltip.
    method Popup {window} {
        # FIRST, the timeout has fired, so we can forget it.
        set timeout {}

        # NEXT, the tooltip will be a child of the window's toplevel.
        set top [winfo toplevel $window]

        # NEXT, the tooltip's name depends on which toplevel it is.
        set tipwin ".gui_tooltip_window"

        if {$top ne "."} {
            set tipwin "$top$tipwin"
        }

        # NEXT, create the tooltip window.
        frame $tipwin \
            -background $options(-topbackground)

        label $tipwin.label \
            -text $tiptext($window) \
            -foreground $options(-foreground) \
            -background $options(-background) \
            -font $options(-font)

        # Pack the label with a 1 pixel gap, so that there's a box
        # around it.
        pack $tipwin.label -padx 1 -pady 1

        # NEXT, the tipwin will be placed in the toplevel relative to
        # the position of the registered window.  We'll figure this out
        # by getting the position of both relative to the root window.

        set tx [winfo rootx $top]
        set ty [winfo rooty $top]

        set wx [winfo rootx $window]
        set wy [winfo rooty $window]

        # We want to the tip to appear below and to the right of the
        # registered window.
        set offset [expr {[winfo width $window]/2}]

        # Compute the final position.
        set x [expr {($wx - $tx) + $offset}]
        set y [expr {($wy - $ty) + [winfo height $window] + 2}]

        # Finally, place the tipwin in its position.
        place $tipwin -anchor nw -x $x -y $y

        # However, if window is to the right of its toplevel, the
        # tipwin might be too wide.  Slide it to the left, as needed.
        # TBD: I don't know of any way to determine the width of the
        # tipwin without letting it pop up, which causes an ugly
        # jump.
        update idletasks

        set rightEdge [expr {$x + [winfo width $tipwin]}]

        set topWid [winfo width $top]

        if {$rightEdge >= $topWid} {
            set x [expr {$x - ($rightEdge - $topWid + 2)}]

            place $tipwin -anchor nw -x $x -y $y
        }
    }

    # When the mouse pointer leaves the window, cancel the timer or
    # popdown the window, as needed.
    method Leave {window} {
        if {$timeout ne ""} {
            after cancel $timeout
            set timeout ""
            return
        }

        if {$tipwin ne ""} {
            destroy $tipwin
            set tipwin ""
        }
    }
}

#-----------------------------------------------------------------------
# The tooltip command

TooltipType tooltip

}