Dec 132017
October 26, 1993 issue of PC Magazine. It contains FONEWORD, ALTADD.WK1,.
File V12N18.ZIP from The Programmer’s Corner in
Category Files from Magazines
October 26, 1993 issue of PC Magazine. It contains FONEWORD, ALTADD.WK1,.
File Name File Size Zip Size Zip Type
ALTADD.WK1 3533 1286 deflated
ECHO.ZIP 1230 976 deflated
FONED1.ZIP 1192582 1135898 deflated
FONEWO.DOC 31225 10584 deflated
FONSRC.ZIP 16674 14471 deflated
METER.ZIP 29353 29041 deflated

Download File V12N18.ZIP Here

Contents of the FONEWO.DOC file

FONEWORD.EXE (VERSION 1.0) Copyright (c) 1993 Neil Rubenking
First Published in PC Magazine October 26, 1993 (Utilities)
Create Mnemonic Phone Numbers With FONEWORD


Do you tend to forget phone numbers? Can you remember 1-800-COLLECT
more easily than 1-800-265-5328?

FONEWORD is a Windows 3.1 utility that finds all the mnemonic words
and phrases that can be made from ordinary seven-digit telephone numbers.
It gives you three different sets of possibilities, ranging from perfect
``vanity numbers'' (BIG SHOT), which consist of nothing but actual words,
to word-plus-number combinations (8 DOLLAR), to a list of all the letter
combinations that correspond to a given phone number. While many telephone
numbers yield no useful mnemonics, it's surprising how many do.


FONEWORD is written in Microsoft Visual Basic 3.0, and it uses both
the library file VBRUN300.DLL and a number of .VBX custom control files.
Because it uses VB 3's built-in database functions, it also requires
several additional DLLs for database support.

Because some readers will already have Visual Basic installed on
their systems and others will not, FONEWORD is available in two versions.
If you already have VB Professional Edition 3.0 installed, you need only
the file FONESM.ZIP. Simply download it, unzip it into a subdirectory on
your hard disk, and create a program item (icon) for it in Program Manager.
Otherwise, download the file FONED1.ZIP, unzip it to a floppy disk or a
subdirectory, and run the SETUP program you'll find on that disk or directory.
This SETUP program was created by the Setup Wizard application that comes
with Visual Basic. It installs the necessary support files if they're not
present and updates them if they're present but out of date.

If you want to modify portions of the source code, you'll need Visual
Basic Professional Edition, Version 3.0. (The regular edition lacks some of
the custom controls.)


FONEWORD's main screen consists of a text box, three list boxes,
and their associated controls. To use FONEWORD, simply type a phone
number (numeric input only, up to seven digits) into the text box at top
center and press the button labeled ``Only words'' at the top of the left
list box. FONEWORD will consult its database and attempt to find
combinations of words that utilize all of the digits in the number.

Some telephone numbers generate no items for this list, while others
produce dozens. Of course, it's up to you to decide among PIN-DROP, SHOE-POP,
SIN-EROS, and a host of others. (I did not purge the dictionary of all the
terms some people might find offensive.) Since the digits 0 and 1 have
no letter representations, they are treated as words in their own rights
and so yield combinations such as NUMBER 1.

If the ``Only words'' list produces no suitable vanity numbers, press
the ``Real words'' button above the middle list box. Entries in the middle
list need contain only one real word of two letters or more. Each entry
displays the real word set off by spaces from any unconverted numbers,
such as 7 COME 11.

To list all possible combinations of letters derivable from the phone
number you entered, press the ``All words'' button above the right list
box. With three possible letters for each digit, a full seven-digit phone
number will yield 3 to the power of 7 (2,187) different letter combinations.
The digits 0 and 1 don't correspond to any letters, so if the number
contains any 0s or 1s, there will be fewer combinations. If you're
willing to accept a vanity number with words misspelled, for example,
KATT-INN for a kennel, you may be able to find one by scrutinizing this

Database lookups are relatively slow operations, so filling the list
boxes can take a few minutes. Immediately to the left of each list box
is a gray bar that gives a visual indication of how much processing is
left to do. If you want to halt a lookup before completion, just press
the STOP sign in the upper-right corner. (You'll notice that the sign
is normally dimmed; it brightens and becomes active only during the
processing of one of the list boxes.)

When you've filled one or more of the lists, you can highlight and
copy the selected item(s) to the Clipboard so they can be written to a
text file or incorporated into another document. The list boxes in
FONEWORD work just like the file lists in File Manager: To select a
range of items, click on the first, then hold Shift down as you click
on the last; to select or deselect an individual item without affecting
other items, press Ctrl as you click on it. When you've highlighted all
of your choices, press the Clipboard button at the top of the list to
copy them to the Clipboard.

That's all you need to know to use FONEWORD! Should you need help
while using the program, just select an item from the help menu. For help
with using a particular on-screen element, tab to that element and press
the F1 key.


Visual Basic makes database programming amazingly easy. It takes
just two lines of VB 3.0 code to open a database; another two lines
check whether a given field value exists. I chose to store FONEWORD's
internal database in Paradox format because my informal experimentation
showed that this produced the smallest database file for the word list.
But I could have used any format that Visual Basic supports.

FONEWORD also uses a number of the special controls from VB's
Professional Edition. Specifically, the phone number text box is a Masked
Edit control; the three completion gauges are Gauge controls; and the
four buttons with pictures are graphical Push buttons.

Generating a list of all possible letter combinations is a simple
exercise in recursion--a topic that I'll discuss a little later.
Finding which combinations contain real words is the challenge. A phone
number can contain any word of up to seven letters that has no Q or Z in
it. Fortunately, years ago I compiled an exhaustive word list for my
NAMEGRAM anagram generator. FONEWORD's internal dictionary is a subset
of that list.

In my first crack at programming FONEWORD, I simply imported the word
list directly into the Paradox database FONENUMS.DB so that it contained
a single 7-letter text field that also served as the primary index.
I took each of the up-to-2,187 combinations of letters and checked
whether each of its substrings was a word. A 7-letter text string has
one 7-letter substring, two 6-letter substrings, and so on, for a total
of 28 substrings. With 2,187 combinations times 28 substrings each, the
program might have to perform over 60,000 database lookups! As a result,
its performance was utterly abysmal--too slow for anyone to actually use.

In fact, however, most of those 60,000 database lookups were redundant.
There are actually only 4,098 distinct words. Where does that number come
from? Multiplying 3 possible letters for the first digit times 3 for the
second digit and so on, we see that the number of possible words formed by
a string of n digits is 3 to the n power. Thus, the one 7-letter substring
forms 3 to the power of 7 possible words; the two 6-letter substrings
each form 3 to the power of 6 possible words, and so on, for a total of
4,098. My original algorithm had been looking up most of the possible
words many times. For example, in processing the number 265-5328,
it looked up ``AMJJD'' as a substring of nine different strings,
ranging from ``AMJJDAT'' to ``AMJJDCV.'' My second algorithm carefully
avoided looking up any word more than once. It was better--but it was
still impractically slow.

I found that the solution to improving FONEWORD's performance was to
look at the problem from a completely different angle. As a result, the
current database doesn't contain words at all! In place of each word,
the database stores the string of digits that you would get by typing
the word on your telephone, together with an integer that encodes
instructions for deriving the original word from the digit string.
Using this as the database, it's necessary to look up only the 28
substrings of the original phone number. That's quite an improvement
--going from over 60,000 database lookups to under 100.


Although they are stored as decimal integers, the integer codes stored
in FONEWORD's database are actually numbers in base 3. A single digit in
base 3 can have the value 0, 1, or 2. If it's 0, the corresponding digit
in the phone number string is replaced by the first possible letter for
that digit. If it's 1, the second possible letter is used; if it's 2,
the third. To decode a digit string, therefore, FONEWORD repeatedly
divides the decimal code number by 3 and uses the remainder to determine
which letter to use.

The table in Figure 1 (see below) shows the actual process of decoding
the string ``2222537,'' using the code 1900 (2121101 in base 3). The first
remainder is 1, so we replace the first character with the second of the
three possible letters. The second remainder is 0, so we replace the
second character with the first of the possible letters. This continues
until the string is completely decoded. Figure 2 (See below) shows the
Visual Basic function Decode$, which implements the process described

There's one hitch in this scheme: A given string of digits may well
decode to more than one word. Indeed, from the user's point of view this
is highly desirable. For example, the string ``22737'' has 12 possible
interpretations, including ACRES, BASES, and CAPER. In this case,
therefore, the database should have 12 entries in which the key is
22737, each with a different code. Like many database systems, however,
Paradox does not permit duplicates in the primary key. Eliminating the
key is not an option because doing so would render database lookups
impossibly slow.

FONEWORD solves this dilemma by appending the letters A, B, C, and
so on to successive key entries for the same digit string. Thus the
second entry for ``22737'' is ``22737A,'' the third ``22737B,'' and so
on. Other solutions may be more elegant, but this technique is economical
in terms of database size.

The function NextMatch$, shown in Figure 3 (See below), can be called
repeatedly to return each entry that matches a given digit string. Its
second parameter is a one-character string, C, which the calling function
must set initially to @. (The ASCII code for the @ character comes just
before A.) Each call to NextMatch advances C to the next character.
NextMatch returns the decoded word, or an empty string if there was no
match. It also includes logic to handle one-character digit strings
without accessing the database.

Figure 4 (See below) lists the subroutine FindRealWords, which uses
NextMatch to find real words within the input phone number. FindRealWords
simply looks up each of the 28 possible substrings and reports any word
matches by replacing the substring with the corresponding word, set off
by spaces. With the support functions NextMatch$ and Decode$ in place,
FindRealWords is remarkably simple!


Sooner or later every programmer must grapple with the concept of
recursion. A recursive procedure or function is one that calls itself.
The factorial function is often used as an example of recursion.
The factorial of a positive number n is defined as the product of all
numbers from n down to 1. A recursive definition of this function might
thus be ``The factorial of 1 or 0 is 1. For any other positive value n,
the factorial is n times the factorial of n-1.'' Note that the definition
includes a provision for the recursion process to end; in this case, the
process finishes when n gets down to 1. Every recursive function must
provide such an outlet.

Unlike some early versions of BASIC, VB 3.0 supports recursion, and
FONEWORD depends on that to fill the ``All words'' and ``Only words''
list boxes. The function AllCombos, listed in Figure 5 (See below), takes
a string of digits and generates all possible combinations of letters,
inserting the results in the list box named ListAll. Its first parameter,
S, is the string being worked on, and its N parameter is the current
character position. If N is beyond the end of the string, the AllCombos
function adds the completed string to the list box. Otherwise, for each
of the three possible letters corresponding to the Nth digit, AllCombos
replaces the digit with that letter and passes the modified string back
to itself, adding 1 to the value of N in order to process the next digit.

It may be instructive to trace the operation of AllCombos on the
partial phone number ``28.'' Initially, S is 28 and N is 1. AllCombos
replaces the first digit (2) with the letter A and calls itself again.
This second instance of AllCombos receives the parameters A8 and 2, and
in its turn, it replaces the second digit (8) with the letter T and
calls itself again. This time the parameters are AT and 3. Since 3 is
greater than the string's length, AllCombos adds ``AT'' to the list box
and returns.

The second instance of AllCombos now calls itself twice more, passing
``AU'' and then ``AV'' with the character position 3. After that it returns
to the first instance, which has remained in memory. The first instance
now calls itself, passing ``B8'' and the position 2. By the time this
series of calls finishes, all nine possible combinations are in the list

The function OnlyRealWords, shown in Figure 6 (See Below), uses a
somewhat different type of recursion to find all ``vanity numbers,''
that is, solutions that use all the digits of the phone number.
The function takes two parameters, the input string S and an initially
empty accumulator string SAcc. OnlyRealWords defines a vanity number as
one that can either be represented by a single real word or one that can
be divided into two vanity numbers. Using this recursive definition handles
breaking down the original string into any number of substrings. It would,
for example, translate the number 242-4242 into ``A I A I A I A.''

OnlyRealWords starts by seeing whether string S begins with a real
word of any length from 1 character up to the entire string. For each
real word found, it calls itself, passing the remainder of the input
string as the first parameter and appending the found word to the
accumulator string in the second parameter. If it manages to convert
all the digits of the input string, it combines the accumulator with
the most recently found word and adds the result to the list box.
By using recursion, this function avoids spelling out every possible
partition of the input string into one- and six-letter substrings,
two- and five-letter substrings, and so on.

FONEWORD relies on Visual Basic's built-in Clipboard object to move
text from a list box to the Clipboard. Each list box has an associated
push button with a picture of the Clipboard on it. Pressing one of these
buttons calls the procedure ToClip, shown in Figure 8, passing the
associated list box as a parameter. From the list box, ToClip builds a
text string that contains each selected item, separated by carriage return
and line-feed. It takes only one line of code to clear the contents of the
Clipboard and just one more line to copy the built-up text string into the


Because Windows is not a preemptive multitasking system, any program
can temporarily slow Windows to a dead stop simply by performing a lengthy
task without giving Windows a chance to process messages. Database lookups
are often such lengthy tasks, during which Windows won't respond to messages.
In some languages, the programmer must write a construct called a PeekMessage
loop, which calls several Windows API functions, just to avoid bringing
Windows to its knees. In Visual Basic, however, the problem is handled by
the built-in DoEvents command. The routines AllCombos, FindRealWords, and
OnlyRealWords all include one or more calls to DoEvents. DoEvents calls
come between other statements, and each database lookup is accomplished
by a single statement, so you'll notice that Windows won't respond to
messages during a database lookup.

If a process takes more than a few seconds, a courteous program will
let the user know how the task is progressing. Each of FONEWORD's list
boxes is associated with a Gauge control--a control that fills up with
color to show how much of the task is complete. Of course, it's up to
the programmer to define what ``complete'' means, by setting the gauge's
Max field to a value that indicates the task has been completed.

AllCombos can calculate in advance how many items will be added:
It simply raises the gauge by one unit for each item added.
FindRealWords calculates the number of substrings it must process and
advances the gauge each time it finishes one substring. Since the amount
of time spent on a given substring will vary, this gauge doesn't advance
smoothly. There's no way for OnlyRealWords to evaluate the number of
recursive calls it will make, so it moves the gauge once for each substring
length from 1 to the original string's length.

The user can now see whether a process is half-done, nearly done, or
barely started. If time is short, the user may want to stop the lengthy
routine before it finishes. In FONEWORD, pressing the STOP button at the
upper right stops the current routines. Pressing this button sets a global
variable called Continue to FALSE, and each of the three routines
terminates when Continue becomes FALSE. The STOP button is dimmed when
a lengthy process is not occurring.

The short Before and After Subroutines listed in Figure 8 are called
to set up each of the three lengthy processes and to clean up afterward.
The Before routine clears the appropriate list box and its item-count line.
It sets Continue to true, changes the mouse pointer to an hourglass, and
sets the STOP button's picture fields to a bright red-and-white stop
-sign image. The After routine dims the STOP button, restores the default
mouse pointer, and reports the item count. The call to MessageBeep,
a Windows API function declared in FONEWORD's declarations section,
either beeps the speaker or causes Windows to play the default .WAV
sound file. Note that this beep will sound even if FONEWORD is minimized.


Visual Basic controls have a property called HelpContext ID. If you
associate a help file with your program in the Options|Project dialog,
pressing F1 will bring up the help screen associated with the
HelpContextID of whatever control has the focus. This makes adding
context-sensitive help to a VB application a snap. I used the Windows
Help Magician from Software Interphase to create FONEWORD's help file.
The package's visual orientation makes it much easier to use than the
traditional combination of Word for Windows and the Windows Help

The standard, Microsoft-defined Help menu includes items that show
the help file's contents page, bring up the Search dialog box, display
help on using Help, and present an About dialog box. Visual Basic does
not directly support the first three of these options, so FONEWORD uses
the Windows API function WinHelp, which is defined in its declarations
section. As Figure 9 shows (See below), WinHelp handles all three of
the necessary tasks. Of course, the About FONEWORD dialog box is simply
a second form in the same program.


In almost any other language, FONEWORD would have been a much bigger
program. Code to handle its user interface and database would have to be
designed, developed, and debugged. By contrast, database access in Visual
Basic is completely handled by support DLLs, and the programmer needs to
write only the highest-level code to create the user interface out of
standard elements.

This simplicity comes at a price, however. The database support files
for Visual Basic programs require just short of a megabyte of disk space.
You'll incur that cost the first time you install any VB program that uses
database functions. Any subsequent VB database programs won't add to that
load, though. And since the VB programs are quite small, if you run a lot
of them, this system of shared resources will ultimately mean a savings
in hard disk space. And if FONEWORD is your first such VB application,
I'm sure it won't be your last!

Decoding Digits

String Code Quotient Remainder

2222537 1900 633 1

B222537 633 211 0

BA22537 211 70 1

BAB2537 70 23 1

BABB537 23 7 2

BABBL37 7 2 1

BABBLE7 2 0 2


Figure 1: An entry in FONEWORD's database includes a string of digits
and a code that is used to convert the string into a word.

Complete Listing

Function Decode$ (ByVal S$, ByVal Code%)
' This function receives a string of digits from 2 to 9
' and an integer that tells how to decode those digits
' into a real word. It repeatedly divides the code by
' 3 and uses the remainder as an index into the A array,
' selecting the first, second, or third letter associated
' with the current digit.
Dim N%, TempS$
If (Len(S) = 1) And InStr("01", S) Then
Decode = S
TempS = NullStr
For N = 1 To Len(S)
TempS = TempS + A(Asc(Mid$(S, N, 1)), Code Mod 3)
Code = Code \ 3
Next N
Decode = TempS
End If
End Function

Figure 2: The Decode$ function takes a digit string and converts it
into a word by repeatedly dividing a code by 3 to get the base 3 number
it represents.

Complete Listing

Function NextMatch$ (ByVal S$, C$)
' Called by FindRealWords and OnlyRealWords
' Handles the fact that multiple decodings of the same
' string of digits exist. The first is keyed with the
' digit string itself, and the later ones have A, B,
' C, and so on appended in turn.
Dim Criteria$, Code%
NextMatch = NullStr
If C = "?" Then Exit Function
If Len(S) = 1 Then
' deal with single-digit "words" w/o hitting database
Select Case S
Case "0", "1"
NextMatch = S
Case "2"
NextMatch = "A"
Case "4"
NextMatch = "I"
Case ""
NextMatch = "O"
End Select
C = "?"
If C = "@" Then
Criteria = "Foneword = '" + S + "'"
Criteria = "Foneword = '" + S + C + "'"
End If
MySet.FindFirst Criteria
If Not MySet.NoMatch Then
Code = MySet("Code")
NextMatch = Decode(S, Code)
End If
C = Chr$(Asc(C) + 1)
End If
End Function

Figure 3: The NextMatch$ function checks the database for entries
matching the input string, returning either the next matching word
or a null string.
Complete Listing

Sub FindRealWords ()
' Called when you press the CommandReal button.
' Considers every substring of the phone number that's
' at least MinWord in length. If it's in the database,
' decodes it into a word and adds the result to the list.
' Then it checks for other words made from the same
' digits. The key values for these other words will
' be the same as the original number with A, B, C
' and so on appended in turn.
Dim Start%, Num%, vLen%, Code%
Dim S$, SPart$, SDecode$
Dim Char As String * 1
vLen = Len(PhoneEdit.ClipText)
GaugeReal.Max = 1
For Num = MinWord To vLen
For Start = 1 To (vLen + 1 - Num)
GaugeReal.Max = GaugeReal.Max + 1
Next Start
Next Num
GaugeReal.Value = 0
For Num = MinWord To vLen
For Start = 1 To (vLen + 1 - Num)
GaugeReal.Value = GaugeReal.Value + 1
If Not Continue Then Exit Sub
SPart = Mid$(PhoneEdit.ClipText, Start, Num)
Char = "@"
SDecode = NextMatch(SPart, Char)
Do While Len(SDecode) <> 0
If Not Continue Then Exit Sub
S = NullStr
If Start > 1 Then S = Mid$(PhoneEdit.ClipText, 1, Start - 1) + " "
S = S + SDecode
If Start + Num <= vLen Then S = S + " " + Mid$(PhoneEdit.ClipText,
Start + Num)
ListReal.AddItem S
SDecode = NextMatch(SPart, Char)
Next Start
Next Num
GaugeReal.Value = GaugeReal.Value + 1
End Sub

Figure 4: FindRealWords repeatedly calls NextMatch$ (Figure 3 (See above))
to identify real words and then lists all real words found within the input
phone number.
Complete Listing

Sub AllCombos (ByVal S$, ByVal N%)
' Called when button CommandAll is clicked
' Recursive function. Replaces the Nth digit of S with
' each of the three possible letters, then calls itself
' to handle the N+1th digit for each. When it passes
' the LAST digit, it records the completed combination
' by adding it to a list box.
Dim Ch%
If Not Continue Then Exit Sub
If N > Len(S) Then
ListAll.AddItem S
GaugeAll.Value = ListAll.ListCount
Ch = Asc(Mid$(S, N, 1))
If (Ch >= 50) And (Ch <= 57) Then
Mid$(S, N, 1) = A(Ch, 0)
AllCombos S, N + 1
Mid$(S, N, 1) = A(Ch, 1)
AllCombos S, N + 1
Mid$(S, N, 1) = A(Ch, 2)
AllCombos S, N + 1
AllCombos S, N + 1
End If
End If
End Sub

Figure 5: The recursive function AllCombos calls itself repeatedly
to generate all possible combinations of letters found in a given
phone number.

Complete Listing

Sub OnlyRealWords (ByVal S$, ByVal SAcc$)
' Called when you press the CommandOnly button
' Checks each prefix of the passed string to see if it's
' a word. If so, adds the decoded word to the accumulator
' string SAcc and calls itself recursively to handle the
' remainder of the string. Only if the string is entirely
' converted to words does it add the result to the list.
Dim N%, SPart$, SDecode$
Dim Char As String * 1
If Not Continue Then Exit Sub
For N = 1 To Len(S)
' Only advance the gauge for the first instance
If Len(SAcc) = 0 Then GaugeOnly.Value = GaugeOnly.Value + 1
If Not Continue Then Exit Sub
SPart = Mid$(S, 1, N)
Char = "@"
SDecode = NextMatch(SPart, Char)
Do While Len(SDecode) <> 0
If Not Continue Then Exit Sub
If N = Len(S) Then
ListOnly.AddItem Mid$(SAcc + " " + SDecode, 2)
OnlyRealWords Mid$(S, N + 1), SAcc + " " + Left$(SDecode, N)
End If
SDecode = NextMatch(SPart, Char)
Next N
' Only advance the gauge for the first instance
If Len(SAcc) = 0 Then GaugeOnly.Value = GaugeOnly.Value + 1
End Sub

Figure 6: The OnlyRealWords subroutine locates combinations of words
that completely replace the digits of the original phone number.

Complete Listing

Sub ToClip (L As ListBox)
' Called when you press one of the clipboard buttons
' Copies the selected items from the associated list
' box to the clipboard.
Dim N%, Text$
Text = NullStr
If L.ListCount = 0 Then Exit Sub
For N = 0 To L.ListCount - 1
If L.Selected(N) Then
Text = Text + L.List(N)
Text = Text + Chr$(13) + Chr$(10)
End If
Next N
If Len(Text) = 0 Then
MsgBox "No items are selected", 0
Clipboard.SetText Text
End If
End Sub

Figure 7: Visual Basic's Clipboard object makes it easy for this
routine to copy selected items from a list box to the Windows Clipboard.

Complete Listing

Sub Before (Lis As ListBox, Lab As Label)
Continue = True
Lab.Caption = NullStr
Form1.MousePointer = 11
PushStop.PictureUp = ImageStopUp.Picture
PushStop.PictureDown = ImageStopDn.Picture
End Sub
Sub After (Lis As ListBox, Lab As Label)
PushStop.PictureUp = ImageDiStopUp.Picture
PushStop.PictureDown = ImageDiStopDn.Picture
Form1.MousePointer = 0
Lab.Caption = Lis.ListCount + " Items"
MessageBeep (0)
End Sub

Figure 8: Since FONEWORD's search routines can take several minutes,
the routines Before and After set up for lengthy processing and clean
up afterward.
Complete Listing

Sub HelpMenu_Click (Index As Integer)
' Note that WinHelp and WinHelpByNum are declared in
' the declarations section, to give this VB program
' access to the Windows API function WinHelp.
Dim Success%
Select Case Index
Case 101
Success = WinHelpByNum(Form1.hWnd, App.HelpFile, HELP_CONTENTS, 0)
Case 102
Success = WinHelp(Form1.hWnd, App.HelpFile, HELP_PARTIALKEY, "")
Case 103
Success = WinHelpByNum(Form1.hWnd, App.HelpFile, HELP_HELPONHELP, 0)
Case 105
End Select
End Sub

Figure 9: FONEWORD's Help menu choices rely on the Windows API function
WinHelp to process the standard Windows Help menu choices.

 December 13, 2017  Add comments

Leave a Reply