Wei is currently a research assistant in the computer-science department at the University of Wisconsin-Madison and can be reached at wei@cs.wisc.edu.
At the commercial auto-insurance company where I work, I was assigned the task of developing a Windows-based insurance-policy system using Visual Basic (VB). Because of the wide range of options available on the insurance policies, hundreds of pages of documents had to be converted to VB forms. It took hours to draw one form using the interface designer that came with Visual Basic, because there were usually hundreds of labels and text boxes to draw. Changing the font or character size for all the controls also took a lot of effort. Realizing that end users can easily enter the form using a text editor (with certain special symbols to specify fill-in blanks and check boxes), I wrote a program to parse a text file and generate a VB form that can be loaded into the project file directly. An end user can also scan the paper form and use an OCR program to generate the text file. With this approach, it takes less than ten minutes to edit the form's text file to make sure the OCR gets it right. My form-generator program handles text boxes, combo boxes, check boxes, and form attributes such as font, margin, and color.
This article describes how I designed the form-description-file format and the form-generator program. The format of the form-description file is simple enough for end users to edit, yet powerful enough for professional programmers who need to quickly generate GUIs with end-user involvement. I'll also discuss how to combine the benefits of both the WYSIWYG interface designer and the form generator.
The form generator is a C program that converts a text file into a VB form file (.FRM file). The program converts a text string in the text file into a label in the .FRM file and converts a series of underscores (often used as fill-in blanks) into a text box. If there are hundreds of labels and text boxes in the form, it is easier and faster to enter or modify the text file rather than to draw the form using the Visual Basic interface designer. The end user can edit the text file or scan the paper forms. When I converted the insurance-policy forms into VB forms, using the form generator was ten times faster than drawing the forms manually.
A form can be as simple as Figure 1, which has three fill-in blanks and three words describing the information requested. When this form is converted to a VB .FRM file, there will be three labels and three text boxes. For the format of the .FRM file, see the Visual Basic Programmer's Reference Manual. The form-generator program reads the text file in Figure 1(the "form-description file") and generates the VB .FRM file; see Figure 2. The .FRM file can be loaded into a Visual Basic project directly.
Check boxes and combo boxes are often used in forms. The form-description file requires a special symbol to tell the form-generator program where those special boxes are. I use an asterisk (*) to denote a check box. For example, the line in Example 1(a) in a form-description file becomes the line in Example 1(b) in the generated VB Form.
Combo boxes need special symbols to specify their locations and default options. In the beginning of the form-description file, a command section is needed which includes the command that defines the options of a combo box. The command section is enclosed in a pair of braces, { }. The command I use is ComboSet. For example, Figure 3 defines two combo-box types: "car-make" with the options "Ford," "Honda," and "Chevy;" and "fruit-type" with the options "apple," "orange," and "grape." The ComboSet command takes one argument. If the argument starts with an @ character, the string after the @ character is the type name of a combo box. Otherwise, it is one of the options of a combo box. The type name of the combo box always immediately follows the box's options. This syntax simplifies the form-generator program. In the form-description file, a combo box is denoted by *@, followed by the type name. The form defined by the file in Figure 3 has two questions, with combo boxes providing default answers.
The command section contains a few other commands: FormName and FormCaption specify the form's name and caption, respectively. FormSet and ControlSet specify some of the form's properties and all of its controls. In Figure 4, the form is set to be an MDIChild, with a border style of 0 (NONE), and with all the controls on the form in bold fonts. The LeftMargin command specifies how much space is on the left side of the form. Notice that all these commands are optional. Even the command section is optional. Users do not have to learn the commands until they need to use them. The last command in Figure 4, Summary, puts appropriate values for five variables in the Visual Basic Form_Load() procedure: nCheckBox, nTextBox, nComboBox, nFormWidth, and nFormHeight, so that programmers can write some routines using these properties of the generated form.
Listing One is a C implementation of the form-generator program. It scans the form-description file twice. (An executable version of the program, plus associated forms, is available electronically; see "Availability," page 3.) The first pass counts the number of lines and maximum line width in the form-description file in order to calculate the form width and height, because these two values are needed in the beginning of the VB .FRM file. The second pass generates the .FRM file as it scans the form-description file, following these three steps:
Figure 1: Simple form-description file.
Figure 2: The beginning of the .FRM file for the file in Figure 1.
Figure 3: Form-description file with combo boxes.
Figure 4: Sample command section.
Example 1 (a) Form-description file; (b) generated Visual Basic form.
Copyright © 1995, Dr. Dobb's JournalName _______________
Age ______________
Address ____________
VERSION 2.00
Begin Form form1
Caption = "form1"
Width = 2100
Height = 1275
Top = 100
Left = 100
Begin Label Label1
Index = 0
Caption = "Name"
FontUnderline = 0 'False
FontBold = 0 'False
FontItalic = 0 'False
FontName = "Courier New"{
ComboSet: apple
ComboSet: orange
ComboSet: grape
ComboSet: @fruit-type
ComboSet: Ford
ComboSet: Honda
ComboSet: Chevy
ComboSet: @car-make
}
What kind of fruit do you like? *@fruit-type
What kind of car do you drive? *@car-make{
FormName: MF1
FormCaption: Optional Coverage
FormSet: MDIChild = -1
FormSet: BorderStyle = 0
ControlSet: FontBold = -1
LeftMargin: 100
Summary
}Listing One
/* Visual Basic Form Generator -- Wei Xiao -- 1994 COPYRIGHT -- MS C700 */
#include <stdio.h>
#include <string.h>
#define DEF_ClientWidth 8400 /* default window width
80 column, 8.25 Courier New,
105 twips per char*/
#define DEF_ClientHeight 6435 /* default window height*/
#define DEF_ControlHeight 255 /* default control height*/
#define LINE_LEN_MAX 200 /* maximum line length*/
#define DEF_LINE_LEN 80
#define DEF_PAGE_LEN 25
#define FORM_NAME_LEN 30 /* maximum form name length*/
#define FORM_CAPTION_LEN 80 /* " form caption "*/
#define DEF_FontBold 0 /* 0 = false */
#define DEF_FontItalic 0
#define DEF_FontName "Courier New"
#define DEF_FontSize 8.25
#define DEF_FontStrikethru 0
#define DEF_FontUnderline 0
#define DEF_CHECK_BOX_ADJ 200
int nCheckBox=0, /*check box count*/
nTextBox=0, /*text box count*/
nLabel=0, /*label count*/
nLine=0, /*line count */
nPerChar, /*average char width in twips*/
nPerLine, /*Line Height in twips */
nComboBox=0, /*number of combo boxes */
fSummary=0, /* 1 if Summary command appears */
nLeftMargin=0, /*leftmargin in twips*/
fFormNameSpec = 0, /* 1 if form name is on command line */
firstLine=1, /* 1 if the first line has not been read*/
fHeaderNotWritten=1; /* assigned 0 in write_header() */
nNumOfLines=DEF_PAGE_LEN, /* total # of lines in input file */
nNumOfCmdLines=0, /* total # of lines in commad part */
nMaxLineLength=DEF_LINE_LEN;/* maximum line length in input file */
long nFormWidth,
nFormHeight; /*dimention of the VB form */
char sFormName[FORM_NAME_LEN]= "form1";
char sFormCaption[FORM_CAPTION_LEN] = "form1";
char buf[LINE_LEN_MAX];
char buf2[LINE_LEN_MAX];
char sFrmName[80]; /* form name */
FILE *fIn, *fOut;
struct stack {
char * pStr;
struct stack *next;
} *stForm = 0, /* form settings */
*stCntl = 0, /* control settings */
*stCobDef=0, /* combo box type definitions items for the
same type of combo are pushed into the
stack followed by the type name. Different
types of combo are pushed into the stack
one after another. See form.txt for a
sample of definition*/
*stCobList=0; /* combo box control types on the form the
combo box on the form are indexed by
0, 1, 2, ... Their types are stored in this
order in a stack, with last combo on top*/
push_stack(char *buf, struct stack**pst) /* & *st is better :-) */
{
char *p;
struct stack *st1;
if((p=strdup(buf)) &&
(st1=(struct stack *)malloc(sizeof(struct stack)))) {
st1->pStr = p;
st1->next = *pst;
*pst = st1;
}else
printf("heap space out:%s not processed",buf);
}
dump_stack(struct stack*st) /*for control setting or form
setting only */
{
while(st){
fprintf(fOut," %s\n",st->pStr);
st = st->next;
}
}
dump_combos(struct stack *st) /* for combos */
{
int n;
n=nComboBox;
while(st) {
fprintf(fOut," ' %s\n",st->pStr);
dump_combo(stCobDef,st,--n);
st = st->next;
}
}
dump_combo(struct stack *stCobDef, struct stack *st,int n)
{
while (stCobDef && (strcmp(stCobDef->pStr+1,st->pStr)))
stCobDef = stCobDef->next;
if (stCobDef)
stCobDef = stCobDef->next;
while (stCobDef && (stCobDef->pStr[0]!='@' )) {
fprintf(fOut, "Combo1(%d).AddItem \"%s\"\n",n, stCobDef->pStr);
stCobDef= stCobDef->next;
}
}
char * trail_sp(char *p) /* take out spaces at the end of string*/
{
int n;
n=strlen(p)-1;
while((n>=0) && isspace(p[n]))
p[n--]='\0';
return p;
}
print_pos(char *p, int left,int wAdj) /* p: the string left: starting pos
wAdj: adjustment for strlen(p) */
{
fprintf(fOut," FontBold = 0 'False\n");
fprintf(fOut," FontItalic = 0 'False\n");
fprintf(fOut," FontName = \"Courier New\"\n");
fprintf(fOut," FontSize = 8.25\n");
fprintf(fOut," FontStrikethru = 0 'False\n");
fprintf(fOut," Width = %d\n", strlen(p)*nPerChar+wAdj);
fprintf(fOut," Top = %d\n", (nLine)*nPerLine);
fprintf(fOut," Height = %d\n", DEF_ControlHeight);
fprintf(fOut," Left = %d\n", left*nPerChar+nLeftMargin);
dump_stack(stCntl);
fprintf(fOut," End\n");
}
proc_form(char *buf) /* processing text part*/
{
char *p,*p1;
int n;
p = buf;
while(*p) {
p1=p;
switch (*p) {
case ' ':
p+=strspn(p," ");
break;
case '_':
n=strspn(p,"_");
memcpy(buf2,p,n);
buf2[n]='\0';
p+=n;
fprintf(fOut," Begin TextBox Text1\n");
fprintf(fOut," BorderStyle = 0 \n");
fprintf(fOut," Index = %d\n",nTextBox++);
fprintf(fOut," FontUnderline = -1 \n");
fprintf(fOut," Text = \"%*s\"\n",strlen(buf2)," ");;
print_pos(buf2,p1-buf,0);
break;
case '*':
n = strcspn(p+1, "~_*");
n++;
memcpy(buf2,p,n);
if (*(p+n)=='~') p++;
buf2[n]='\0';
p+=n;
if ((n>2) && (buf2[1]=='@')) {
trail_sp(buf2+2);
fprintf(fOut," Begin ComboBox Combo1\n");
push_stack(buf2+2,&stCobList);
fprintf(fOut," Text = \"%s\"\n", buf2+2);
fprintf(fOut," Index = %d\n",nComboBox++);
print_pos(buf2,p1-buf,DEF_CHECK_BOX_ADJ);
break;
}else {
trail_sp(buf2+1);
fprintf(fOut," Begin CheckBox CheckBox1\n");
fprintf(fOut," Caption = \"%s\"\n", buf2+1);
fprintf(fOut," Index = %d\n",nCheckBox++);
fprintf(fOut," FontUnderline = 0 'False\n");
print_pos(buf2,p1-buf,DEF_CHECK_BOX_ADJ);
break;
}
default:
n = strcspn(p, "_*~");
memcpy(buf2,p,n);
if (*(p+n)=='~') p++;
p+=n;
buf2[n]='\0';
if (n-- >0)
while(n>=0 && (buf2[n] == ' '))
buf2[n--]='\0';
fprintf(fOut," Begin Label Label1\n");
fprintf(fOut," Index = %d\n",nLabel++);
fprintf(fOut," Caption = \"%s\"\n", buf2);
fprintf(fOut," FontUnderline = 0 'False\n");
print_pos(buf2,p1-buf,0);
break;
}
}
nLine++;
}
write_header()
{
fHeaderNotWritten =0;
printf("%d Lines, %d Command lines, %d Chars per line max\n",
nNumOfLines, nNumOfCmdLines, nMaxLineLength);
fprintf(fOut, "Begin Form %s\n",sFormName);
fprintf(fOut, " Caption = \"%s\"\n",sFormCaption);
nFormWidth= (long) DEF_ClientWidth* (long) nMaxLineLength/DEF_LINE_LEN;
fprintf(fOut, " Width = %d\n",nFormWidth);
nFormHeight= (long) DEF_ControlHeight*
(long)(nNumOfLines-nNumOfCmdLines + 2);
fprintf(fOut, " Height = %d\n",nFormHeight);
fprintf(fOut, " Top = 100\n");
fprintf(fOut, " Left = 100\n");
dump_stack(stForm);
}
int proc_command(char *buf) /* command part*/
{
char *vars, *value;
if (!buf)
return 0;
nNumOfCmdLines++;
value = buf + strcspn(buf, ":") + 1;
trail_sp(value);
vars= strtok(buf, " :");
if (vars){
if (!strcmp(vars,"}")){
sFormName[FORM_NAME_LEN -1 ] ='\0';
sFormCaption[FORM_CAPTION_LEN -1] = '\0';
write_header();
return 1;
}
if (!fFormNameSpec) {
if (!_stricmp(vars, "FormName"))
strncpy(sFormName,value,FORM_NAME_LEN);
else if (!_stricmp(vars, "FormCaption"))
strncpy(sFormCaption, value, FORM_CAPTION_LEN);
}
if(!_stricmp(vars, "FormSet"))
push_stack(value, &stForm);
else if (!_stricmp(vars,"ControlSet"))
push_stack(value, &stCntl);
else if (!_stricmp(vars,"ComboSet"))
push_stack(value + strspn(value," "), &stCobDef);
else if (!_stricmp(vars,"Summary"))
fSummary =1;
else if (!_stricmp(vars,"LeftMargin"))
nLeftMargin=atoi(value);
else if (!_stricmp(vars,"CharWidth"))
nPerChar = atoi(value);
else if (!_stricmp(vars,"LineHeight"))
nPerLine = atoi(value);
}
return 0;
}
fatal(char *msg)
{
perror(msg);
exit(1);
}
count_lines()
{
fgets(buf, LINE_LEN_MAX-1,fIn);
nNumOfLines =0;
nMaxLineLength = 0;
while (! feof(fIn)) {
nNumOfLines++;
buf[LINE_LEN_MAX -1]='\0';
if (nMaxLineLength < strlen(buf))
nMaxLineLength = strlen(buf);
fgets(buf, LINE_LEN_MAX-1,fIn);
}
}
main(int argc, char *argv[])
{
char *rest;
int fFormPart = 0;
nPerChar = DEF_ClientWidth/DEF_LINE_LEN;
nPerLine = DEF_ControlHeight;
if (argc<3){
printf("Usage: %s <form description file> <VB form file name> [options] \n",argv[0]);
printf("Options: <form name>, -f fast mode, ");
exit(1);
}
if (argc==4){
strncpy(sFormName, argv[3], FORM_NAME_LEN);
sFormName[FORM_NAME_LEN -1] = '\0';
strncpy(sFormCaption, argv[3], FORM_CAPTION_LEN);
sFormName[FORM_CAPTION_LEN -1] = '\0';
fFormNameSpec = 1;
}
if ((fIn=fopen(argv[1],"rt")) == NULL)
fatal(argv[1]);
count_lines();
if (fseek(fIn, 0L, SEEK_SET))
fatal("fseek Input");
if ((fOut=fopen(argv[2],"wt")) == NULL)
fatal(argv[2]);
fprintf(fOut,"VERSION 2.00\n");
fgets(buf, LINE_LEN_MAX-1,fIn);
while (! feof(fIn)) {
if(rest=strchr(buf,'\n'))
*rest = '\0';
if (firstLine && (strcmp(buf,"{"))) {
fFormPart = 1;
write_header();
}
firstLine = 0;
if (! fFormPart) {
if (proc_command(buf))
fFormPart = 1; /* form started */
}else
proc_form(buf);
fgets(buf, LINE_LEN_MAX-1,fIn);
}
if (fHeaderNotWritten)
write_header();
fprintf(fOut, "End\n");
fprintf(fOut, "Sub Form_Load()\n");
if (fSummary){
fprintf(fOut, "nTextBox = %d\n",nTextBox);
fprintf(fOut, "nComboBox = %d\n",nComboBox);
fprintf(fOut, "nCheckBox = %d\n",nCheckBox);
fprintf(fOut, "nFormHeight = %d\n",nFormHeight);
fprintf(fOut, "nFormWidth = %d\n",nFormWidth);
}
if (nComboBox)
dump_combos(stCobList);
fprintf(fOut, "End Sub\n");
fclose(fIn);
fclose(fOut);
printf("%d Labels, %d TextBoxes, %d Checkboxes, %d Combos\n",
nLabel,nTextBox,nCheckBox, nComboBox);
return 0;
}