Updated 2012-03-08 22:42:41 by AK

Snitwiz is a wizard package written using the snit object system. I began using the tkWizard package but found the interface a little awkward for what I wanted. Snitwiz doesn't rely on variable traces to decide where to go next and reconfiguring steps on the fly is easy.

Share and enjoy...Steve Cassidy
 # snitwiz.tcl --
 #
 #      This file implements package snitwiz, which is a wizard
 #       building package built with snit
 #
 # Copyright (c) 2004 Steve Cassidy
 # 
 #

 package require snit
 package require msgcat

 if {[package present msgcat] != "package msgcat not present"} {namespace import msgcat::mc}

 # load message catalogs
 ::msgcat::mcload [file join [file dirname [info script]] .. msgs]

 package provide snitwiz 0.1 


 namespace eval snitwiz {;
 #namespace export -clear *

     # create a default image
     image create photo snitwiz::feather -data {
        R0lGODlhIAAgALMAANnZ2QAAwAAA/wBAwAAAAICAgAAAgGBggKCgpMDAwP//
        /////////////////////yH5BAEAAAAALAAAAAAgACAAAAT/EMhJq60hhHDv
        pVCQYohAIJBzFgpKoSAEAYcUIRAI5JSFlkJBCGLAMYYIIRAI5ASFFiqDgENK
        EUIwBAI5ywRlyhAEHFKKEEIgEMgJyiwUBAGHnCKEEAyBQM4yy5RhCDikFDBI
        SSCQExRKwxBDjAGHgEFKQyCQk9YgxBBjDAGDnAQCOWkNQgwxxDgwyGkIBHJS
        GoQQYohRYJDTEAjkpDWIIYQQBQY5A4FATlqDEEIMgWCQMxgCgZy0BiikRDDI
        GQyBQE5aAxRSIhjkNIRAICetAQop04BBTgOBnLTKIIQQacAgZzAQyEkrCEII
        kQYMckoDgZy0giCESAMGOaWBQMoydeeUQYhUYJBTGgikLHNOGYRACQY5pYFA
        yjLnnEGgNGCQMxgAACgFAjnpFEUNGOQ0BgI5Z6FUFlVgkJNAICctlMqiyggB
        BkMIBHLOUiidSUEiJwRyzlIopbJQSilFURJUIJCTVntlKhhjCwsEctJqr0wF
        Y0xhBAA7
     }


 }; # end of namespace snitwiz


 snit::widget wizard {

     hulltype toplevel 

     variable buttonFrame
     variable layoutFrame
     variable sep
     variable helpButton
     variable backButton
     variable nextButton
     variable cancelButton
     variable finishButton

     variable currentstep
     variable disabledSteps {}; # a list of steps that won't be shown


     option -title {Wizard}
     option -showhelp 1

     option -steps {}  ;# the order of steps

     # when we get new steps we make sure each knows it's parent
     # and set the current step to the first    
     onconfigure -steps {value} {
         set options(-steps) $value
             foreach s $value {
                 $s configure -parent $self
             }
             set currentstep 0
     }

     constructor {args} {

         # The dialog is composed of two areas: the row of buttons and the
         # area with the dynamic content. To make it look the way we want it to
         # we'll use another frame for a visual separator
         install buttonFrame using frame $win.buttonFrame -bd 0
         install layoutFrame using frame $win.layoutFrame -bd 0\
             -height 200 \
             -width 300 
         install sep using frame $win.sep1 -class WizSeparator\
             -height 2 -bd 2 -relief groove

         pack $layoutFrame -side top -expand 1 -fill both
         pack $sep -side top -expand 0 -fill x
         pack $buttonFrame -side top -expand 0 -fill x

         # make all of the buttons
         install helpButton using button $buttonFrame.helpButton \
             -text [mc "Help"] \
             -default normal \
             -bd 1 \
             -relief raised \
             -command [mymethod help]

         install backButton  using button $buttonFrame.backButton \
             -text "< [mc Back]" \
             -default normal \
             -width 8 \
             -bd 1 \
             -relief raised \
             -command [mymethod back]

         install nextButton  using button $buttonFrame.nextButton \
             -text "[mc Next] >" \
             -default normal \
             -width 8 \
             -bd 1 \
             -relief raised \
             -command [mymethod next]

         install finishButton using button $buttonFrame.finishButton \
             -text [mc Finish] \
             -default normal \
             -width 8 \
             -bd 1 \
             -relief raised \
             -command [mymethod finish]

         install cancelButton using button $buttonFrame.cancelButton \
             -text [mc Cancel]   \
             -default normal \
             -width 8 \
             -bd 1  \
             -relief raised \
             -command [mymethod cancel]

         # pack the buttons
         if {[$self cget -showhelp]} {
             pack $helpButton -side left -padx 4 -pady 8
         }
         pack $cancelButton -side right -padx 4 -pady 8
         pack $finishButton -side right -pady 8 -padx 4
         pack $nextButton -side right -pady 8
         pack $backButton -side right -pady 8

         wm title $self [$self cget -title]
         wm minsize $self 550 430

         $self configurelist $args      
     }


     method layoutFrame {} {
         return $layoutFrame
     }

     ## methods to handle the button commands
     method help {} {
         event generate $self <<WizHelp>>
     }

     method next {} {
         event generate $self <<WizNext>>
         update idletasks

         # go to the next non-disabled step
         foreach step [lrange [$self cget -steps] [expr $currentstep+1] end] {
             incr currentstep
             if {[lsearch $disabledSteps $step] < 0} {
                 $self buildStep $step
                 return
             }
         }
     }

     method back {} {
         event generate $self <<WizBack>>
         update idletasks

         ## go to the previous non-disabled step
         for {incr currentstep -1} {$currentstep >= 0} {incr currentstep -1} {
             set step [lindex [$self cget -steps] $currentstep]
             if {[lsearch $disabledSteps $step] < 0} {
                 $self buildStep $step
                 return
             }
         }
     }

     method finish {} {
         event generate $self <<WizFinish>>
     }

     method cancel {} {
         exit
     }

     method buttonstate {name state} {

         switch $name {
             next {$nextButton configure -state $state}
             back {$backButton configure -state $state}
             finish {$finishButton configure -state $state}
             cancel {$cancelButton configure -state $state}
             help {$helpButton configure -state $state}
         }
     }

     ## defining and rendering steps 
     ## a step is an object of type wizardstep
     ## it is rendered by calling it's 'render' method
     ## 

     method buildStep {step} {
         # destroy the existing layout
         eval destroy [winfo children $layoutFrame]

         # run the step rendering method
         $step render $layoutFrame $self

         # enable/disable buttons as required
         if {$currentstep >= [llength [$self cget -steps]]-1} {
             # disable next
             $self buttonstate next disabled
         } else {
             $self buttonstate next normal
         }
         if {$currentstep == 0} {
             $self buttonstate back disabled
         } else {
             $self buttonstate back normal
         }
     }

     ##  start --
     #
     # set the wizard going with the first step
     #
     method start {} {
        set currentstep 0
        $self buildStep [lindex $options(-steps) 0]
     }

     ## enable -- 
     #  
     # enable a step, if previously disabled
     method enable {step} {

         set pos [lsearch $disabledSteps $step]
         if {$pos >= 0} {
             set disabledSteps [lreplace $disabledSteps $pos $pos]
         }
     }

     ## disable -- 
     #  
     # disable a step so that it won't appear in the sequence
     #   
     method disable {step} {

         set pos [lsearch $disabledSteps $step]
         if {$pos < 0} {
             lappend disabledSteps $step
         }
     }
 }

 snit::widget wizardLayout-basic {
     option -icon snitwiz::feather
     option -title ""
     option -subtitle ""
     option -pretext ""
     option -posttext ""

     variable titleframe
     variable title
     variable subtitle
     variable icon
     variable sep
     variable pretext
     variable posttext
     variable layoutFrame

     onconfigure -icon {value} {
         $icon configure -image $value
         set options(-icon) $value
     }

     onconfigure -title {value} {
         $title configure -text $value
         set options(-title) $value
     }

     onconfigure -subtitle {value} {
         $subtitle configure -text $value
         set options(-subtitle) $value
     }

     onconfigure -pretext {value} {
         $pretext configure -text $value
         set options(-pretext) $value
     }

     onconfigure -posttext {value} {
         $posttext configure -text $value
         set options(-posttext) $value
     }

     constructor {args} {

         install titleframe using frame $win.tf -bd 4 -relief flat \
             -background white
         install title using label $titleframe.t\
             -background white -width 40\
             -text [$self cget -title]\
             -anchor w -justify left
         install subtitle using label $titleframe.st\
             -height 2\
             -background white\
             -padx 15\
             -width 40 \
             -text [$self cget -subtitle]\
             -anchor w -justify left
         install icon using label $titleframe.icon\
             -borderwidth 0 \
             -image $options(-icon) \
             -background white \
             -anchor c
         install sep using frame $win.sep -class WizSeparator\
             -height 2 -bd 2 -relief groove

         set labelfont [font actual [$title cget -font]]
         $title configure -font [concat $labelfont -weight bold]

         install pretext using label $win.pretext -width 40\
             -text [$self cget -pretext]\
             -anchor w -justify left

         install layoutFrame using frame $win.layout -bd 0 -height 200 
         install posttext using label $win.posttext -width 40\
             -text [$self cget -posttext]\
             -anchor w -justify left

         grid $title $icon -sticky ew
         grid $subtitle ^ -sticky ew

         grid $titleframe -sticky new
         grid $sep -sticky new
         grid $pretext -sticky ew
         grid $layoutFrame -sticky nsew
         grid $posttext -sticky ew

         grid columnconfigure $win 0 -weight 1

         grid rowconfigure $win 0 -weight 0   ;# titleframe
         grid rowconfigure $win 1 -weight 0   ;# sep
         grid rowconfigure $win 2 -weight 0   ;# pretext
         grid rowconfigure $win 3 -weight 1   ;# layoutFrame
         grid rowconfigure $win 4 -weight 0   ;# posttext

         $self configurelist $args
     }

     method layoutFrame {} {
         return $layoutFrame
     }
 }


 snit::type wizardstep {

     option -title {}
     option -subtitle {}
     option -icon snitwiz::feather
     option -pretext {}
     option -posttext {}
     option -body {}
     option -parent {}
     option -layout basic

     ## render method calls the body code to generate the frame
     method render {frame wizard} {
         # first make the layout
         set layout [wizardLayout-$options(-layout) $frame.wiz\
                         -title $options(-title)\
                         -subtitle $options(-subtitle)\
                         -icon $options(-icon)\
                         -pretext $options(-pretext)\
                         -posttext $options(-posttext)]

         pack $layout -side top -fill both -expand 1

         ## setup the special variable win as the container frame for
         ## the content note that we also have $wizard as the name of
         ## the containing wizard
         set win [$layout layoutFrame]

         # now render the step itself
         eval [$self cget -body]
     }
 }



 # require_all_keys --
 #
 #   Disable the Next button in a wizard unless all
 #   keys in the given array have values
 #
 # Arguments:
 #   wiz    -- a wizard widget
 #   arrayvar  -- array name
 #   keys   -- list of keys to check 
 # Results:
 #   Puts a variable trace on the given 
 #   array variables which will enable the next button
 #   only if all keys have non-null values.
 #
 proc snitwiz::require_all_keys {wiz arrayvar keys} {
     upvar $arrayvar array

     foreach k $keys {
         trace add variable array($k) write \
             [list snitwiz::require_all_keys_tracefn $wiz $keys]
     }
 }


 proc snitwiz::require_all_keys_cleanup {wiz arrayvar keys} {
     upvar $arrayvar array

     foreach k $keys {
         trace remove variable array($k) write \
             [list snitwiz::require_all_keys_tracefn $wiz $keys]
     }
 }




 # helper proc used by require_all_keys
 proc snitwiz::require_all_keys_tracefn {wiz keys arrayvar key op} {
     upvar $arrayvar array

     foreach k $keys {
         if {$array($k) eq ""}  {
             $wiz buttonstate next disable
             return
         }
     }
     # reset to normal only if all ok
     $wiz buttonstate next normal
 }

And here's an example of how to use it, by way of documentation (documentation -- ha ha ha....)
 source snitwiz.tcl
 package require snitwiz

 wm withdraw .


 wizardstep step1\
     -title "The first step in the process"\
     -subtitle "Or what you will..."\
     -pretext "Please fill in the following form"\
     -posttext "Thank you for doing that"\
     -body {
         set can [canvas $win.canvas -background red]

         pack $can -fill both -expand 1
         .w buttonstate finish disabled
     }

 wizardstep step2  -title "Step2" -layout basic -body {

     set can [canvas $win.canvas -background blue]

     pack $can -fill both -expand 1
     .w buttonstate finish normal
 }

 wizardstep step3  -title "Final Step" -layout basic -body {

     set lab [label $win.l -text "Enter Your name"]
     set ent [entry $win.e]

     grid $lab $ent
     .w enable step2
 }

 wizard .w -steps {step1 step2 step3}

 .w disable step2
 .w start

See also Snit.