Now comes the fun part. I know this looks like a complicated program,
but 90% of the code you've already seen in one form or another. The file
I/O part is a very small subset of this program.
This is the first of many programs which I will consider 'advanced'. I
will no longer write extremely short pointless examples to illustrate a
single concept while shying away from any actual writing. Though this
program is largely pointless, it could be viewed as the beginning of an
address book program, which we will write by the end of this lesson.
Since the program is so much larger than most examples, we should start
off with a description of what the program does. Most parts of the code
I will not be covering because you've seen most of it before. I will
focus on the parts we're trying to learn here. File I/O, the menus used
in the dialog box, and the preprocessor directives used to distinguish
between TI-89 and TI-92+/V200 versions (due to screen size differences).
This program takes a structure called PERSON and allows us to modify
that structure, and save/load that data to and from a file. So, we can
store personal information about a person. We can save this data to a
file. And we can later load this data from that file and display our
person record again. The data is all presented in dialog boxes to
provide an easy-to-use interface.
Okay, now that we know what the program's designed to do, let's see how
it accomplishes it.
#include <dialogs.h>
#include <ctype.h>
#include <string.h>
#include <mem.h>
#include <stdio.h>
#include <kbd.h>
#include <stdlib.h>
#include <alloc.h>
#include <args.h>
#include <menus.h>
First a word about the include files. Instead of including tigcclib.h
like we normally do, we will start including only the header files that
we need to accomplish our tasks. It makes compiling faster (the compiler
doesn't have to work with unnecessary files) and is more standard in
actual C programming. Normally, you will not have one header file which
has the information for all the other header files inside it. I will
mention a certain header file that is needed whenever we encounter a
function that's not already part of our includes. You can find the
functions that these header files contain in the TIGCC docs.
// comment this next line out for TI-92+/V200 support
#define USE_TI89
#ifndef USE_TI89
#define USE_TI92PLUS
#define USE_V200
#endif
This is another thing we have not used before, but which becomes
increasingly necessary in more complex C programs; the preprocessor
directives. Remember from way back that the preprocessor gets called to
process the file before the compiler works on the code. It replaces
certain items in our code with other things according to our directions.
For example, wherever we have a #include directive, the preprocessor
will replace that line with the contents of that file. We have also seen
the #define directive before, used to define global values for our
program. We have also seen #define directives in header files to make
sure no header is included twice. Here we are using these defines to
determine which calculator our program will be built for.
The reason we have done this is because the TI-92+ uses a different
font for dialog boxes (the large font) and takes up more space than the
TI-89. We could have used the same technique we used in lesson 8, but
this is just another technique. The difference between the two is code
size. More code is used in the lesson 8 method, but the program will run
on either the TI-89 or the TI-92+/V200. Using the method we use here,
the TI-89 program will only run correctly on the TI-89, and we have to
compile two binaries instead of one.
#ifdef USE_TI89
enum DialogConstants {DLG_WIDTH = 140, DLG_HEIGHT = 85};
#else
enum DialogConstants {DLG_WIDTH = 200, DLG_HEIGHT = 90};
#endif
Here is the important part of our #define directives, the difference in
our enumeration constants. So, if we defined USE_TI89, the program will
keep the dialog constants for the TI-89 size, and if the TI-92+/V200 is
what we want, then it keeps those constants.
typedef struct {
char name[15];
short int height;
short int weight;
char dob[11];
} PERSON;
This is the PERSON structure used for saving our record. It saves a
name, height (in centimeters), weight (in kilograms), and a date of
birth. I used the metric system since it seemed more universal, but it
doesn't really matter. But we have validation functions that make sure
the data is within 'reasonable' limits. We will see the validation
functions later.
void _main(void) {
char buffer[34];
PERSON p;
short int done = 0, option = MENU_DATA;
HANDLE dlg = H_NULL, menu = H_NULL;
// initialize the person structure
initPerson(&p);
// format the request buffer string
formatRequestString(buffer,&p);
if ((dlg = DialogNewSimple(DLG_WIDTH,DLG_HEIGHT)) == H_NULL) {
dlgError(MEMERR1,MEMERR2);
return;
}
if ((menu = PopupNew(NULL,0)) == H_NULL) {
dlgError(MEMERR1,MEMERR2);
HeapFree(dlg);
return;
}
// create Dialog Menu
PopupAddText(menu,-1,"Enter new Data",MENU_DATA);
PopupAddText(menu,-1,"Read Data from File",MENU_READ);
PopupAddText(menu,-1,"Save Data to File",MENU_WRITE);
PopupAddText(menu,-1,"Print Information",MENU_PRINT);
PopupAddText(menu,-1,"Exit",MENU_EXIT);
DialogAddTitle(dlg,"Main Menu",BT_OK,BT_CANCEL);
DialogAddPulldown(dlg,5,15,"Selection:",menu,0);
do {
if (DialogDo(dlg,CENTER,CENTER,NULL,&option) != KEY_ENTER) {
option = MENU_EXIT;
}
switch (option) {
case MENU_DATA:
getData(&p,buffer);
printData(&p);
break;
case MENU_READ:
if (readData(&p)) {
dlgError(FILEIO4,FILEIO5);
formatRequestString(buffer,&p);
printData(&p);
}
break;
case MENU_WRITE:
if (writeData(&p)) {
dlgError(FILEIO4,FILEIO5);
}
break;
case MENU_PRINT: printData(&p); break;
case MENU_EXIT: done = TRUE; break;
}
} while (!done);
// free the memory used by the dialog box and selector menu
HeapFree(menu);
HeapFree(dlg);
}
The main method is short in this program, as it should be in most
programs. The _main() function is simply for starting the program and
calling helper functions to do the work. We should rarely do any work in
the main function. The first task is to initialize the person structure.
void initPerson(PERSON *p) {
// terminate the person strings
p->name[0] = 0;
p->dob[0] = 0;
// enter generic person information
strcpy(p->name,"Dominic Silver");
strcpy(p->dob,"06-02-1972");
p->height = 190;
p->weight = 87;
}
The person initializer function works very simply. We start by
terminating the strings. This is necessary to use the strcpy function.
The strcpy (STRing CoPY) function copies a string onto another string.
You can look up more about it in the TIGCC docs if the usage is not
clear here. strcpy is defined in the string.h header.
Our next task is the format the string buffer. Setting this buffer to
keep our PERSON values stored inside it allows us to set default values
to the dialog box. I'll explain how that works in the function.
void formatRequestString(char *temp, PERSON *p) {
// erase the buffer string
memset(temp,0,34*sizeof(char));
// format the buffer string so the dialog box will have default values
sprintf(temp,"%-15s%-11s%03hd %03hd",p->name,p->dob,p->height,p->weight);
// add string separators
temp[14] = 0;
temp[25] = 0;
temp[29] = 0;
temp[33] = 0;
}
The first step is to erase the string. This is accomplished by using
memset to set all our bytes to zero. I don't think we have used memset
before, but it's quite simple. It takes an array, a value, and a length.
Then sets that many bytes of the array to the value. It's a quick way to
fill an array. Next we use sprintf to set the values inside our request
string. memset is defined in mem.h, and sprintf is defined in stdio.h.
This string uses lots of format specifiers, so we need to be clear about
how they work. %-15s means to add a string (s) here that is a least 15
characters long, and if the string is not at least 15 characters, add
spaces to it until it is. The default is to add the spaces to the left,
the - tells us to add those spaces to the right instead. I think you can
guess how %-11s works. Then we have the %03hd values. We know it's a
number because hd is a short integer, and 3 tells us it must be 3 digits
long. However, we don't want to add spaces to the left, we want to add
zeros, so we can use 03 to tell it to prefix zeros.
Finally, we must terminate the strings so the dialog box will know where
to stop printing. Our first string has 14 characters, so the terminator
must go in the 15th space, which is index 14 (counting starts at 0
remember). The next string is 10 characters, so 14 + 10 + 1 for the
terminator is index 25. The next two strings are 3 characters each + 1
for the terminator, so 25 + 3 + 1 = 29, and 29 + 3 + 1 = 33. This can be
tricky. In fact, when I first published this lesson, I did it wrong and
the program stopped working in later TIGCC versions.
if ((dlg = DialogNewSimple(DLG_WIDTH,DLG_HEIGHT)) == H_NULL) {
dlgError(MEMERR1,MEMERR2);
return;
}
if ((menu = PopupNew(NULL,0)) == H_NULL) {
dlgError(MEMERR1,MEMERR2);
HeapFree(dlg);
return;
}
Our next step is to create the dialog box. That is pretty standard, but
we have a new function which creates a pulldown menu. It looks similar
to creating a dialog, but with a new function. PopupNew creates a new
popup menu with a title and a height. Since we are using it as a
pulldown menu and not a popup menu, use use NULL for the name and a
height of 0 (for auto-calculate the height). If we have any errors, free
the dialog box memory used and exit the function. It's good to note that
dialogs and menus use HANDLE's for their type. This is because they are
both using memory to create their structures. Most operating systems
work via a system of handles where handles are used to manage blocks of
memory. Files are actually done in this manner too, but we will hide
that fact with an encompassing structure. The Dialog* functions are
defined in dialogs.h, while the Popup* functions are defined in menus.h.
// create Dialog Menu
PopupAddText(menu,-1,"Enter new Data",MENU_DATA);
PopupAddText(menu,-1,"Read Data from File",MENU_READ);
PopupAddText(menu,-1,"Save Data to File",MENU_WRITE);
PopupAddText(menu,-1,"Print Information",MENU_PRINT);
PopupAddText(menu,-1,"Exit",MENU_EXIT);
DialogAddTitle(dlg,"Main Menu",BT_OK,BT_CANCEL);
DialogAddPulldown(dlg,5,15,"Selection:",menu,0);
We add menu options to our pulldown menu by using the PopupAddText
command. PopupAddText takes a popup menu handle, a link value (which for
our purposes is always -1, because we're not using linked popup
structures), a text string for the menu choice, and a value. You can see
that we have a MenuConstants enumeration defined at the top that has
these values.
So, when the user chooses an option from the menu, we will get one of
these values back as a result.
The DialogAddTitle() should be simple enough, we've seen it before. The
DialogAddPulldown is how we add our pulldown menu to the dialog box. We
specify a dialog box handle, the dialog window coordinates (5,15), a
text label, the popup menu handle, and the position in the array to
store our answer. Remember that in dialog requests, we used a large text
buffer where all the answers where stored. Following from this, if we
have multiple menus, we must have multiple places to store the answer.
Since all the options on the menu return integer values, we use an array
of integers. However, since we only have one menu, we can use a single
integer.
if (DialogDo(dlg,CENTER,CENTER,NULL,&option) != KEY_ENTER) {
option = MENU_EXIT;
}
We have seen the DialogDo() function before, but we have new options
this time. It's much the same except we use NULL for the RequestBuffer
(since we have no text requests), and give the address of the option
variable to store our menu options in. Also remember from our call to
DialogAddTitle() that we added OK and CANCEL buttons, so if the user
doesn't press ENTER, he must have pressed ESC to exit the dialog,
meaning cancel. So, we then set the option to MENU_EXIT so the program
will quit.
switch (option) {
case MENU_DATA:
getData(&p,buffer);
printData(&p);
break;
case MENU_READ:
if (readData(&p)) {
dlgError(FILEIO4,FILEIO5);
formatRequestString(buffer,&p);
printData(&p);
}
break;
case MENU_WRITE:
if (writeData(&p)) {
dlgError(FILEIO4,FILEIO5);
}
break;
case MENU_PRINT: printData(&p); break;
case MENU_EXIT: done = TRUE; break;
}
Here is the part where we evaluate the selection of the user. Remember
that our option variable will contain the value returned from the dialog
box. If they select Enter New Data (MENU_DATA), we call the getData()
and printData() functions.
short int getData(PERSON *p, char *buffer) {
HANDLE dlg = H_NULL;
int done = FALSE;
char *token;
// create the dialog box
if ((dlg = DialogNewSimple(DLG_WIDTH,DLG_HEIGHT)) != H_NULL) {
// format the dialog box
DialogAddTitle(dlg,"Personal Information",BT_OK,BT_NONE);
DialogAddRequest(dlg,5,15,"Name:",0,14,18);
DialogAddRequest(dlg,5,25,"DOB:",15,10,18);
DialogAddRequest(dlg,5,35,"Height (cm):",26,3,5);
DialogAddRequest(dlg,5,45,"Weight (kg):",30,3,5);
while (!done) {
// loop until the user presses ENTER
while (DialogDo(dlg,CENTER,CENTER,buffer,NULL) != KEY_ENTER);
// grab the name from the string buffer
token = buffer;
p->name[0] = 0;
strcpy(p->name,token);
// grab the DOB from the string buffer
token = buffer + 15;
p->dob[0] = 0;
strcpy(p->dob,token);
// grab the height from the string buffer
token += 11;
p->height = atoi(token);
// grab the weight from the string buffer
token += 4;
p->weight = atoi(token);
// we're done unless we fail one of our validity tests
done = TRUE;
// check for valid DOB entry (MM/DD/YYYY)
if (!isValidDOB((const unsigned char *)p->dob)) {
dlgError(DATA_ERROR,DOB_ERROR);
done = FALSE;
}
// check for reasonable valid height
if (!isValidHeight((const short int)p->height)) {
dlgError(DATA_ERROR,HEIGHT_ERROR);
done = FALSE;
}
// check for reasonable valid weight
if (!isValidWeight((const short int)p->weight)) {
dlgError(DATA_ERROR,WEIGHT_ERROR);
done = FALSE;
}
}
// free the memory used by the dialog
HeapFree(dlg);
} else {
dlgError(MEMERR1,MEMERR2);
return FALSE;
}
return TRUE;
}
This function looks complicated, but it's not. Most of the function is
simply constructing and handling the dialog box. We ask the user to
input their name, height, weight, and date of birth. However, more
important in this function is our data verification functions. We force
the data to be within certain ranges, or have certain values or formats.
For example, we force the DOB (date of birth) to be MM-DD-YYYY. If the
DOB is not in this format, we consider it to be invalid. In this way, we
can make sure we have valid data (at least syntactically). The HeapFree
function is defined in alloc.h.
// grab the name from the string buffer
token = buffer;
p->name[0] = 0;
strcpy(p->name,token);
I want to cover these segments real quick just to make sure we're up to
speed on pointers. The variable token is a character pointer. We assign
its value to be buffer, so now token and buffer are pointing at the same
string. This is helpful in dividing our large buffer into our smaller
pieces. Later, we can use this same technique, but we'll attach names to
our variable pointers. That will make it seem more intuitive, although
the process is the same.
// check for valid DOB entry (MM/DD/YYYY)
if (!isValidDOB((const unsigned char *)p->dob)) {
dlgError(DATA_ERROR,DOB_ERROR);
done = FALSE;
}
// check for reasonable valid height
if (!isValidHeight((const short int)p->height)) {
dlgError(DATA_ERROR,HEIGHT_ERROR);
done = FALSE;
}
// check for reasonable valid weight
if (!isValidWeight((const short int)p->weight)) {
dlgError(DATA_ERROR,WEIGHT_ERROR);
done = FALSE;
}
Here is where we make our validity checks. If any one of our checks
fail, display an error message and set done to false. Remember that
since we are in a while (!done) loop, we will keep displaying the dialog
until we are satisfied. This should probably be adapted in a real
program so we can cancel.
// check for valid date of birth (MM-DD-YYYY)
short int isValidDOB(const unsigned char *dob) {
if (isdigit(dob[0]) && isdigit(dob[1]) && (dob[2] == '-' || dob[2] == DASH) &&
isdigit(dob[3]) && isdigit(dob[4]) && (dob[5] == '-' || dob[5] == DASH) &&
isdigit(dob[6]) && isdigit(dob[7]) && isdigit(dob[8]) && isdigit(dob[9])) {
return TRUE;
}
return FALSE;
}
This is another example of a function that looks complex, but is really
not. We simply verify that each character in our DOB string is either a
number or a dash. The first two must be digits, as are the 4th and 5th,
and the 7th through 10th. Characters 3 and 5 must be a dash. This is a
relatively simplistic validity function for DOB, which only checks to
make sure the data looks right. A better function would check the values
to make sure they are reasonable. For example, I could enter my DOB at
13-32-1000, and it would return valid, but that's obviously not a valid
DOB.
// check for valid height (in centimeters)
short int isValidHeight(const short int height) {
if (height > 100 && height < 250) {
return TRUE;
}
return FALSE;
}
// check for valid weight (in kilograms)
short int isValidWeight(const short int weight) {
if (weight > 35 && weight < 180) {
return TRUE;
}
return FALSE;
}
The next two validity checkers simply make checks on reasonable values.
Height between 100 and 250 centimeters and weight between 35 and 180
kilograms. If you're not familiar with the metric system, 1 inch is 2.2
centimeters, and 2.2 kilograms is 1 pound. So, from 4 ft to 9 ft, and 75
to 400 lbs.
Now that we've entered our data, display it back so we can make sure
what we entered is what we want.
void printData(PERSON *p) {
char name[25],dob[20],height[20],weight[20];
HANDLE dlg = H_NULL;
if ((dlg = DialogNewSimple(DLG_WIDTH,DLG_HEIGHT)) != H_NULL) {
// create the personal information strings
sprintf(name,"Name: %s",p->name);
sprintf(dob,"DOB: %s",p->dob);
sprintf(height,"Height: %hd cm",p->height);
sprintf(weight,"Weight: %hd kg",p->weight);
// format the dialog box
DialogAddTitle(dlg,"Personal Information",BT_OK,BT_NONE);
DialogAddText(dlg,5,15,name);
DialogAddText(dlg,5,25,dob);
DialogAddText(dlg,5,35,height);
DialogAddText(dlg,5,45,weight);
// display the dialog box
DialogDo(dlg,CENTER,CENTER,NULL,NULL);
HeapFree(dlg);
}
}
The printData() function is simple. We take our person structure, and
create a dialog box with text strings representing our data. Then we
display the dialog box. There is nothing here we haven't already seen
many times before.
case MENU_READ:
if (readData(&p)) {
dlgError(FILEIO4,FILEIO5);
formatRequestString(buffer,&p);
printData(&p);
}
break;
case MENU_WRITE:
if (writeData(&p)) {
dlgError(FILEIO4,FILEIO5);
}
break;
The next two options in our switch statement control the reading and
writing of our data. When we read the data back from the file, we format
the request string and print the data we've read.
I'm just now noticing it looks like I completely glossed over any
possible I/O problems. Looks like this program will fail silently if
something bad happens. Let's hope it doesn't I guess.
short int readData(PERSON *p) {
FILE *f = NULL;
short int fileio = TRUE;
// open file for reading in binary mode
if ((f = fopen("TESTFILE","rb")) == NULL) {
dlgError(FILEIO1,FILEIO2);
fileio = FALSE;
} else {
// read data from file into PERSON structure
if (fread(p,sizeof(PERSON),1,f) != 1) {
dlgError(FILEIO1,FILEIO3);
fileio = FALSE;
}
// close the file
fclose(f);
}
return fileio;
}
Here is our first taste of actual file I/O. Although the AMS has its own
built-in routines for file handling, they are a bit more complex than
the standard C library. Fortunately for us, the TIGCC team has
implemented the C File I/O routines so we can use them here. These
include fopen, fclose, fread, fwrite, fprintf, fputc, fgetc, fputs, and
fgets. The f* functions are defined in stdio.h.
To work with files, we use a special structure called a FILE struct.
Because many functions need to change the data associated with a file
struct, the FILE struct is always a pointer. We can get a pointer to a
file structure by calling the fopen() function. The fopen() function
opens a file for reading or writing and returns a FILE pointer. The
modes we are most concerned with are the following:
- rb (read binary)
- rt (read text)
- wb (write binary)
- wt (write text)
Text files are the files we can edit with the text editor application.
So, by using text files, we can edit the files directly. However, for
record keeping of data (mixing strings, numbers, floats, etc), it is
best to use binary. This is because storing text records force us to
convert back and forth from text to binary every time we want to use the
data, and vice-versa to store the data back in the file. So, we will
focus on binary mode in this example. However, this doesn't mean text
mode is useless. If you only need to store strings, and need an easy way
to edit the strings, a text file might be ideal.
If there is a problem opening the file, NULL will be returned. So we
must check for this occurrence. We can't read from a NULL pointer, and
your program will crash if you try, taking the system with it. You will
be tempted not to perform these little error checks. It's a lot of work
for things that happen rarely. If you don't check for errors, errors
seem to happen. File errors cause calc crashes, so keep that in mind.
Try choosing the read option from the program before you save the data
the first time and see what happens. (fear not, it won't crash)
Now, our next step now that we have an open file is to read the data in
from that file. The fread() function takes a pointer (where it will
store the data), the size of a data record (the size of our structure),
how many records we want to read (in our case 1, but we can read any
number we want), and the FILE struct pointer. It returns the number of
records it successfully read. So, if fread() doesn't return 1, we didn't
get what we were looking for.
To see how the error checking we have works, create any variable
(expression, text, program, whatever) and name it testfile. Then try to
have the program read data from the file. See what happens. Unless that
file just happens to be the exact size of a PERSON struct, you will get
an error.
Finally, for every fopen() call we make, we must make sure to make a
call to fclose(). This is because our FILE pointer was allocated
dynamically by fopen(), and so is the space for our file. If we forget
to close a file, we will have a memory leak. The system has no way of
knowing if you don't close a file, so just remember to make sure you
fclose() every file you fopen().
short int writeData(PERSON *p) {
FILE *f = NULL;
short int fileio = TRUE;
// open file for writing
if ((f = fopen("TESTFILE","wb")) == NULL) {
dlgError(FILEIO1,FILEIO2);
fileio = FALSE;
} else {
// write structure data to the file
if (fwrite(p,sizeof(PERSON),1,f) != 1) {
dlgError(FILEIO1,FILEIO3);
fileio = FALSE;
}
// append the file ID tag
fputc(0,f);
fputs("FIO",f);
fputc(0,f);
fputc(OTH_TAG,f);
// close the file
fclose(f);
}
return fileio;
}
For every read, we also need a corresponding write. Writing is nearly
identical to reading, we just change fread() to fwrite(). However, since
we are writing custom data, we must give our file a file type. This is
so the AMS never tries to interpret our data.
Remember that rb has a corresponding wb for write binary data. If we
open a file for writing that doesn't exist, the fopen() call will create
the file for us. If we open a file for writing that already exists, we
will overwrite the file.
The calls to fopen() and fwrite() are nearly identical to the ones in
the readData() function, with fread() replaced with fwrite(). So let's
move on to the file tag.
The fputc() function writes a single character to a file. So, we want to
write the number 0 to the file. It's a terminator, like in strings. Then
we put the file type (1-4 characters). We use fputs() for that, which
prints a string (without the null terminator) to a file. We print a
final terminator, then put an OTH_TAG (from args.h) for the
miscellaneous file tag type. This lets the system know we are dealing
with user-data. The file type is "FIO". This is displayed in the
var-link screen.
The rest of the program should be rather self-explanatory. But remember
that popup menus must undergo HeapFree(handle) before you exit the
program just like malloc() variables and dialog boxes.