Dynamic forms and variables

Last week on comp.lang.tcl I was helping someone who claimed to have problems with passing value from an entry widget to a procedure. Initially it seemed to be a problem with scoping, but it turns out his real problem was in needing to create a form with a variable number of input fields. He was struggling with what to use as the target of the -textvariable option and how to reference that in a proc.

Dynamically creating widgets based on a runtime specification — for example, a list of field names from a database — is something Tk does extremely well. I've been doing it so long that the fact that the solution may be non-obvious never occurs to me. The truth is, though, if you're just starting out with Tcl and Tk the solution probably isn't obvious.

However, just because it may not be obvious doesn't mean that it's not very easy to do. Although one can create variables on the fly with ease, the trick is to instead use a global or namespace variable.

The person asking the question in comp.lang.tcl had code that looked roughly like the following code. Where he got stuck was in figuring out what to do for ???, and how to ultimately pass the data entered by the user into the validate procedure.

foreach field $listOfFields {
  frame .frame$field
  label .frame$field.label ...
  entry .frame$field.entry ... -textvariable ???
  bind .frame$field.entry  {validate ???}
  ...
}
proc validate {string} {...}

Using the same value (e.g. -textvariable field) for every iteration of the loop can't work because all entry widgets would end up sharing a single variable. If you use something like $field in the above code you get unique variables for each entry widget (whatever string is stored in the field variable) but then you still have the problem of what to send to the validate command.

There is a simple solution, which is to use an array to contain the data for all the fields. You can index the array with the field name, and pass the field name around to other procs. In the specific example coded above, because the validate proc couldn't be rewritten we need to introduce a small helper proc.

The following code shows a complete working example.

set listOfFields [list "Field 1" "Field 2" "Field 3"]
foreach field $listOfFields {
  set f ".frame-$field"
  frame $f
  label $f.label -text "$field:"
  entry $f.entry -textvariable data($field)
  pack $f.label -side left
  pack $f.entry -side left -fill x -expand 1
  bind $f.entry <Return> [list validate_field $field]
  pack $f -side top -fill x
}
proc validate_field {field} {
  variable data
  validate $data($field)
}
proc validate {string} {puts "validate called with string '$string'"}

As you can see, by using arrays we avoid all the unpleasantness of dynamic variable names and the odd quoting that sometimes requires.

Built-in data validation

The entry widget has options for doing input validation which obviates the need to bind on <Return>. This article is more about the technique of using arrays rather than dynamic variable names so I stuck to the coding style of the original example. If you are doing forms that need to do input validation I encourage you to check out the entry widget options "-validate", "-validatecommand" and "-invalidcommand".

Resources

  1. Simple dynamic form example -