A simple GUI for decision trees

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" 

fred_stmk - 2010-03-24 14:59:33

<could you please add a sample file for an unskilled user?>

AM Sure, but how much simpler can I make it? There have to be some decisions and some calculations, otherwise it gets really boring. Can you describe an example that you would like to see turned into this form?