[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. ---- [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) ---- 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) } 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) 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 { <>} {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 "" \ +[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] } 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://www.8ung.at/matthias_hoffmann/prog/mapsource.jpg] [http://www.8ung.at/matthias_hoffmann/prog/maprun.jpg] ---- [[ [Arts and crafts of Tcl-Tk programming] ]] ---- [Category GUI]