Classic Computer Magazine Archive A.N.A.L.O.G. ISSUE 78 / NOVEMBER 1989 / PAGE 7

C-manship:

A Complete GEM Application, Part 4

by Clayton Walnum

    Believe it or not, there are many people who prefer to do things the old-fashioned way, people who despise such newfangled contraptions as menu bars and other mousedriven devices that make them lift their chubby fingers from the keyboard. You and I, of course, are great fans of GEM, but whenever we design a program, we have to remember that not everyone shares our good taste. Put simply, programs should have keyboard alternatives whenever possible, especially for selecting functions from a menu bar.
    Needless to say, MicroCheck ST provides keyboard selection for every function in the menu bar. The user who considers the ST's mouse a furless rodent unworthy of his touch may type a Control-key combination to select any function he desires. This month, we'll be looking at the portion of MicroCheck STs source code that handles the Control-key selections. We'll also look at the routines that allow the user to begin a new account.

Compiling
    Listing 1 is this month's portion of the MicroCheck ST source code. Add it to the combined source code from the previous installments, then delete the handle_keys() and do_newacct() stubs from the resulting file.
    After compiling the program, you'll find that you can now select functions from the menu bar by pressing a Control-key combination. For example, pressing Control-Q will exit the program.
    Pressing Control-N, on the other hand, will activate the "New" selection of the File menu. A dialog box will appear, asking for your name, address and account balance, after which a second dialog box will ask for the filename you wish to use for the account. This filename can be up to six characters long. When you've typed in the filename, MicroCheck ST will save the information you typed in the new-account dialog box to a file with an MCK extension and will create all the monthly data files for your new account.
    Now let's take a closer look at how all this works.

Function handle_keys()
    If you look at the selections in the menu bar, you'll see that each has a single letter next to it. This is the key to press along with Control in order to select that function from the keyboard. But just having the letters on the menu isn't enough, of course. We have to retrieve the key presses from the keyboard (which we do by watching for MU_ KEYBD events with evnt_multi()), and when we get a Control-key combination, we have to route it to the right portion of the program.
    This is handled by the function handle_keys(), which does much the same work as do_menu(), except that we're using Control-key values in the switch statements rather than a menu message. Another major difference is that we're using the loaded, search and canceling flags to determine which menu functions are active. We didn't have to do this in do_menu() because inactive menu selections are grayedout and are not selectable by the user. Notice that in handle_keys(), we are highlighting the appropriate menu title with menu_tnormal(), just as we did in do_menu(). This tells the user which menu he is working with.
    All of the values for the Control-key combinations in handle_keys() are defined at the top of the program. (That portion of the code was presented in the July '89 installment of C-manship.)

Function do_newacct()
    Whenever the "New" selection of the File menu is selected or a Control-N is typed, the function do_newacct() is called. This function brings up the dialog box for entering the information needed to start a new MicroCheck ST account.
    First, we call clear_newacct() to make sure any information that was previously entered into the dialog is erased. Next, we call up the dialog box in the usual way (see the C-manship in the May '87 ST-LOG) and activate it with a call to form_do().
    The variable choice will contain the number of the object used to exit the dialog. If this object is the OK button (NEWOK), we call the function check_newacct() to make sure all the information in the dialog is filled in. If the dialog has empty fields (okay is FALSE), we redraw the dialog (the dialog is still on the screen, but the buttons haven't been redrawn to their deselected state) and loop back to form_do() in order to let the user try again.
    If the user clicked the OK button and the dialog was properly filled in, we call newacct_file(), which will get a filename from the user for the new account and call the functions that will write the newaccount information out to the disk.
    If the user clicked the cancel button (NEWCANCL), we clear the dialog and close it, then go back to wait for another event.

Function check_newacct()
    This function simply checks to see that none of the fields in the dialog has been left empty. We use the flag okay to communicate this information back to the calling function. First, we set okay to TRUE. Then we use a for loop to cycle through the editable text objects in the dialog, checking that none is empty. (An empty field will begin with a "@" .
    Note that should you ever use this method in any of your own programs, the objects you're checking must have been created sequentially, all at the same time. Otherwise, you can't use a for loop to check the objects. If an empty field is found, okay is set to FALSE and an alert box warns the user that he must complete the form.

Function newacct_file()
    Most of this function is dedicated to getting a filename from the user and combining it with the right path specification.
    First, we get the address of the string in the filename dialog box and clear it with a null. (With dialog boxes, you can clear a string with the "@" or by the usual null character.) Next, we zero out the string filename and call up the dialog box. If the user exits the dialog with the OK button (FILEOK), we call check_file() to be certain a filename has been entered. If it hasn't, we loop back to let the user try again.
    If the user entered the filename properly, we retrieve it from the dialog, copy it into acct_name (a string that will be used in the window's title bar), then add the complete pathname and the MCK extension. We then open the file in binary-write mode, and a call to write_new_info() writes all the account information to the file, as well as creates the monthly data files.
    A call to open_acct() actually opens the account so the user can enter checks if he wishes. (Note that open_acct() is represented only by a stub at this point; so when you run this month's portion of the program, although you can create a new account, the account cannot actually be opened.)
    If the user exited the dialog by clicking on the CANCEL button (FILECANC), we exit the dialog without starting a new account.

Function clear newacct()
    This function simply steps through each of the editable text fields in the new-account dialog box, clearing them of whatever information they may already contain.

Function get_tedinfo_ str()
    In order to handle dialog boxes with editable text fields, it's necessary you know how to handle TEDINFO structures (see the C-manship in the May '87 ST-LOG). This function simplifies the process of getting the address of a string in an editable text field. Simply pass it the address of the tree and the number of the object, and it'll return the address of the string.

Function check_file()
    In order to open a file for a new account, we must, of course, have a legal filename. The validation string for the dialog's editable text field will make sure the user enters only legal filename characters, but it's up to us to make sure that the field is filled in. This function simply checks the object FILENAME to be sure it's not empty. If it is empty, we scold the user with an alert box.

Function no_decimal()
    In MicroCheck ST, we have to deal with numbers in two ways. The user, since he is entering amounts as dollars and cents, needs to use decimal numbers; that is, he needs to enter his values with dollars on the left of a decimal point and cents on the right. Unfortunately, this means using floating point numbers, which are infamously inaccurate due to the rounding operations performed when calculating with these numbers.
    In order to avoid rounding errors, MicroCheck ST calculates not with both dollars and cents, but only with cents. For MicroCheck, $100 isn't 100.00, but rather 10,000. So every time the user enters a value, we need, as the first step to making the conversion from dollars to cents, to combine both the dollar and cent portions of the number.
    The function no_decimal() accomplishes that task using conventional stringhandling techniques. The string to be converted will have one of three formats: dollars and no cents, dollars and cents, or cents and no dollars. The strings, as retrieved from the dialog, will take one of the forms shown in the first column of the chart below (none of the strings contains trailing spaces):

String
Actual value After no-decina()

99
$   99.08
   9908
99   99
    99.99
   9999
9999999
 99999.99
   9999999
     9
      .90
   90
     99
      .99
   99

    The strings, when they are retrieved from a dialog box, contain no decimal points; instead, dollars and cents are separated by spaces. If the entire field is full, there are no spaces at all. Without spaces delimiting the two numbers, you have to know the field length in order to separate the dollars and cents; in this case, dollars can be up to five digits, and cents, of course, can be up to two.
    The chart above shows the string obtained from the dialog, the value it represents and the value that no_decimal() will return, respectively. Note that the value returned is the number of cents, but it's still in string-not numeric-form.

Function str_to_long()
    Once we have the number of cents in string form, we need to convert it to longinteger form. The function str_to_long() handles this by multiplying each digit in the string times its corresponding value in the pwrs[] array. Each product is added to num until the end of the string is reached. The long integer num will contain the final result.

Function write_new_info()
    In order to begin a new account, MicroCheck ST must write out the information retrieved from the new-account dialog to the MCK file, as well as create each of the 13 monthly data files. These files will be named file0.DAT through file12.DAT, where "file" is the filename that was entered into the filename dialog box.
    All this is accomplished by write_new_ info(). First, we write out the user's name and address to the file we opened back in newacct _file() (acctfile). Then we retrieve the account balance, convert it to long integer and write it out to the same file.
    The last step is to create the 13 monthly data files. (Yes, I know there are only 12 months. The extra file, the one numbered "0," is storage for uncanceled checks from the previous year, necessary whenever the "new year" function is used.)
    We use a for loop to repeat the filecreation process 13 times. The loop variable, x, is used as the file number and is tacked onto the end of the filename the user typed into the filename dialog box. The complete pathname is tacked onto the filename, as is the extension ".DAT." When the filename is complete, we open the file (binary) and write out a zero, after which the file is closed. (The first word [two bytes, that is] of the monthly files is the number of transactions in the file; since a new file has no transactions, we start off with a zero.)

Conclusion
    That finishes things up for another month. If anything I've described in this or the past few installments of C-manship doesn't make sense to you, you should review the topic in question. If you want to write full-GEM applications, you need to know all this stuff!

Clayton

    Clayton Wulnum is the exutive editor of ANALOG Computing, as well as the associate editor of VIDEOGAMES & COMPUTER ENTERTAINMENT.


handle_kegs ()
{
   int button:

   if ( loaded && !search && !canceling )
      switch ( key ) {

         case CNTL_A:
            menu_tnormal ( menu_addr, CHECKS, FALSE );
            do_auto ():
            menu_tnormal ( menu_addr, CHECKS, TRUE );
            break:

         case CNTL_E:
            menu_tnormal ( menu_addr, CHECKS, FALSE );
            do_enter ():
            menu_tnormal ( menu_addr, CHECKS, TRUE );
            break:

         case CNTL_M:
            menu_tnormal ( menu_addr, FILEBAR, FALSE );
            do_new_mnth ();
            menu_tnormal ( menu_addr, FILEBAR, TRUE );
            break:

         case CNTL_P:
            menu_tnormal ( menu_addr, CHECKS, FALSE );
            do_check_canc ();
            menu_tnormal ( menu_addr, CHECKS, TRUE );
            break:

         case CMTL_R:
            menu_tnormal ( nenu_addr, CHECKS, FALSE );
            do_reconcil ();
            menu_tnormal ( menu_addr, CHECKS, TRUE );
            break;
      }

   if ( !loaded )
      switch ( key ) {

         case CNTL_N:
            menu_tnormal ( menu_addr, FILEBAR, FALSE );
            do_newacct ();
            menu_tnormal ( menu_addr, FILEBAR, TRUE );
            break:

         case CNTL_O:
            menu_tnormal ( menu_addr, FILEBAR, FALSE );
            button = get_acct ();
            if ( button )
               open acct ( filename );
            menu_tnormal ( menu_addr, FILEBAR, TRUE );
            break:

         case CMTL_Y:
            menu_tnormal ( menu_addr, UTILITY, FALSE );
            do_new_gear ();
            menu_tnormal ( menu_addr, UTILITY, TRUE );
            break:

         case CMTL_I:
            menu_tnormal ( menu_addr, UTILITY, FALSE );
            do_import ():
            menu_tnormal ( menu_addr, UTILITY, TRUE );:
            break;
      }
  switch ( key ) {

      case CNTL_Q:
         menu_tnormal ( menu_addr, FILEBAR, FALSE );
         do_quit ();
         menu_tnormal ( menu_addr, FILEBAR, TRUE );
         break:

      case CHTL_S:
         if ( loaded && !canceling ) {
            menu_tnormal ( menu_addr, CHECKS, FALSE );
            do_search ():
            menu_tnormal ( menu_addr, CHECKS, TRUE );
         }
         break:

      case CMTL_C:
         if ( loaded ) {
            menu_tnormal ( menu_addr, FILEBAR, FALSE );
            do_wind_close ();
            menu_tnormal ( menu_addr, FILEBAR, TRUE );
         }
         break;:

      case CHTL_D:
         menu_tnormal ( menu_addr, UTILITY, FALSE );
         get_new_date ();
         menu_tnormal ( menu_addr, UTILITY, TRUE );
         break;

      case CHTL_W:
         if ( loaded ) {
            menu_tnormal ( menu_addr, PRINT, FALSE );
            print_wind ();
            menu_tnormal ( menu_addr, PRINT, TRUE );
         }
         break;

      case CHTL_G:
         if ( loaded ) {
            menu_tnormal ( menu_addr, PRINT, FALSE ):
            print_reg ():
            menu_tnormal ( menu_addr, PRINT, TRUE ):
         }
         break:
   }
}

do_newacct ()
{
   int choice, okay;
   int dial_x, dial_y, dial_w, dial_h;

   clear_newacct ();
   form_center ( newacct_addr, &dial_x, &dial_y, &dial_w, &dial_h );
   form_dial ( FMD_START, 0, 0, 10, 10, dial_x, dial_y, dial_w, dial_h 1;
   objc_draw ( newacct_addr, 0, 8, dial_x, dial_y, dial_w, dial_h );

   do (
      choice = form_do ( newacct_addr, NEWNAME );
      newacct_addr[choice].ob_state = SHADOWED;

      switch ( choice ) {

         case NEWOK:
            okay = check_newacct ();
            if ( !okay )
               objc_draw ( newacct_addr, 0, 8,
                           dial_x, dial_y, dial_w, dial_h );
            else
               newacct_file ();
            break:

         case NEWCANCL:
           clear_newacct ();
      }
   }
   while ( okay == FALSE && choice != NEWCANCL );

   form_dial ( FMD_FINISH, 0, 0, 10, 10, dial_x, dial_y, dial_w. dial_h );
}

check_newacct ()
{
   int x, okay;

   okay = TRUE;
   for ( x=NEWNAME: x<=NEWBALNC: ++x ) (
      string = get_tedinfo_str ( newacct_addr, x );
      if ( string[0] == '@' )
         okay = FALSE:
   }
   if ( !okay )
      form_alert(1,"[1][You must completelthe form to startia new account!]\
[OK]");
   return ( okay );
}

newacct_file ()
{
   int choice, okay, x;
   int dial_x, dial_y, dial_w, dial_h;

   string = get_tedinfo_str ( newfile_addr, FILENAME );
   string[0] = 0:
   for ( x=0; x<64: filename[x++]=0 );
   newfile_addr[NEWOK].ob_state = SHADOWED;
   form_center ( newfile_addr, &dial_x, &dial_y, &dial_w, &dial_h );
   form_dial ( FMD_START, 0, 0, 10, 10, dial_x, dial_y, dial_w, dial_h );
   objc_draw ( newfile_addr, 0, 8, dial_x, dial_y, dial_w, dial_h );

   do {
      choice = form_do ( newfile_addr, FILENAME );
      newfile_addr[choice].ob_state = SHADOWED;

      switch ( choice ) {

         case FILEOK:
            okay = check_file ();
            if ( !okay )
               objc_draw ( newfile_addr, 0, 8,
                           dial_x, dial_y, dial_w, dial_h );
            else {
              string = get_tedinfo_str ( newfile_addr, FILENAME );
              strcpy ( acct_name, string );
              filename[0] = Dgetdrv () + 'a';
              filename[1l = ':';
              Dgetpath ( &filename[2], DFLT_DRV );
              filename[strlen(filename)] = '\\';
              strcpy ( &filename[strlen(filename)], string );
              strcpy ( &filename[strlen(filename)], ".MCK" );
              acctfile = fopen ( filename, "bw" );
              if ( acctfile != 0 ) {
                  write_new_info ();
                  open_acct ( filename );
              }
            }
            break;

         case FILECANC:
            string = get_tedinfo_str ( newfile_addr, FILENAME );
            string[0] = 0;
      }
   }
   while ( !okay && choice != FILECANC );

   form_dial ( FMD_FINISH, 0, 0, 10, 10, dial_x, dial_y, dial_w, dial_h );
}

clear_newacct ()
{
   int x;

   for ( x=NEWHAME: x(=NEWBALNC; ++x ) {
      string = get_tedinfo_str ( newacct_addr, x );
      string[0] = '@';
   }
   newacct_addr[NEWCANCL].ob_state = SHADOWED;
}

char *get_tedinfo_str ( tree, object )
OBJECT *tree;
int object;
{
   TEDINFO *ob_tedinfo;

   ob_tedinfo = (TEDINFO *) tree[object].ob_spec;
   return ( ob_tedinfo->te_ptext );
}

check_file ()
{
   int okay;

   okay = TRUE;
   string = get_tedinfo_str ( newfile_addr, FILENAME );
   if ( strlen ( string ) == 0 ) {
      form_alert(1,"[1][Invalid filename!][OK]");
      okay = FALSE;
   }
   return ( okay );
   no_decimal ( s )
   char *s;
{
   int x, i, d, len;
   char s2[20], s3[20];

   strcpy ( s2, s );
   len = strlen ( s2 );
   i = 0;
   d = FALSE;
   for ( x=0; x<len: ++x )
      if ( d && s2[x] != ' ' )
         ++i;
      else
         if ( s2[x] == ' ' )
            d = TRUE;

   if ( i == 0 && len < 6 )
      strcpy ( &s2[len], "00" );
   else
      if ( i == 1 || len == 6 )
         strcpy ( &s2[len], "0" );

   i = 0;
   for ( x=0: x<strlen(s2); ++x )
      if ( s2[x] != ' ' & s2[x] != '.' )
         s3[i++] = s2[x];
   s3[i] = 0;
   strcpy ( s, s3 );
}

long str_to_long ( s )
char *s;
{
   int x, len, factor;
   long num;

   num = 0;
   len = strlen ( s );
   factor = len - 1;
   for C x=0; x<len; ++x )
      num += (long) ( s[x] - '0' ) * pwrs[factor--];
   return ( num );
}

write_new_info ()
{
   int len, x;
   char s[10], tmpfile[64];
   FILE *f;

   string = get_tedinfo_str ( newacct_addr, NEWNAME );
   fwrite ( string, 1, 26, acctfile );
   string = get_tedinfo_str ( newacct_addr, NEWADDR );
   fwrite ( string, 1, 26, acctfile );
   string = get_tedinfo_str ( newacct_addr, NEWCITY );
   fwrite ( string, 1, 26, acctfile );
   string = get_tedinfo_str ( newacct_addr, NEWSTATE );
   fwrite ( string, 1, 3, acctfile );
   string = get_tedinfo_str ( newacct_addr, NEWZIP );
   fwrite ( string, 1, 10, acctfile );
   string = get_tedinfo_str ( newacct_addr, NEWBALNC );
   no_decimal ( string );
   balance = str_to_long ( string );
   fwrite ( &balance, 1, 4, acctfile );
   if ( fclose ( acctfile ) != 0 )
      form_alert ( 1, "[1][File close error!] [OKAY]"];
   for ( x=0; x<13: ++x ) {
      sprintf ( s, "%d", x );
      strcpy ( &s[strlen(s)], ".dat" );
      ob_tedinfo = (TEDINFO *) newfile_addr[FILENAME].ob_spec;
      tmpfile[0] = Dgetdry () + 'a';
      strcpy ( &tmpfile[1], ":" );
      Dgetpath ( &tmpfile[strlen(tmpfile)], DFLT_DRV );
      strcpy ( &tmpfile[strlen(tmpfile)], "\\" ):
      strcpy ( &tmpfile[strlen(tmpfile)], ob_tedinfo->te_ptext );
      strcpy ( &tmpfile[strlen(tmpfile)], s );
      if ( ( f = fopen ( tmpfile, "bw" ) ) == NULL )
         form_alert ( 1, "[1][Error creating file!][OK]" );
      else
         fwrite ( &zero, 2, 1, f );

      if ( fclose ( f ) != 0 )
            form_alert ( 1, "[1][File close error!][OK]");
   }
}