I did something similar to map between user date format and SQL ('yyyy-mm-dd'). Yes, snit makes it easy. - CLN
(Peter Spjuth - 15 Jun 2004): Updated the code here to my latest. Mainly bugfixes and some behavioural changes.
# MappingEntry # Copyright (c) 2003, Peter Spjuth. # Permission to use this is granted under the terms of the standard # Tcl license agreement. package require snit package require Tk package provide MappingEntry 0.4.3 namespace eval MappingEntry { namespace export MappingEntry BinEntry HexEntry DecEntry } #---------------------------------------------------------------------- # # MappingEntry::MappingEntry -- # # Create an entry widget that supports setting up mapping # functions between the edit text and the textvariable. # It also supports some validation. # # Options: # -fromvar A partial command. The textvariable's value is added # as last argument to the command and the result is # displayed in the entry field. # -tovar A partial command. The textvariable's contents and the # entry's contents is added as last arguments to the # command and the result is stored in the textvariable. # -maxlength Limit the entry's contents to this maximum length. # -maxvalue Limit the textvariable to this maximum value. # -minvalue Limit the textvariable to this minimum value. # -validre A regular expression that the entry's contents must # match. # # Note: # The partial commands must be valid lists and no substitutions # will take place before the command is called. # # Max/minvalues are checked using expr's < and >, so they can be # used for integers, reals or strings. # #---------------------------------------------------------------------- ::snit::widgetadaptor MappingEntry::MappingEntry { # The variable holding the value displayed in the entry. variable dispvar "" # A flag to avoid race conditions variable busy 0 # Mapping options option -fromvar option -tovar option -textvariable # Validation options option -maxlength option -maxvalue option -minvalue option -validre # Experimental option -ignorechars "" constructor {args} { installhull [entry $self -textvariable [varname dispvar]] $self configurelist $args $hull configure -validate key -vcmd [mymethod Validate %d %P] # Set up a trace on the entry's variable # This does not need the same special handling as the other # trace since it can't be changed and it will be destroyed # if the widget is destroyed. trace add variable [varname dispvar] write [mymethod UpdateVar] # Make sure the displayed value is "canonical" when leaving the # entry. bind $win <FocusOut> [mymethod UpdateDisp] } destructor { $self RemoveTrace } onconfigure -textvariable {value} { $self RemoveTrace set options(-textvariable) $value $self CreateTrace } # Remove variable trace method RemoveTrace {} { if {$options(-textvariable) == ""} return upvar \#0 $options(-textvariable) TheVariable trace remove variable TheVariable write [mymethod UpdateDisp] } # Set up a trace on the -textvariable to keep the entry updated. method CreateTrace {} { if {$options(-textvariable) == ""} return upvar \#0 $options(-textvariable) TheVariable if {![info exists TheVariable]} { set TheVariable "" } if {$options(-minvalue) != "" && $TheVariable == ""} { set TheVariable $options(-minvalue) } after idle [mymethod UpdateDisp] trace add variable TheVariable write [mymethod UpdateDisp] } # Update the textvariable when the displayed variable changes method UpdateVar {args} { if {$busy} return if {$options(-textvariable) == ""} return # Avoid upvar in here to not confuse any traces on the # -textvariable. if {$options(-tovar) == ""} { # There is no mapping function. Use it directly. uplevel \#0 [list set $options(-textvariable) $dispvar] return } set cmd $options(-tovar) set old [uplevel \#0 [list set $options(-textvariable)]] lappend cmd $old $dispvar if {[catch {uplevel \#0 $cmd} result]} { set result "" } set busy 1 uplevel \#0 [list set $options(-textvariable) $result] set busy 0 after idle [mymethod CheckInsert] } # Update the displayed variable when the textvariable changes method UpdateDisp {args} { if {$busy} return set value [uplevel \#0 [list set $options(-textvariable)]] if {$options(-fromvar) == ""} { set dispvar $value return } set cmd $options(-fromvar) lappend cmd $value if {[catch {uplevel \#0 $cmd} result]} { set result "" } set busy 1 set dispvar $result set busy 0 after idle [mymethod CheckInsert] } # Overload icursor to track cursor movements method icursor {index} { $hull icursor $index after idle [mymethod CheckInsert] } # Check the insertion cursor. If the entry is full, change to # overwrite behaviour method CheckInsert {} { if {$options(-maxlength) == ""} return if {[string length $dispvar] < $options(-maxlength)} return if {[$hull selection present]} return if {[focus] != $win} return # Select the char at the cursor to get overwrite behaviour set from [$hull index insert] set to [expr {$from + 1}] if {$options(-ignorechars) != ""} { set char [string index [$hull get] $from] if {[string first $char $options(-ignorechars)] >= 0} { $win icursor $to return } } $hull selection range $from $to } # Apply validation options # If any error occurs, the change is denied method Validate {access new} { if {[catch {$self DoValidate $access $new} result]} { after idle [list bgerror $result] return 0 } return $result } # Do the actual validation method DoValidate {access new} { # Accept textvariable changes if {$access == -1} { return 1 } # Check maxlength if specified and if it is not a delete operation if {$options(-maxlength) != "" && $access == 1} { if {[string length $new] > $options(-maxlength)} { return 0 } } # Check the RegExp if specified if {$options(-validre) != ""} { if {![regexp $options(-validre) $new]} { return 0 } } # Check min/maxvalue if {$options(-minvalue) != "" || $options(-maxvalue) != ""} { # Min/max is checked against the textvariable so we must # first apply the mapping function. if {$options(-tovar) == ""} { set value $dispvar } else { set old [uplevel \#0 [list set $options(-textvariable)]] set cmd $options(-tovar) lappend cmd $old $new if {[catch {uplevel \#0 $cmd} value]} { set value 0 } } if {$options(-minvalue) != ""} { if {$value < $options(-minvalue)} { return 0 } } if {$options(-maxvalue) != ""} { if {$value > $options(-maxvalue)} { return 0 } } } return 1 } # Pass all other methods and options to the real entry widget, so # that the remaining behavior is as expected. delegate method * to hull delegate option * to hull } #---------------------------------------------------------------------- # # MappingEntry::BinEntry -- # # Create an entry widget for entering binary values. # A decimal value is stored in the textvariable. # # Options: # -digits : Number of binary digits # -shift : If specified, the entry will only edit a few bits # of the value. A -shift of 0 means that the least # significant bits are edited. # # Note: # The displayed binary may contain underscores that will be # ignored. When generated, the binary will have an underscore # for every four binary digits. # #---------------------------------------------------------------------- ::snit::widgetadaptor MappingEntry::BinEntry { option -digits 8 option -shift {} constructor {args} { installhull [MappingEntry::MappingEntry $self] $self configurelist $args $hull configure -ignorechars "_" } # Decimal to binary converter method dec2bin {width dec} { if {$dec == ""} {return ""} if {$options(-shift) != ""} { set dec [expr {$dec >> $options(-shift)}] set dec [expr {$dec & ((1 << $options(-digits)) - 1)}] } binary scan [binary format W $dec] B* bin set bin [format "%0*s" $width [string trimleft $bin 0]] # Add underscore for each fourth binary digit regsub -all {\d(?=(\d{4})+$)} $bin {\0_} bin return $bin } # Binary to decimal converter method bin2dec {olddec bin} { regsub -all "_" $bin "" bin binary scan [binary format B* [format %064s $bin]] W dec if {$options(-shift) != ""} { set mask [expr {((1 << $options(-digits)) - 1) << $options(-shift)}] set dec [expr {$dec << $options(-shift)}] set dec [expr {($olddec & ~$mask) | $dec}] } return $dec } onconfigure -digits {value} { set options(-digits) $value set width [expr {$value + (($value + 3) / 4 - 1)}] # An RE that maximizes the number of digits regardless of # any underscores in the string. set re [string map [list "%" $value] {^([01]_?){0,%}$}] $hull configure -tovar [mymethod bin2dec] \ -fromvar [mymethod dec2bin $value] \ -width $width -maxlength $width -validre $re } # Pass all other methods and options to the real entry widget, so # that the remaining behavior is as expected. delegate method * to hull delegate option * to hull } #---------------------------------------------------------------------- # # MappingEntry::HexEntry -- # # Create an entry widget for entering hexadecimal values. # A decimal value is stored in the textvariable. # # Options: # -digits : Number of hexadecimal digits # #---------------------------------------------------------------------- ::snit::widgetadaptor MappingEntry::HexEntry { option -digits 2 constructor {args} { installhull [MappingEntry::MappingEntry $self] $self configurelist $args } # Decimal to hex converter method dec2hex {width dec} { if {$dec == ""} {return ""} return [format "%0*lX" $width $dec] } # Hexadecimal to decimal converter method hex2dec {olddec hex} { set dec 0 scan $hex %lx dec return $dec } onconfigure -digits {value} { set options(-digits) $value $hull configure -tovar [mymethod hex2dec] \ -fromvar [mymethod dec2hex $value] \ -width $value -maxlength $value \ -validre {^[0-9a-fA-F]*$} } # Pass all other methods and options to the real entry widget, so # that the remaining behavior is as expected. delegate method * to hull delegate option * to hull } #---------------------------------------------------------------------- # # MappingEntry::DecEntry -- # # Create an entry widget for entering decimal values. # # Options: # -digits : Number of digits # #---------------------------------------------------------------------- ::snit::widgetadaptor MappingEntry::DecEntry { option -digits 3 constructor {args} { installhull [MappingEntry::MappingEntry $self -minvalue 0] $self configurelist $args } # Convert from decimal to "pure" integer proc dec2dec {olddec dec} { if {$dec eq ""} {return 0} scan $dec %ld } # Convert from valid integer to "pure" integer proc dec2dec2 {dec} { if {$dec eq ""} {return 0} expr {$dec} } onconfigure -digits {value} { set options(-digits) $value $hull configure -tovar [codename dec2dec] \ -fromvar [codename dec2dec2] \ -width $value -maxlength $value \ -validre {^\d*$} } # Pass all other methods and options to the real entry widget, so # that the remaining behavior is as expected. delegate method * to hull delegate option * to hull } # Testing if {[string equal $argv0 [info script]]} { catch {console show ; console eval {focus .console}} set thisscript [file join [pwd] [info script]] proc _rs {} { uplevel \#0 source \$thisscript eval destroy [winfo children .] } namespace import -force MappingEntry::HexEntry namespace import -force MappingEntry::BinEntry namespace import -force MappingEntry::DecEntry proc testa {} { option add *Entry.font "courier 10" # A test that creates multiple views of the same variable. wm deiconify . eval destroy [winfo children .] if {![info exists ::miffo]} {set ::miffo 4711} HexEntry .eh -textvariable miffo -digits 9 -maxvalue 0x3ffffffff BinEntry .eb -textvariable miffo -digits 34 -maxvalue 0x3ffffffff DecEntry .ed -textvariable miffo -digits 11 -maxvalue 0x3ffffffff BinEntry .ex1 -textvariable miffo -digits 6 -shift 0 BinEntry .ex2 -textvariable miffo -digits 6 -shift 6 BinEntry .ex3 -textvariable miffo -digits 6 -shift 12 BinEntry .ex4 -textvariable miffo -digits 6 -shift 18 BinEntry .ex5 -textvariable miffo -digits 6 -shift 24 BinEntry .ex6 -textvariable miffo -digits 4 -shift 30 label .l1 -text "A 34 bit number" label .l2 -text "As Hex" label .l3 -text "As Dec" label .l4 -text "As Fields" lower [frame .f] pack .ex1 .ex2 .ex3 .ex4 .ex5 .ex6 -in .f -side right -padx 2 pack .l4 -in .f -side left grid .l1 - - - -sticky w grid .eb - - - - -sticky w grid .l2 .eh .l3 .ed -sticky w grid .f - - - - -sticky w grid columnconfigure . 4 -weight 1 } testa }
How about contributing this to tklib?I wouldn't mind putting it there but I'm not currently inclined to spend the effort to make it happen.Too bad; the more of this type of thing that becomes available, the more valuable tklib becomes both as a toolbox and as example code for developers.