Version 2 of A simple GUI for decision trees

Updated 2008-02-25 09:31:08 by arjen

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.


With the program and the example below you get the following screens:

WikiDbImage step1.jpg WikiDbImage step2.jpg WikiDbImage step3.jpg


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"