A Visual Basic Form Generator

Speeding up the Windows development process

Wei Xiao

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.

The Program

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:

  1. The command section is processed, and the header of the form is generated. Three stacks are used to store arguments of the FormSet, ControlSet, and ComboSet commands. Some flags and values are also stored in this step.
  2. The rest of the form-description file is scanned. For underscores, text boxes are generated; for strings that start with an asterisk (*), check boxes are generated; for strings that start with *@, combo boxes are generated. The rest of the text is converted to labels. For each control, the stack that stores the arguments of ControlSet is dumped as part of the attribute list for that control. Thus, ControlSet sets attributes for all the controls on the form.
  3. The Form_Load() function is written to the end of the .FRM file. This function includes initialization statements for the summary variables if the Summary command is in the command section. It also includes the initialization code for combo boxes if they are used in the form.
The syntax of the form-description file can be extended into a form-description language in which you can define new objects and write application code. Based on the same form-description file, the form generator may also generate forms and code for other GUI development environments. Visual GUI designers (like the one that comes with Visual Basic) are great for beginners, but they may not fully satisfy the needs of professional programmers. The best solution may be to use the visual GUI designer for designs that need immediate interaction, and to use a form generator to eliminate tedious drawing jobs.

Figure 1: Simple form-description file.

Name _______________
Age ______________
Address ____________

Figure 2: The beginning of the .FRM file for the file in Figure 1.

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"

Figure 3: Form-description file with combo boxes.

{
   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

Figure 4: Sample command section.

{
FormName: MF1
     FormCaption: Optional Coverage
     FormSet: MDIChild = -1
     FormSet: BorderStyle = 0
     ControlSet: FontBold = -1
     LeftMargin: 100
     Summary
}

Example 1 (a) Form-description file; (b) generated Visual Basic form.

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;
}

Copyright © 1995, Dr. Dobb's Journal