A poor mans GUI

Arjen Markus (12 October 2002) Every so often, a graphical user-interface would be nice for a relatively simple program, just gather the information, put it into a file and run the program. Picking up the results and presenting them can then be done in another program.

Now, if there is a budget and time to make it a really nice user-interface, there are plenty of possibilities. But what if you need to something quickly and for low costs - the program is important enough to build such a GUI, but there is no time or money to do a proper job.

What you need then is a poor man's approach to building GUIs. And this page presents one such solution.

The philosophy is this:

  • We can divide the input into small forms, each taking a bunch of input parameters, but not that many.
  • We collect all the input in individual variables, perhaps validating them with simple rules.
  • We define the input forms and the output file via templates. The procedures below allow us to automatically set up a form, save the input and then write out the results.
  • Basic constraint: the variables are fairly isolated, they do not require extensive checking against other data. The forms can be simple and straightforward.

The most recent code can be found on Chiselapp


A similar alternative for a simple GUI specification is TkVSform.

See TEPAM.


AM Update, 6 november 2002:

I reorganised the source code so that it is now possible to add your own widget types and parameter types (thanks to Rolf Ade for the idea). Support for tables is on its way. Proper documentation (alas) is not.

Should you be interested (I dare not post the 1200 lines of code here - what is the limit?), just let me know.

AM Update, 14 november 2002:

I have now written quite a bit of documentation. It should help any one who wants to use it with setting up an application. (Status for tables: no advance yet)

AM Update, 21 december 2006

Recently, interest arose to turn a few of the applications I built with this package into web demos. I have been experimenting with generating HTML pages instead of Tk widgets and that works fine, but the real step forward would be to use Tclhttpd. I see quite a few possibilities there and the nice thing would be that by merely loading a different version you could have a standalone application or a web-based application.

Now, all I need to have is some time to make this dream come true ...

AM Update, 22 january 2007

I have something (almost) ready :). It works under its own CGI/HTTP server (happily derived from a tiny one on this Wiki). Main issues left:

  • The equivalent for "exit" in such an environment?
  • Making it possible that there is more than one simulateneous user

AM Another update, 27 august 2013

Picking up this HTML idea again, I could not find the code anymore, but with small steps I am able to reconstruct it. Targeting SCGI and NGINX. (MG It's archived here - AM I was unclear about it: I meant the HTML version)

AM I created a project on Chiselapp that will hold the code.

AM (6 october 2013) I have managed to adapt the code for the HTML version to use slave interpreters - one per connection, so that you get independent sessions. Still some issues left, especially with cleaning up, but it is looking promising.


Here is an overly simple little example:

  • We want to calculate the volume of a rectangular block via a specialised program.
  • This program uses an input file that contains the three dimensions of the block.
  • The user-interface should read the three numbers and write them into such an input file.

Well, what can be simpler? This is the input for the PMG package/application:

 package require PMG

 realParameter length
 realParameter width
 realParameter height
 defineForm form {
   Length =   [length    ] (m)
   Width  =   [width     ] (m)
   Height =   [height    ] (m)

     <ok_b    >  <cancel_b>

 }
 formButton form ok_b "OK" {
   outputData "blockv.tpl" "blockv.inp"
   saveData "blockv.prv"
   exit
 }
 formButton form cancel_b "Cancel" {
   exit
 }

 #
 # Code to run the GUI
 #
 loadData "blockv.prv"
 showForm form

The template file, blockv.tpl looks like:

   [length  ]      Length of block
   [width   ]      Width of block
   [height  ]      Height of block

In both templates, the form template and the file template, use is made of [ and ] to delimit a entry field. Inside the entry field is the name of the variable that needs to be set.

Other field definitions (only in the forms, not in the output) include:

Field defining one radio button (repeat the same variable name, properties via choiceParameter):

   [(o) radio_choice ]

Field to define an option menu (define the properties via via choiceParameter):

   [(?) option_menu ]

Field to define a checkbox (define the properties via logicalParameter):

   [(x) check_box ]

Field to define a label whose contents may be vary: properties via logicalParameter):

   {label_variable }

Field to define a push button (define the properties via formButton)

   <push_button >

The layout is derived from the form and the fields in the form, the width of the fields in the window is the same as the width of the fields in the form (in characters). Pushbuttons must be defined separately, via the formButton command.

The package makes elementary checks on consistency in the definitions and provides some facilities for validation.

Note:

The script below is much more limited than you might expect from the above description. It provides very little consistency checking, it does not load and save the variables yet, it is not possible yet to properly cancel an action.

Also, choices via a set of radio buttons or an option button are not implemented yet.

Nevertheless, it does provide a start.


14oct02 escargo - OK. Here's my persistent question: What kind of license do you release this code with? Public domain? BSD? GPL (and what version)?

AM Just plain public domain. I thought of this a couple of days ago, independent of your discussion (I had not read the page yet) and had a go at a proof of concept - as below. I surely want to develop this somewhat further and then put it on CANTCL.


 # package PMG --
 #
 namespace eval ::PMG {
    variable params
    variable forms
    variable font "Courier 12"

    namespace export realParameter integerParameter logicalParameter \
                     choiceParameter defineForm showForm formButton
 }

 # realParameter --
 #    Define a real parameter (possibly with valid range)
 #
 # Arguments:
 #    name     Name of the real parameter
 #    lower    Lower bound (inclusive) for any values (optional)
 #    upper    Upper bound (inclusive) for any values (optional)
 #
 # Result:
 #    None
 #
 # Side effect:
 #    Create an entry in the array params, parameters are considered to
 #    be global for all forms
 #
 proc ::PMG::realParameter {name {lower {}} {upper {}}} {
    variable params

    set params($name)       0.0
    set params($name,type)  "real"
    set params($name,lower) $lower
    set params($name,upper) $upper
    if { $lower != {} } {
       set params($name) $lower
    }
 }

 # integerParameter --
 #    Define an integer parameter (possibly with valid range)
 #
 # Arguments:
 #    name     Name of the integer parameter
 #    lower    Lower bound (inclusive) for any values (optional)
 #    upper    Upper bound (inclusive) for any values (optional)
 #
 # Result:
 #    None
 #
 # Side effect:
 #    Create an entry in the array params, parameters are considered to
 #    be global for all forms
 #
 proc ::PMG::integerParameter {name {lower {}} {upper {}}} {
    variable params

    set params($name)       0
    set params($name,type)  "integer"
    set params($name,lower) $lower
    set params($name,upper) $upper
    if { $lower != {} } {
       set params($name) $lower
    }
 }

 # logicalParameter --
 #    Define a logical parameter (value: 0 or 1)
 #
 # Arguments:
 #    name     Name of the logical parameter
 #    text     Text to describe the parameter
 #
 # Result:
 #    None
 #
 # Side effect:
 #    Create an entry in the array params, parameters are considered to
 #    be global for all forms
 #
 proc ::PMG::logicalParameter {name text} {
    variable params

    set params($name,type)  "logical"
    set params($name,text)  $text
    set params($name)       0
 }

 # choiceParameter --
 #    Define a choice parameter (values and descriptive text)
 #
 # Arguments:
 #    name     Name of the choice parameter
 #    values   Pairs of values and descriptive text
 #
 # Result:
 #    None
 #
 # Side effect:
 #    Create an entry in the array params, parameters are considered to
 #    be global for all forms
 #
 proc ::PMG::choiceParameter {name values} {
    variable params

    set params($name,type)    "choice"
    set params($name,choices) $values
    set params($name)         [lindex $values 0]
 }

 # defineForm --
 #    Define the contents of a form
 #
 # Arguments:
 #    name     Name of the form
 #    layout   Layout of the form
 #
 # Result:
 #    None
 #
 # Side effect:
 #    Create an entry in the array forms
 #
 # Note:
 #    There is no check for the validity
 #
 proc ::PMG::defineForm {name layout} {
    variable forms

    set forms($name) $layout
 }

 # formButton --
 #    Define the properties of a button in a form
 #
 # Arguments:
 #    form     Name of the form to which the button belongs
 #    button   Formal name of the button
 #    label    Label on the button
 #    script   Script to execute when the button is pressed
 #
 # Result:
 #    None
 #
 # Side effect:
 #    Create an entry in the array forms
 #
 # Note:
 #    There is no check for the validity
 #
 proc ::PMG::formButton {form button label script} {
    variable forms

    set forms($form,$button,label)  $label
    set forms($form,$button,script) $script
 }

 # showForm --
 #    Show the form in a new toplevel window
 #
 # Arguments:
 #    form     Name of the form
 #
 # Result:
 #    None
 #
 # Side effect:
 #    Create a new toplevel window with the form in it
 #
 proc ::PMG::showForm {form} {
    variable forms
    variable font

    #
    # Check if the toplevel window exists (.f_$form)
    # Also check if there is such a form
    #
    set f .f_$form
    set t $f.text

    if { [winfo exists $f] } {
       wm raise $f
       return
    }

    if { [array get forms $form] == {} } {
       error "Form $form not defined"
    }

    #
    # Create the toplevel and the text widget holding the form
    #
    set width  0
    set height 0
    set layout [split $forms($form) "\n"]
    foreach line $layout {
       incr height
       set length [string length $line]
       if { $length > $width } {
          set width $length
       }
    }
    incr width  2
    incr height 2

    toplevel $f
    text     $t -width $width -height $height -font $font -background gray
    pack     $t

    foreach event {<KeyPress> <<PasteSelection>>} {bind $t $event break}

    foreach line $layout {
       set line [string trimright $line]
       #
       # Split the line into pieces, ordinary text and fields
       #
       set line2 \
          [string map {[ "~[" ] "]~" \{ "~\{" "\}" "\}~" < "~<" > ">~"} $line]
       set line2 [split $line2 "~"]
       foreach segment $line2 {
          set first  [string index $segment 0]
          if { [string first $first "\[]\{\}<>"] > -1 } {
             InsertField $t $form $segment
          } else {
             $t insert end $segment
          }
       }
       $t insert end "\n"
    }
 }

 # InsertField --
 #    Create a widget specific to the field's type and insert into the
 #    text widget
 #
 # Arguments:
 #    t        Text widget
 #    form     Name of the form
 #    field    Definition of the field
 #
 # Result:
 #    None
 #
 # Side effect:
 #    Create a new embedded widget
 #
 proc ::PMG::InsertField {t form field} {
    variable font
    variable forms
    variable params

    set type0        [string index $field 0]
    set type1        [string index $field 1]
    set type2        [string index $field 2]
    set field_length [string length $field]

    switch -- $type0 {
    "\[" {
       if { $type1 != "(" } {
          set param [string trim [string range $field 1 end-1]]
          entry $t.$param -textvariable ::PMG::params($param) \
             -font $::PMG::font -width $field_length
          $t window create end -window $t.$param
          bind $t.$param "<FocusOut>" \
             +[list ::PMG::ValidateParameter $t.$param $param]

       } else {
          set param [string trim [string range $field 4 end-1]]
          switch -- $type2 {
          o { radiobutton $t.$param.$count -variable ::PMG::params($param) \
                 -text "???" -value "???" \
                 -font $::PMG::font -width $field_length
              $t window create end -window $t.$param.$count
            }
          x { checkbutton $t.$param -variable ::PMG::params($param) \
                 -text $::PMG::params($param,text) \
                 -font $::PMG::font -width $field_length \
                 -anchor nw
              $t window create end -window $t.$param
            }
          default {
              return
            }
          }
       }
    }

    "<" {
        set param [string trim [string range $field 1 end-1]]
        button $t.b_$param -text $::PMG::forms($form,$param,label) \
           -command $::PMG::forms($form,$param,script) \
           -font $::PMG::font -width $field_length
        $t window create end -window $t.b_$param
    }

    "\{" {
        set param [string trim [string range $field 1 end-1]]
        label $t.$param -textvariable ::PMG::params($param) \
           -font $::PMG::font -width $field_length \
           -anchor nw
        $t window create end -window $t.$param
    }
    default {
       return
    }
    }
 }

 # ValidateParameter --
 #    Validate the new value for a parameter
 #
 # Arguments:
 #    entry    Entry widget that holds the parameter
 #    param    Parameter name
 #
 # Result:
 #    None
 #
 # Note:
 #    This is invoked when the focus leaves the entry widget.
 #    At least on Windows, there is no FocusOut event if you press a
 #    button, rather than move to the next entry field.
 #    P.M:
 #    This requires extra attention
 #
 # Side effect:
 #    Presents a message box if the new value is out of range
 #
 proc ::PMG::ValidateParameter {entry param} {
    variable params

    set lowup   0
    set message 0
    if { $params($param,lower) != {} } {
       incr lowup 1
       if { $params($param) < $params($param,lower) } {
          set message 1
       }
    }
    if { $params($param,upper) != {} } {
       incr lowup 2
       if { $params($param) > $params($param,upper) } {
          set message 1
       }
    }

    if { $message } {
       switch -- $lowup {
       1 { set text "Parameter $param must be greater/equal $params($param,lower)" }
       2 { set text "Parameter $param must be lower/equal $params($param,upper)" }
       3 { set text "Parameter $param must be between $params($param,lower) and $params($param,upper)" }
       }
       tk_messageBox -message $text -type ok -title "Value out of range"
    }

    # PM: Set the focus back into the entry

    return
 }

 package provide PMG 0.1


 # main --
 #   Set up the form, show it
 #
 # Note:
 #   This is NOT identical to the example appearing in the text!
 #
 # package require PMG

 namespace import ::PMG::*

 realParameter length  0.0
 realParameter width   0.0
 realParameter height  0.0
 realParameter volume
 logicalParameter save_data "Save data"
 defineForm form {
    Length =  [length    ] (m)
    Width  =  [width     ] (m)
    Height =  [height    ] (m)

    { volume } - Volume

    [(x) save_data]

    <calc_b>  <exit_b>
 }
 formButton form calc_b "Calculate" {
    #outputData "blockv.tpl" "blockv.inp"
    #saveData "blockv.prv"
    #
    # TODO: get rid of this clumsy way of working
    #
    set ::PMG::params(volume) \
       [expr {$::PMG::params(length)*$::PMG::params(width)*$::PMG::params(height)}]
    #exit
 }
 formButton form exit_b "Exit" {
   exit
 }

 #
 # Code to run the GUI
 #
 #loadData "blockv.prv"
 showForm form
 wm withdraw .

MHo: This is funny! Many years ago I followed the very same ideas: 'drawing' a dialog with a simple text editor is enough, let the computer do the stupid rest! Ok, in between there are two programs that has to be written ;-) one for checking the syntax and 'compiling' a sort of bytecode, and one to interpret these intermediate code, which means: to display a nice GUI under MSDOS:

http://home.arcor.de/hoffenbar/prog/mapsource.jpg http://home.arcor.de/hoffenbar/prog/maprun.jpg


Arlie L. Codina: I think I can really used this. It would be nice if this could be documented and PMG is further developed. I'm a Tcl/Tk newbie.

AM PMG is actually available via CANTCL. Has been for a couple of years actually, let me know if you need advice or have problems using it...