Mentry Programmer's Guide

by

Csaba Nemethi

csaba.nemethi@t-online.de


Contents

Overview

Examples

Start page


Overview

What is Mentry?

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.

How to get it?

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.

How to install it?

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.

How to use it?

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:

Contents     Start page


Examples

A mentry widget for phone numbers

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:

Phone Number - Please load this image!

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]

A mentry widget for Ethernet addresses

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:

Ethernet Address - Please load this image!

Using mentry widgets for date and time

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:

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:

Date & Time - Please load this image!

The Mentry package exports two further commands for date and time mentries:

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.

Contents     Start page