Updated 2011-06-17 09:52:14 by RLE

See What is a Megawidget? for megawidget info.

Some people have been asking (on news:comp.lang.tcl) for the Tk core to be modified so that widgets all carry with them some additional user data. This is not necessary. The following script illustrates how you can do this without all that paraphernalia in pure Tcl...
  proc substvars {varlist script} {
      foreach varname $varlist {
          upvar $varname var
          regsub -all %%${varname}%% $script [list $var] script
      }
      return $script
  }
  foreach class {
      button checkbutton radiobutton menubutton menu label message
      frame toplevel scrollbar scale text canvas
  } {
      rename $class userdata'enabled'$class
      proc $class {pathname args} [substvars class {
          set class %%class%%
          set w [eval [list userdata'enabled'$class $pathname] $args]
          rename $w userdata'enabled'$w
          upvar #0 ::userdata'data ary
          set ary($w) {}
          proc $w {subcommand args} [substvars w {
              set w %%w%%
              if {![string compare $subcommand "setUserData"]} {
                  upvar #0 ::userdata'data ary
                  switch [llength $args] {
                      0 {
                          return $ary($w)
                      }
                      1 {
                          set ary($w) [lindex $args 0]
                          return
                      }
                      default {
                          return -code error "wrong # args: should be\
                                  \"$w setUserData ?value?\""
                      }
                  }
              } else {
                  return [uplevel userdata'enabled'$w $subcommand $args]
              }
          }]
          return $w
      }]
  }

I'll leave adding the finishing flourishes (like making the error messages take account of the new command, and deleting the user data and the proc when the widget is destroyed) up to others for now. DKF

Vince writes it is not necessary, indeed, but it could still be desirable. The code you have here is pretty dense, and if you also add the cleanup code, error message fixing code, etc, it's going to become even more impenetrable (and perhaps slow?). That's when one has to think about a providing a simple set of C hooks to Tk to let this happen very simply.

I do think the speed issue can be a problem, especially when you add handling and fixing of error conditions. The problem is that a lot of Tk's .tcl library contains code like this (from tkTextInsert):
    catch {
        if {[$w compare sel.first <= insert] \
         && [$w compare sel.last >= insert]} {
            $w delete sel.first sel.last
        }
    }

which, if the '$w' is a wrapped object, can generate a massive error traceback, which then has to be discarded. (This is a general disadvantage of all the 'rename' styles of changing the behaviour of Tk's core widgets). In this particular case, Tk's library should be changed to be:
  # New version which doesn't use 'catch', to avoid
  # generating a long stack trace every time the window doesn't
  # have a current selection
  proc tkTextInsert {w s} {
      if {[string equal $s ""] || [string equal [$w cget -state] "disabled"]} {
          return
      }
      if {[llength [$w tag ranges sel]]} {
          if {[$w compare sel.first <= insert] \
                  && [$w compare sel.last >= insert]} {
              $w delete sel.first sel.last
          }
      }
      $w insert insert $s
      $w see insert
  }

but there are other examples where such changes are not possible. (e.g. in menus, there is no 'entryexists' sub-command, so we need to use a catch).

Peter Newman: 28 Feb 2004: I'm interested in extending and enhancing Tk widgets. And the above looks like it might be interesting in this respect. But I haven't got a clue what they're talking about. Could somebody please explain:-

  • What is meant by "user data".

And could you please give 2 or 3 realistic examples of a specific widget (button, frame, whatever) - and the "user data" that you might want to "add" to that widget. Many thanks.

I (Bryan Oakley) would use the user data to store things like help information (well, pointers to help information), tooltips, and possibly other information. For example:
    button .toolbar.cut ... -userdata {tooltip  "cut selected text" helpid "edit->cut"}
    button .toolbar.copy ... -userdata {tooltip "copy selected text to clipboard" helpid "edit->copy"}
    ....
    bind .toolbar.cut <FocusIn> {
        statusbar echo [%W userdata tooltip]
    ....
    button .clickForHelp -command clickForHelp ...
    proc clickForHelp {} {
        set w [letUserClickOnSomeWidget]
        showHelp [$w userdata helpid]
    }
    ....
    entry .form.e1 -userdata {default "foo" required 0}
    entry .form.e2 -userdata {default "bar" required 1}
    button .form.submit -command [list submit .form]
    button .form.reset -command [list reset .form]
    proc submit {form} {
        foreach widget [winfo children $form] {
            set required [$widget userdata required]
            if {$required && [$widget get] == ""} {
                error "..."
            }
        }
    }
    proc reset {form} {
        foreach widget [winfo children $form] {
            $widget delete 0 end
            set default [$widget userdata default]
            $widget insert 0 $default
    }

... and so on. Yes, all of that can be done with global arrays, but I think it would be easier and better to keep widget-specific data with the widget itself. And no, the above code doesn't work and has lots of missing bits and such. It's just to illustrate ways I would personally use user data.

I would also find it useful when writing megawidgets, too. I'd let the internal widgets keep track of their individual state. By making use of user data, this data would automatically be reaped when the widget goes away. It's just a minor convenience, but one I would relish.

Peter Newman 28 Feb 2004: Thanks Bryan. That's much clearer. Looks like a good idea too. You could presumably do the same thing other ways. But the -userdata approach looks like it'd be simpler and cleaner.