[Arjen Markus] (20 february 2008) A question from a colleague inspired me to write the program below and the associated example. The question was related to making an easy-to-use system for analysing problems that may require either a simple computation in, say, a spreadsheet, or a much more sophisticated approach using advanced numerical modelling. The client wants an answer fast, so the system would ask a bunch of questions to decide what solution method is appropriate - a decision tree, so to speak. Simple cases can be dealt with via simple computations, for more complex cases the client is advised to follow the sophisticated path. Anyhow, such a system in this day and age requires a graphical user-interface and quite likely a web-oriented user-interface. While I did not do the latter bit yet, the first bit was easy enough. For the statistics: * Requirements analysis and design: slightly less than 1 hour (using pen and paper) * Coding the first version: approximately 1 hour (admitted: little attention paid to beautifying the GUI layout) * Testing and debugging via a simple example: again 1 hour Note that the program uses some more or less subtle namespace manipulations to keep the program's variables away from the user-defined ones. ---- The use is simple: * Implement the decision tree and the associated computations in a way as shown by the example * Run the general program with the file containing that decision tree as an argument The program itself looks like this: ====== # decision.tcl -- # Program to define and show GUIs for decision trees # # Decide -- # Namespace for running the GUI and associated computations # namespace eval ::Decide { # Private namespace namespace eval v { variable previous_window variable window variable window_title variable wcount set previous_window {} set wcount 0 } } # window -- # Define a new window # # Arguments: # name Name of the window # title Title # contents Script defining the contents of the window # # Result: # None # # Side effects: # Entry in array window filled # proc ::Decide::window {name title contents} { set v::window($name) $contents set v::window_title($name) $title } # text -- # Define/show a text label # # Arguments: # string String to present # second Second string if any # # Result: # None # # Side effects: # Label widget created # proc ::Decide::text {string {second {}}} { if { $second eq "" } { label .frame1.label$v::wcount -text $string -font "Helvetica, 14" grid .frame1.label$v::wcount - -sticky news } else { set l1 $v::wcount incr v::wcount set l2 $v::wcount label .frame1.label$l1 -text $string -font "Helvetica, 14" label .frame1.label$l2 -text $second -font "Helvetica, 14" grid .frame1.label$l1 .frame1.label$l2 -sticky news } incr v::wcount } # entry -- # Define/show an entry widget # # Arguments: # string String to present # name Variable name # # Result: # None # # Side effects: # Label widget created # proc ::Decide::entry {string name} { set l1 $v::wcount incr v::wcount set e1 $v::wcount ::label .frame1.label$l1 -text $string -font "Helvetica, 14" ::entry .frame1.entry$e1 -textvariable ::Decide::$name -font "Helvetica, 14" ::grid .frame1.label$l1 .frame1.entry$e1 -sticky news incr v::wcount } # choice -- # Define/show a set of radio buttons # # Arguments: # name Name of the associated variable # choices List of values and descriptive texts # # Result: # None # # Side effects: # Set of radio buttons created # proc ::Decide::choice {name choices} { if { ![info exists ::Decide::$name] } { set ::Decide::$name [lindex $choices 0] } foreach {value text} $choices { radiobutton .frame1.radio$v::wcount -variable ::Decide::$name -text $text \ -value $value -font "Helvetica, 14" grid .frame1.radio$v::wcount -sticky nw incr v::wcount } } # button -- # Define/show a pushbutton # # Arguments: # type Type of button # command Command associated with it (optional) # # Result: # None # # Side effects: # Pushbutton created # proc ::Decide::button {type {command {}}} { switch -- $type { "previous" { set command "::Decide::previousWindow" set text "<<" } "next" { set text ">>" } "done" { set command "exit" set text "Done" } default { tk_messageBox -code error -message "Unknown button type: $type" exit } } ::button .frame2.button$v::wcount -text $text -font "Helvetica, 14" \ -command [list namespace eval ::Decide $command] append v::buttons ".frame2.button$v::wcount " incr v::wcount } # show -- # Show a particular window # # Arguments: # name Name of the window # # Result: # None # # Side effects: # Entry in array window filled # proc ::Decide::show {name} { if { [info exists v::window($name)] } { foreach child [winfo children .frame1] { destroy $child } foreach child [winfo children .frame2] { destroy $child } set v::wcount 0 set v::buttons {} namespace eval ::Decide $v::window($name) if { $v::buttons ne "" } { eval grid $v::buttons } } else { tk_messageBox -type ok -icon error -message "No window defined by the name '$name'" exit } wm title . $v::window_title($name) lappend v::previous_window $name } # previousWindow -- # Show the previous window # # Arguments: # None # # Result: # None # # Side effects: # Previous window shown and "previous_window" updated # proc ::Decide::previousWindow {} { set prev [lindex $v::previous_window end-1] set v::previous_window [lrange $v::previous_window 0 end-2] show $prev puts >>$v::previous_window<< } # mainWindow -- # Set up the main window # # Arguments: # None # # Result: # None # # Side effects: # Main window set up with two frames # proc ::Decide::mainWindow {} { wm geometry . 400x250 frame .frame1 frame .frame2 pack .frame1 -side top -fill x -padx 4 pack .frame2 -side bottom -fill x -padx 4 } # main -- # test the code so far # if { 0 } { ::Decide::mainWindow ::Decide::window x "Title" { text "Welcome to this simple" text "Decision tree" choice v { red "Red" blue "Blue" green "Green text" } button next { show y } } ::Decide::window y "Title" { text "The end" button previous button done } ::Decide::show x } ::Decide::mainWindow namespace eval ::Decide { source [lindex $argv 0] } ====== The simple example: ====== # example.app -- # Simple example of the application of "decision.tcl": # Compute the area and volume of three simple geometrical # shapes # window "type" "Select the type" { text "Type of geometrical body:" choice type { cube "Cube" block "Block" sphere "Sphere" } button next { show $type } } window "cube" "Size of the cube" { text "Please enter the side of the cube:" entry "Size: " size button previous button next { set area [expr {6.0*$size*$size}] set volume [expr {$size*$size*$size}] show "result" } } window "block" "Dimensions of the block" { text "Please enter the three sides of the block:" entry "Length: " length entry "Width: " width entry "Height: " height button previous button next { set area [expr {2.0*($length*$width+$length*$height+$width*$height)}] set volume [expr {$length*$width*$height}] show "result" } } window "sphere" "Size of the sphere" { text "Please enter the radius of the sphere:" entry "Radius: " radius button previous button next { set area [expr {4.0*acos(-1.0)*$radius*$radius}] set volume [expr {4.0*acos(-1.0)*$radius*$radius*$radius/3.0}] show "result" } } window "result" "Area and volume" { text "Area:" "$area" text "Volume:" "$volume" button previous button done } # # Start the application # show "type" ====== ---- !!!!!! %| [Category GUI] | [Category Concept] |% !!!!!!