Mentry is a library package for Tcl/Tk version 8.0 or higher, written in pure Tcl/Tk code. It contains:
A multi-entry widget consists of any number of entry widgets separated by labels, all embedded in a frame. Appropriately chosen configuration options make this conglomerate look like one single entry containing preinserted text pieces having invariant positions within the widget. The initial width of an entry child also determines the maximal number of characters that can be inserted into it; when reaching this limit in an entry having the input focus, the latter is set automatically to the next enabled entry child. The same action is triggered by typing a character contained in the label following the current entry, if the latter is non-empty.
Within a mentry widget, the Left
, Right
,
Home
, End
, and BackSpace
keys work
across entry boundaries, while Control-Left
and
Control-Right
play the same role as Tab
and
Shift-Tab
in the case of ordinary entries.
Some of the above features are implemented with the aid of the widget callback package Wcb, written in pure Tcl/Tk code as well. The Mentry package requires version 2.7 or higher of Wcb, whose download location is
http://www.nemethi.de
It is very easy to create a multi-entry widget. For example, the command
mentry::mentry .me -body {3 - 3 - 4}
will create a mentry widget consisting of two entries of width 3 and one of
width 4, separated by "-"
characters. With the command
foreach w [.me entries] { wcb::cbappend $w before insert wcb::checkStrForNum }
you can make sure that the three entries will only accept numeric input, thus providing a comfortable and safe user interface for editing 10-digit phone numbers.
Mentry is available for free download from the same URL as Wcb. The
distribution file is mentry2.8.tar.gz
for UNIX and
mentry2_8.zip
for Windows. These files contain the same
information, except for the additional carriage return character preceding the
linefeed at the end of each line in the text files for Windows.
Install the package as a subdirectory of one of the directories given by the
auto_path
variable. For example, you can install it as a
directory at the same level as the Tcl and Tk script libraries. The
locations of these library directories are given by the
tcl_library
and tk_library
variables, respectively.
To install Mentry on UNIX, cd
to the desired directory
and unpack the distribution file mentry2.8.tar.gz
:
gunzip -c mentry2.8.tar.gz | tar -xf -
This command will create a directory named mentry2.8
, with the
subdirectories demos
, doc
, and scripts
.
On Windows, use WinZip or some other program capable of unpacking the
distribution file mentry2_8.zip
into the directory
mentry2.8
, with the subdirectories demos
,
doc
, and scripts
.
To be able to access the commands and variables defined in the package Mentry, your scripts must contain one of the lines
package require Mentry package require mentry
You can use either one of the above two statements because the file
mentry.tcl
contains both lines
package provide Mentry ... package provide mentry ...
You are free to remove one of these two lines from mentry.tcl
if you want to prevent the package from making itself known under two different
names. Of course, by doing so you restrict the argument of
package require
to a single name. Notice that the
examples below use the statement package
require Mentry
.
Since the package Mentry is implemented in its own namespace called
mentry
, you must either invoke the
namespace import mentry::pattern ?mentry::pattern ...?
command to import the procedures you need, or use qualified names
like mentry::mentry
. In the examples below we have chosen
the latter approach.
To access Mentry variables, you must use qualified
names. There are only two Mentry variables that are designed to be
accessed outside the namespace mentry
:
mentry::version
holds the current version
number of the Mentry package.
mentry::library
holds the location of the
Mentry installation directory.
Let's resume the example mentioned in the Overview
in a bit more systematical manner. First, we will write a procedure for
creating a mentry widget that allows to display and edit 10-digit phone numbers
and accepts any configuration options supported by the mentry::mentry
command:
#------------------------------------------------------------------------------ # phoneNumberMentry # # Creates a new mentry widget win that allows to display and edit 10-digit # phone numbers. Sets the type attribute of the widget to PhoneNumber and # returns the name of the newly created widget. #------------------------------------------------------------------------------ proc phoneNumberMentry {win args} { # # Create a mentry widget consisting of two entries of width 3 and one of # width 4, separated by "-" characters, and set its type to PhoneNumber # eval [list mentry::mentry $win] $args $win configure -body {3 - 3 - 4} $win attrib type PhoneNumber # # Allow only decimal digits in all entry children; use # wcb::cbappend (or wcb::cbprepend) instead of wcb::callback # in order to keep the wcb::checkEntryLen callback, # registered by mentry::mentry for all entry children # foreach w [$win entries] { wcb::cbappend $w before insert wcb::checkStrForNum } return $win }
The first argument win
is the name of the widget, and the
keyword args
represents a list of configuration options and their
values, just like in the case of the standard Tk widgets. The value
{3 - 3 - 4}
of the -body
option specifies that the mentry
should consist of two entries of width 3 and one of width 4, separated by
labels displaying the "-"
character.
Each mentry widget may have any number of private attributes, which
can be set and retrieved with the aid of the attrib
subcommand of the Tcl procedure
corresponding to the widget. We use this subcommand to define the
type
attribute of the newly created widget and set it to the value
"PhoneNumber"
. Although this is not strictly necessary, it
will enable us to distinguish a phone number mentry from other multi-entry
widgets.
The mentry::mentry
command registers the wcb::checkEntryLen
callback with each entry
child of the mentry widget to restrict the number of characters that can be
inserted into it to the initial width specified in the -body
option. Besides this constraint, we want our entries to accept only
decimal digits, therefore we use the wcb::cbappend
command to add the
procedure wcb::checkStrForNum
to
the callback list of each entry child. By invoking wcb::callback
instead of
wcb::cbappend
(or wcb::cbprepend
), we would replace the
callback list with the one consisting of the single element
wcb::checkStrForNum
.
Our second procedure outputs a phone number to a mentry widget having a
type
attribute value of "PhoneNumber"
:
#------------------------------------------------------------------------------ # putPhoneNumber # # Outputs the phone number num to the mentry widget win of type PhoneNumber. # The phone number must be a string of length 10, consisting of decimal digits. #------------------------------------------------------------------------------ proc putPhoneNumber {num win} { # # Check the syntax of num # if {[string length $num] != 10 || ![regexp {^[0-9]*$} $num]} { return -code error "expected 10 decimal digits but got \"$num\"" } # # Check the widget and display the properly formatted phone number # checkIfPhoneNumberMentry $win $win put 0 [string range $num 0 2] [string range $num 3 5] \ [string range $num 6 9] }
We use the put
subcommand
of the Tcl procedure corresponding to the mentry widget to display the three
substrings of the given phone number in the corresponding entries, starting
with the entry child whose index is specified as the first argument following
the word put
.
Next, we need a procedure that returns the phone number contained in a
mentry widget having a type
attribute value of
"PhoneNumber"
:
#------------------------------------------------------------------------------ # getPhoneNumber # # Returns the phone number contained in the mentry widget win of type # PhoneNumber. #------------------------------------------------------------------------------ proc getPhoneNumber win { # # Check the widget # checkIfPhoneNumberMentry $win # # Generate an error if any entry child is empty or incomplete # for {set n 0} {$n < 3} {incr n} { if {[$win isempty $n]} { focus [$win entrypath $n] return -code error EMPTY } if {![$win isfull $n]} { focus [$win entrypath $n] return -code error INCOMPL } } # # Return the phone number built from the # values contained in the entry children # $win getarray strs return $strs(0)$strs(1)$strs(2) }
The procedure runs over the indices of the entry children of the given
mentry widget and invokes the isempty
and isfull
subcommands of the Tcl command
corresponding to the given mentry widget. If one of the entries is found
to be empty or incomplete, the procedure gets its path name by calling the
entrypath
subcommand,
sets the focus to that entry, raises an error, and returns the value
"EMPTY"
or "INCOMPL"
, respectively. The
application invoking this procedure should then display an appropriate error
message corresponding to the return value.
Notice that the number 3
in the for
loop above is
nothing else than [$win entrycount]
. Also, it would be
sufficient to check whether all entry children are full, because an empty entry
is at the same time incomplete. The preliminary check whether an entry is
empty is just made for the user's convenience.
To build the phone number from the values contained in the entry children,
we use a temporary array variable and invoke the getarray
subcommand, which copies the
contents of the entries to the corresponding array elements.
The last two procedures presented above contain an invocation of the command
checkIfPhoneNumberMentry
, which is implemented as folows:
#------------------------------------------------------------------------------ # checkIfPhoneNumberMentry # # Generates an error if win is not a mentry widget of type PhoneNumber. #------------------------------------------------------------------------------ proc checkIfPhoneNumberMentry win { if {![winfo exists $win]} { return -code error "bad window path name \"$win\"" } if {[string compare [winfo class $win] "Mentry"] != 0 || [string compare [$win attrib type] "PhoneNumber"] != 0} { return -code error \ "window \"$win\" is not a mentry widget for phone numbers" } }
This procedure retrieves the value of the type
attribute of its
argument to check whether the latter denotes a mentry widget for phone numbers
(remember that this attribute was set to the value "PhoneNumber"
in the procedure phoneNumberMentry
).
The four procedures discussed above are implemented in the file
phonenumber.tcl
, contained in the demos
directory. This script also puts them together to build a small
application displaying the following figure:
Here is the relevant code fragment:
package require Mentry set title "Phone Number" wm title . $title # # Get the current windowing system ("x11", "win32", "classic", # or "aqua") and add some entries to the Tk option database # if {[catch {tk windowingsystem} winSys] != 0} { switch $::tcl_platform(platform) { unix { set winSys x11 } windows { set winSys win32 } macintosh { set winSys classic } } } switch $winSys { x11 { option add *Font "Helvetica -12" } classic { option add *background #dedede } } # # Frame .f with a mentry displaying a phone number # frame .f label .f.l -text "A mentry widget for phone numbers:" phoneNumberMentry .f.me -bg white pack .f.l .f.me # # Message strings corresponding to the values # returned by getPhoneNumber on failure # array set msgs { EMPTY "Field value missing" INCOMPL "Incomplete field value" } # # Button .get invoking the procedure getPhoneNumber # button .get -text "Get from mentry" -command { if {[catch { set num "" set num [getPhoneNumber .f.me] } result] != 0} { bell tk_messageBox -icon error -message $msgs($result) \ -title $title -type ok } } # # Label .num displaying the result of getPhoneNumber # label .num -textvariable num . . . putPhoneNumber 1234567890 .f.me focus [.f.me entrypath 0]
Ethernet addresses are usuallly written in the form
"XX:XX:XX:XX:XX:XX"
, where each "X"
is a hexadecimal
digit. The file ethernetaddr.tcl
in the demos
directory contains the steps needed to create and use a multi-entry widget for
displaying and editing Ethernet addresses. It implements the procedures
ethernetAddrMentry
, putEthernetAddr
, and
getEthernetAddr
; the last two invoke the helper procedure
checkIfEthernetAddrMentry
, while the first one is implemented as
follows:
#------------------------------------------------------------------------------ # ethernetAddrMentry # # Creates a new mentry widget win that allows to display and edit Ethernet # addresses. Sets the type attribute of the widget to EthernetAddr and returns # the name of the newly created widget. #------------------------------------------------------------------------------ proc ethernetAddrMentry {win args} { # # Create a mentry widget consisting of 6 entry children of # width 2, separated by colons, and set its type to EthernetAddr # eval [list mentry::mentry $win] $args $win configure -body {2 : 2 : 2 : 2 : 2 : 2} $win attrib type EthernetAddr # # Install automatic uppercase conversion and allow only hexadecimal # digits in all entry children; use wcb::cbappend (or wcb::cbprepend) # instead of wcb::callback in order to keep the wcb::checkEntryLen # callback, registered by mentry::mentry for all entry children # foreach w [$win entries] { wcb::cbappend $w before insert wcb::convStrToUpper \ {wcb::checkStrForRegExp {^[0-9A-F]*$}} } # # In the case of a proportionally-spaced font make the # entries within the widget a bit wider because the # characters A - F need more room than the digits 0 - 9 # if {![font metrics [$win cget -font] -fixed]} { foreach w [$win entries] { $w configure -width 3 } } return $win }
As explained in the last comment above, in the case of a
proportionally-spaced font we increase the width of each entry child of the
multi-entry widget just created to 3. It is important to realize that
this has no impact on the number of characters accepted by these entries, which
remains set to the initial width 2 of the entry children, specified in
the value of the -body
configuration option.
The procedure putEthernetAddr
expects as its first argument a
string of the form "XX:XX:XX:XX:XX:XX"
, where each
"XX"
must be a hexadecimal string in the range 0
-
255
:
#------------------------------------------------------------------------------ # putEthernetAddr # # Outputs the Ethernet address addr to the mentry widget win of type # EthernetAddr. The address must be a string of the form XX:XX:XX:XX:XX:XX, # where each XX must be a hexadecimal string in the range 0 - 255. Leading # zeros are allowed (but not required), hence the components may have more (but # also less) than two characters; the procedure displays them with exactly two # digits. #------------------------------------------------------------------------------ proc putEthernetAddr {addr win} { set errorMsg "expected an Ethernet address but got \"$addr\"" # # Check the syntax of addr # set lst [split $addr :] if {[llength $lst] != 6} { return -code error $errorMsg } # # Try to convert the 6 components of addr to hexadecimal # strings and check whether they are in the range 0 - 255 # for {set n 0} {$n < 6} {incr n} { set val 0x[lindex $lst $n] if {[catch {format "%02X" $val} str$n] != 0 || $val < 0 || $val > 255} { return -code error $errorMsg } } # # Check the widget and display the properly formatted Ethernet address # checkIfEthernetAddrMentry $win $win put 0 $str0 $str1 $str2 $str3 $str4 $str5 }
The procedure getEthernetAddr
raises an error if any entry
child of the given mentry widget is empty. It accepts also entry strings
of length one, but in the return value all components will have exactly two
digits:
#------------------------------------------------------------------------------ # getEthernetAddr # # Returns the Ethernet address contained in the mentry widget win of type # EthernetAddr. #------------------------------------------------------------------------------ proc getEthernetAddr win { # # Check the widget # checkIfEthernetAddrMentry $win # # Generate an error if any entry child is empty # for {set n 0} {$n < 6} {incr n} { if {[$win isempty $n]} { focus [$win entrypath $n] return -code error EMPTY } } # # Return the properly formatted Ethernet address built # from the values contained in the entry children # $win getarray strs return [format "%02X:%02X:%02X:%02X:%02X:%02X" \ 0x$strs(0) 0x$strs(1) 0x$strs(2) 0x$strs(3) 0x$strs(4) 0x$strs(5)] }
We will not show the rest of the code here, because it is very similar to the one presented in the preceding section. The mentry widget for Ethernet addresses looks like in the following figure:
Multi-entry widgets can be used to display and edit date and time in a great variety of formats. The Mentry package contains ready-to-use commands for this purpose:
mentry::dateMentry
creates a
new mentry widget for displaying and editing a date according to the
format passed to the command as a three-character string consisting of
the field descriptor characters "d"
, "m"
, and
"y"
or "Y"
, known from the clock
format
command. Another argument expected by the
procedure is the string (usually a single character) to be displayed in
the labels separating the three components of the date.
mentry::timeMentry
creates a
new mentry widget for displaying and editing a time according to the
format passed to the command as a two- or three-character string
consisting of the following field descriptor characters of the
clock format
command: "H"
or
"I"
, followed by "M"
, and optionally the letter
"S"
. An "H"
as first character specifies
the time format "%H:%M"
or "%H:%M:%S"
(i.e.,
with the hour between 0
and 23
), while the
letter "I"
stands for "%I:%M %p"
or "%I:%M:%S %p"
(i.e., with AM/PM
indicator). The procedure expects the separator string (which is
usually the ":"
character) as another command-line argument.
Before describing the other date- and time-related commands provided by the
Mentry package, let's see how the above two are invoked in the file
datetime.tcl
, located in the demos
directory:
package require Mentry set title "Date & Time" wm title . $title # # Get the current windowing system ("x11", "win32", "classic", # or "aqua") and add some entries to the Tk option database # if {[catch {tk windowingsystem} winSys] != 0} { switch $::tcl_platform(platform) { unix { set winSys x11 } windows { set winSys win32 } macintosh { set winSys classic } } } switch $winSys { x11 { option add *Font "Helvetica -12" } classic { option add *background #dedede } } # # Date and time formats supported by this demo # script and the corresponding field separators # array set dateFmts {0 mdy 1 dmy 2 Ymd} array set dateSeps {0 / 1 . 2 - } array set timeFmts {0 IMS 1 HMS} array set timeSeps {0 : 1 : } # # Choose the date & time formats; don't use the %p field descriptor # for displaying the AM/PM indicator, because it doesn't work on # UNIX if Tcl/Tk 8.4 or higher is used in a non-default locale # wm withdraw . set clockVal [clock seconds] if {[clock format $clockVal -format "%H"] < 12} { set meridian AM } else { set meridian PM } set dateIdx [tk_dialog .choice $title "Please choose a date format" {} -1 \ [clock format $clockVal -format "%m/%d/%y"] \ [clock format $clockVal -format "%d.%m.%y"] \ [clock format $clockVal -format "%Y-%m-%d"]] set timeIdx [tk_dialog .choice $title "Please choose a time format" {} -1 \ [clock format $clockVal -format "%I:%M:%S $meridian"] \ [clock format $clockVal -format "%H:%M:%S"]] wm deiconify . # # Frame .f with mentries displaying the date & time # frame .f label .f.lDate -text "Date: " mentry::dateMentry .f.date $dateFmts($dateIdx) $dateSeps($dateIdx) \ -justify center -bg white frame .f.gap -width 10 label .f.lTime -text "Time: " mentry::timeMentry .f.time $timeFmts($timeIdx) $timeSeps($timeIdx) \ -justify center -bg white pack .f.lDate .f.date .f.gap .f.lTime .f.time -side left
Before displaying the main window, the script lets the user choose one out
of three date and one out of two time formats. The corresponding
command-line arguments passed to mentry::dateMentry
and
mentry::timeMentry
are taken from the arrays
dateFmts
, dateSeps
, timeFmts
, and
timeSeps
.
The following figure corresponds to the choices dateIdx =
2
and timeIdx = 1
:
The Mentry package exports two further commands for date and time mentries:
mentry::putClockVal
outputs
the date or time corresponding to an integer clock value specified as its
first argument to a date or time mentry widget, passed to it as the
second parameter. Like the  clock format
command, the procedure accepts the optional argument pair
-gmt boolean
.
mentry::getClockVal
returns
the clock value corresponding to the date or time contained in the date
or time mentry specified as its first argument. Like the
 clock scan
command, the procedure accepts the
optional argument pairs -base clockValue
and -gmt boolean
. On failure, the
procedure sets the focus to the first erronous entry child, generates an
error, and returns one of the values contained in the following code
fragment taken from the script datetime.tcl
:
# # Message strings corresponding to the values # returned by mentry::getClockVal on failure # array set msgs { EMPTY "Field value missing" BAD "Invalid field value" BAD_DATE "Invalid date" BAD_YEAR "Unsupported year" }
The string "EMPTY"
is returned if any entry child (except
the one containing the seconds) was found to be empty. The value
"BAD"
means a day, month, or hour value of 0
(the hour must not be 0
if the AM/PM indicator is
present). The string "BAD_DATE"
is returned when the
<year, month, day> triple is invalid (note that the procedure is
aware of leap years). Finally, even if this triple is valid, the
conversion (made with the aid of the clock scan
command) can fail because of an unsupported year value (e.g., between
38
and 70
); in this case the string
"BAD_YEAR"
is returned.
The demo script datetime.tcl
invokes the last two commands as
follows:
# # Button .get invoking the procedure mentry::getClockVal # button .get -text "Get from mentries" -command { if {[catch { set dateTime "" set base [mentry::getClockVal .f.date] set clockVal [mentry::getClockVal .f.time -base $base] set dateTime [clock format $clockVal -format "%c"] } result] != 0} { bell tk_messageBox -icon error -message $msgs($result) \ -title $title -type ok } } # # Label .dateTime displaying the result of mentry::getClockVal # label .dateTime -textvariable dateTime . . . set clockVal [clock seconds] mentry::putClockVal $clockVal .f.date mentry::putClockVal $clockVal .f.time focus [.f.date entrypath 0]
To obtain the clock value from the mentry widgets .f.date
and
.f.time
, we first pass the name of the date mentry to the command
mentry::getClockVal
and then use the result as the value of the
-base
option when passing the name of the time mentry to the same
procedure.