I'll bet you didn't think I could top that endless example from part I,
eh? Well, that's okay, there's not a lot of new stuff. Dialog box
creation is just a pain in the arse. But I think you know how that works
by now, so let's stick to the relevant parts of the code...
// comment this next line out for TI-92+/V200 support
#define USE_TI89
#ifndef USE_TI89
#define USE_TI92PLUS
#define USE_V200
#endif
First off, we need to make sure you are compiling the correct version.
Although they work the same on both calculators, the dialog boxes will
be cut off on the TI-92+/V200 and vice-versa. If you are compiling the
TI-89 version, you can leave it as is. I assume most people have a
TI-89, so I leave that as the default. If however, you are using a
TI-92+/V200, make sure to COMMENT OUT THAT TOP LINE!
We're going to skip the _main() method altogether since it should be
second-hand to you now. We will start with the addEntry() function which
is called when we choose to add a new entry from the main menu.
#define ENTRY_BUFFER_SIZE 131
// add a new address book record
void addEntry(void) {
char buffer[ENTRY_BUFFER_SIZE];
// erase the buffer string
memset(buffer,0,ENTRY_BUFFER_SIZE);
// now display the add new entry dialog box
doEntryDialog(buffer,NEW_ENTRY);
}
Adding and editing entries are very similar, since the dialog box
interface assumes that all variables are strings, and non-empty strings
should be default values in input boxes, so, the only thing we need to
do in an add/edit routine is setup a buffer to hold the data, then tell
the write routine how to interpret the data, either as an edit or a
database addition. The ENTRY_BUFFER_SIZE is the size our buffer string
needs to be. Since we use this value in several places, it's a good idea
to give it a name, and only change the value in one place. This helps
minimize stupid coding errors.
Since the functions are much the same, we will look at the edit function
too before examining how they do their work inside the doEntryDialog()
function.
// edit the current entry
void editEntry(void) {
char buffer[ENTRY_BUFFER_SIZE];
char *name = buffer + 0, *address = buffer + 26, *city = buffer + 52,
char *state = buffer + 78, *zipcode = buffer + 81, *phone = buffer + 87;
char *email = buffer + 100;
// we can't edit if there are no entries
if (recordCount == 0) {
dlgError(RECORD_ERROR,NO_RECORDS_ERROR);
return;
}
// erase the buffer string
memset(buffer,0,ENTRY_BUFFER_SIZE);
// copy the current information into the buffer string
strcpy(name,p->name);
strcpy(address,p->address);
strcpy(city,p->city);
strcpy(state,p->state);
strcpy(zipcode,p->zipcode);
strcpy(phone,p->phone);
strcpy(email,p->email);
// now display the edit dialog box
doEntryDialog(buffer,EDIT_ENTRY);
}
The function looks more complicated, but it's not really. The only
difference is that we need to fill the input buffer with the data from
the record we want to edit. So, just copy the data we have in our global
PERSON structure to the buffer the dialog box needs. We have setup a
series of pointers to address different parts of the string buffer. So,
instead of treating it as a single piece of string, we can treat it like
all the elements of the PERSON structure. This is helpful so we only
have to do the math once to find which parts of the string carry which
pieces of data, then assign those pieces names.
In addybook.h, we defined a global PERSON structure pointer called p.
The variable p contains the address book record we are currently working
on (adding, editing, removing, displaying, etc.) So, if we want to copy
the data we have to our string buffer, we copy it from the PERSON
structure p. Then we call the doEntryDialog() function.
Now the doEntryDialog() function takes care of creating the dialog box
and saving the data we've collected from the user to the address book
file. Let's take a look at that now.
// display the dialog box to add/edit a record entry
void doEntryDialog(char *buffer, short int action) {
HANDLE dlg = H_NULL;
char *name = buffer + 0, *address = buffer + 26, *city = buffer + 52;
char *state = buffer + 78, *zipcode = buffer + 81, *phone = buffer + 87;
char *email = buffer + 100;
const char *strings[] = {"Add New Record","Record Added Successfully",
"Edit Record","Record Edited Successfully"};
const char *title = NULL, *msg = NULL;
// allocate memory for the dialog box
if ((dlg = DialogNewSimple(DLG_DISPLAY_WIDTH,DLG_DISPLAY_HEIGHT)) == H_NULL) {
dlgError(DMA_ERROR,MEM_ERROR);
return;
}
// set the title strings based on the action we're taking (add or edit)
if (action == NEW_ENTRY) {
title = strings[0];
msg = strings[1];
} else {
title = strings[2];
msg = strings[3];
}
// create the dialog box
DialogAddTitle(dlg,title,BT_NONE,BT_NONE);
DialogAddRequest(dlg,5,15,"Name:",0,25,15);
DialogAddRequest(dlg,5,25,"Address:",26,25,15);
DialogAddRequest(dlg,5,35,"City:",52,25,15);
DialogAddRequest(dlg,5,45,"State:",78,2,5);
DialogAddRequest(dlg,5,55,"Zipcode:",81,5,10);
DialogAddRequest(dlg,5,65,"Phone:",87,12,15);
DialogAddRequest(dlg,5,75,"Email:",100,30,15);
// display the dialog box
if (DialogDo(dlg,CENTER,CENTER,buffer,NULL) == KEY_ENTER) {
// erase PERSON structure
memset(p,0,sizeof(PERSON));
// increment the record count if we're adding a new one
if (action == NEW_ENTRY) {
recordCount++;
record = recordCount;
}
// copy the entered data into our person structure
p->id = record;
strcpy(p->name,name);
strcpy(p->address,address);
strcpy(p->city,city);
strcpy(p->state,state);
strcpy(p->zipcode,zipcode);
strcpy(p->phone,phone);
strcpy(p->email,email);
// save the new data
if (writeRecords(action)) {
DlgMessage((char *)title,(char *)msg,BT_OK,BT_NONE);
}
}
// free the dialog memory
HeapFree(dlg);
}
Dialog box creating may still seem a little new to some people, but once
you get the pattern down, it's very easy. Refer back to lessons 6 and 7
if you're having trouble with dialogs.
Okay, assuming you are comfortable with the dialog box, once the box has
the data, all we have to do is copy it to our PERSON structure p, then
save the data. Also note that if we are adding a new record, we only add
things at the end (for simplicity sakes -- no alphabetization, no
sorting or searching -- plain and simple). So, when adding new entries,
we must increase the count of records in our book. The address book
information is stored in two global variables, record which indicates
the current record we are editing, viewing, adding, removing, etc., and
recordCount which is the total number of records in our book. When we
add a new record, both of these must be incremented, since they both
start at 0. Our first record will be #1, and when we have it, we will
have 1 record(s). Hopefully that made sense. It's fairly well commented,
so this shouldn't be too problematic, so long as you take a look at
addybook.h and the init() function first (which we didn't do, but you
should).
Now that we have a way of getting data for an address book entry, we
need some way of storing those entries. Again, for simplicity (and for
memory concerns, since we have so little of it), we only keep one record
in memory at any time. The rest of the records (so long as we have
enough space to store them on the file) are stored in a file outside the
program memory. We read them back into memory (only one at a time) when
we need them. So, let's take a look at the writeRecords() function,
which comprises a major part of the program, and is the core of the file
I/O section.
// write the address book records to file
// action tells us how to handle the current data (new record, remove old, or edit current)
short int writeRecords(short int action) {
FILE *in = NULL, *out = NULL;
short int tempRecordCount = recordCount, adjust = FALSE, copy = TRUE;
PERSON temp;
// rename the address file to a temp filename
rename(ADDRESSBOOK,ADDRESSTEMP);
// open the input file to copy the old records
in = fopen(ADDRESSTEMP,"rb");
// open the new address book file
if ((out = fopen(ADDRESSBOOK,"wb")) == NULL) {
fclose(in);
return FALSE;
}
// decrement the record count if we're removing an entry
if (action == REMOVE_ENTRY) {
tempRecordCount--;
}
// write the new record count
fwrite(&tempRecordCount,sizeof(short int),1,out);
// if we need to copy over old data...
if (in != NULL) {
// find out how many records are already stored
fread(&tempRecordCount,sizeof(short int),1,in);
// loop through the old records and recopy them
while (tempRecordCount-- > 0) {
// read in the old record
memset(&temp,0,sizeof(PERSON));
fread(&temp,sizeof(PERSON),1,in);
// are we removing or editing this record?
if (temp.id == p->id) {
if (action == REMOVE_ENTRY) {
// skip this entry -- adjust additional id's
adjust = TRUE;
copy = FALSE;
} else if (action == EDIT_ENTRY) {
// replace old entry data with new data
memcpy(&temp,p,sizeof(PERSON));
}
}
// decrement the id's of successive items (after the removed one)
if (adjust) {
temp.id--;
}
// if we're copying the record, write it
if (copy) {
// write the old record to the new book file
fwrite(&temp,sizeof(PERSON),1,out);
} else {
// only one reason not to copy, so reenable copying after
// we skip the one we're not copying
copy = TRUE;
}
};
// close the input file
fclose(in);
}
// if we're adding an entry, write it to the file at the end
if (action == NEW_ENTRY) {
fwrite(p,sizeof(PERSON),1,out);
}
// now write the file tag
fputc(0,out);
fputs("ADDY",out);
fputc(0,out);
fputc(OTH_TAG,out);
fclose(out);
// remove the temporary records
unlink(ADDRESSTEMP);
// if we didn't have an error up to now, we're good to go
return TRUE;
}
The core of the file I/O section looks very complicated, but it is not.
It merely takes some time to look over to understand how it works
together in three different capacities. This function will be called if
we want to add a new record, remove an old record, or edit a record.
Since we have a very limited memory set to use, it is better to keep
only one entry in memory at any one time, and load others from 'disk' as
needed.
Since we will need to remove entries as well as edit them, we will need
to keep two copies of the address book. One will be for reading the old
records in that we don't have in memory, and the other is for permanent
storage. The first copy is just a temp. The filenames are stored in
addybook.h with the names ADDRESSBOOK and ADDRESSTEMP for simplicity.
Since we will be reading from one file to copy into another file, we
will need two file handles, *in and *out. Remember that file handles are
always pointers because the memory is dynamically allocated by the
fopen() function. This is also why we must never forget to close a file
handle, not just because it's bad style, but because we'll lose memory
every time the program is run.
// rename the address file to a temp filename
rename(ADDRESSBOOK,ADDRESSTEMP);
The first step is to rename our permanent book to the temp file. This is
so we can read in the old entries from that file and copy them over.
Since we only keep one entry in memory at a time, we will need to read
the old entries in and copy them over. The other reason we do this is
due to a 'feature' of the TIGCC library which does not allow in-file
editing with custom tags. If we overwrite data inside a file, we will
lose the file tag (even if we rewrite it). I have been told by Zeljko
Juric that this is not a bug. Nonetheless, we also must deal with
removing file entries, and it's hard to cut data out of a file. It's
easier to simply recopy the relevant parts to a new file.
We should probably do error checking to make sure the temp file name is
not taken, but since it's unlikely, we'll ignore it for now. Now (since
we are assuming the rename was successful), we now have a file we can
read the old entries from, and write them to the permanent address book
file later, which we will be doing very soon.
// open the input file to copy the old records
in = fopen(ADDRESSTEMP,"rb");
// open the new address book file
if ((out = fopen(ADDRESSBOOK,"wb")) == NULL) {
fclose(in);
return FALSE;
}
Unless we are working with text files, we always work in binary mode.
Since we will be reading from the temp file and writing to the address
book file, we open the *in file for read-binary mode, and the *out file
for write-binary. If for some reason we cannot open the *out file, then
we have a big problem, and should return false.
// decrement the record count if we're removing an entry
if (action == REMOVE_ENTRY) {
tempRecordCount--;
}
// write the new record count
fwrite(&tempRecordCount,sizeof(short int),1,out);
The next step is to determine how many entries the address book has. We
keep track of that in the recordCount variable, but you will see that we
can't change that right away when we remove a record, so we keep a
temporary count stored inside the function. If we are removing the
current entry, we obviously have one less than we had a minute ago.
The address book, as all binary files must, has a very rigid file
structure. This is so we know how to interpret the data. All data is
meaningless if we don't know how to interpret it. For the purposes of
this program, I've chosen a simple structure which stores the number of
records in the address book, followed by each entry in the book sorted
by their id number (from 1 to whatever). This way, when we load the
program and run the init() routine, we can find out how many records are
in the book, and read in the first one without having to go through the
whole file. For more complicated data storage, you might store an entire
record structure with information about the file at the top. This is
called a file header.
I hope you remember how the fwrite() function works, because it's very
important in file I/O. Supply a pointer to the data, the size of the
data, how many sets of the data exist, and the file pointer to write the
data to. This is documented in the TIGCC docs if you forget.
// if we need to copy over old data...
if (in != NULL) {
// find out how many records are already stored
fread(&tempRecordCount,sizeof(short int),1,in);
Assuming our in-file is there, we probably have records to copy over
(unless we have removed all the records from the book, in which case we
still have a file, but it says there are 0 records inside it).
Now we need to find out how many old entries we have, so we will read
the count from the temp file. Now we are ready to loop through the
entries and copy them all over.
// loop through the old records and recopy them
while (tempRecordCount-- > 0) {
First, the while loop sets us up to loop until there are no more entries
in the file. The semantics are very subtle, so they must be examined
closely. First, we need to understand the difference between --var and
var--. The operators do the same thing, but they have different
precedence. First, if we look at the statement while (tempRecordCount--
> 0), we have 2 operators and 2 values. -- and > are the
operators, and tempRecordCount and 0 are the values. Because we need to
interpret the meaning, we need to know which ones get done at which
time, and how. First, lookup the values of the variables. Let's say
tempRecordCount is 3. So, we can rewrite the statement as (3-- > 0).
Now, we still have to know which operator is handled first. If -- is
handled first, then the statement should read (2 > 0), which is
clearly true, but if > is handled first, then it becomes (3 > 0),
and then afterwards, tempRecordCount becomes 2. Now, you may think this
doesn't make any difference. Well, here's the subtlety. Let's say
tempRecordCount is equal to 1. Now this would mean that there is 1
record in the file. Let's also assume -- is handled first. So, the
statement would them read (0 > 0), which is not true, and the while
body never would be executed. Now we've ruined it. We had a whole record
we never evaluated. Fortunately, this is not the case. > has a higher
precedence than var--. So, the statement would actually translate out to
(1 > 0); tempRecordCount = tempRecordCount - 1; Now it evaluates to
true, and we execute the body of the while loop. Now, here is the even
more fun part. We did not use --var, but if we had, then the statement
would read (0 > 0) and not execute the while loop. This is because
--var has a higher precedence than >, and will thus be executed
first.
Okay, now that we are finished with that complex description of operator
precedence, let's move on to the body of the while loop.
// read in the old record
memset(&temp,0,sizeof(PERSON));
fread(&temp,sizeof(PERSON),1,in);
Okay, the first step in the copy process is to erase our temporary
PERSON structure and read in an entry from the file. Reading is just
like writing, we just use fread() instead of fwrite(). The arguments are
identical. Better code would check to make sure we didn't get any errors
in reading data, but we'll leave it be for now.
// are we removing or editing this record?
if (temp.id == p->id) {
if (action == REMOVE_ENTRY) {
// skip this entry -- adjust additional id's
adjust = TRUE;
copy = FALSE;
} else if (action == EDIT_ENTRY) {
// replace old entry data with new data
memcpy(&temp,p,sizeof(PERSON));
}
}
This is where the three actions force us to alter what we do slightly.
Remember that we are working with a sole entry record in memory, so the
action is always referring to this entry. We are either adding it into
the address book (a new record), removing it from the address book, or
editing the record to update the information. If we are editing or
removing, we need to check to see if the entry we are reading from the
file matches the entry we are currently working with. If it does (i.e.
the id's match on our current person structure p and our temp
structure), then we need to set flags to either skip or replace this
entry with the entry we have. We also need to set a mark here if we are
removing things to change the id's of all successive entries. If we
have entries 1, 2, 3, 4, and 5, and we remove 2, then 3, 4, and 5 should
become 2, 3, and 4, just to keep it simple. It also helps us search for
entries when we load them from the file, as you will see later. The
memcpy() function copies the data in our current p PERSON to the temp
person. We do this because we are going to write the temp person to the
file, and there's no reason to make an exception here. It seems a little
easier to copy the data here rather than write the data in this one
special location, then tell it not to write the temp later for this
special reason.
We haven't used memcpy before, but it is similar to memset. But rather
than setting a number of bytes to a value, we copy a number of bytes to
our variable. Consult the TIGCC docs for more information. It is defined
in the mem.h header.
// decrement the id's of successive items (after the removed one)
if (adjust) {
temp.id--;
}
This is just that part where we decrement the successive ids for entries
that are past the removed entry. So, delete entry 2, and now 3 becomes
2, and 4 becomes 3, and so on.
// if we're copying the record, write it
if (copy) {
// write the old record to the new book file
fwrite(&temp,sizeof(PERSON),1,out);
} else {
// only one reason not to copy, so reenable copying after
// we skip the one we're not copying
copy = TRUE;
}
Finally, we're ready to copy the old records. Remember that if we are
removing a record, we don't want to copy it, so we check the copy
variable to see if we need to copy. And since we only skip one record,
we re-enable copying afterwards so it will copy successive entries past
the removed one. So, skip 2, but not 3 or 4 or 5, etc.
The fwrite() function should be becoming second nature, but we'll go
over it one more time. We pass the address of the data, the size of the
data record, how many records are at that address (for arrays, it could
be more than 1), and finally the file we are writing to, in this case
*out. We already went over the case where removing an entry causes us
not to copy it, but we only want to remove one, so we should re-enable
copying after we are through.
// if we're adding an entry, write it to the file at the end
if (action == NEW_ENTRY) {
fwrite(p,sizeof(PERSON),1,out);
}
// now write the file tag
fputc(0,out);
fputs("ADDY",out);
fputc(0,out);
fputc(OTH_TAG,out);
fclose(out);
// remove the temporary records
unlink(ADDRESSTEMP);
Okay, the last special case, adding a new entry causes us to add the
entry at the end of the file. So, simply write the current entry last,
and we're finished. Now we write the file tag, which in this case we
will call 'ADDY', and close the *out file. Note that custom file tags
can be 1-4 characters. Since we are done copying the old records, we no
longer need the temp file, so we will remove it now. The unlink()
function deletes a file. The name actually has meaning, but it's
complicated, so just remember that unlink means to delete a file.
Now that we can add, remove, and edit entries, let's take a look at
displaying entries.
// display the current address book entry
void displayEntry(void) {
HANDLE dlg = H_NULL;
char buffer[51];
// we can't display if there is nothing to display
if (recordCount < 1) {
dlgError(RECORD_ERROR,NO_RECORDS_ERROR);
return;
}
// allocate memory for the display dialog
if ((dlg = DialogNewSimple(DLG_DISPLAY_WIDTH,DLG_DISPLAY_HEIGHT)) == H_NULL) {
dlgError(DMA_ERROR,MEM_ERROR);
return;
}
// setup the dialog box window
sprintf(buffer,"Address Book Record #%hd",p->id);
DialogAddTitle(dlg,buffer,BT_OK,BT_NONE);
sprintf(buffer,"Name: %s",p->name);
DialogAddText(dlg,5,15,buffer);
sprintf(buffer,"Address: %s",p->address);
DialogAddText(dlg,5,25,buffer);
sprintf(buffer,"City: %s",p->city);
DialogAddText(dlg,5,35,buffer);
sprintf(buffer,"State: %s Zip: %s",p->state,p->zipcode);
DialogAddText(dlg,5,45,buffer);
sprintf(buffer,"Phone: %s",p->phone);
DialogAddText(dlg,5,55,buffer);
sprintf(buffer,"Email: %s",p->email);
DialogAddText(dlg,5,65,buffer);
// display the dialog and free the memory when done
DialogDo(dlg,CENTER,CENTER,NULL,NULL);
HeapFree(dlg);
}
Displaying an entry is very simple, but to make it look better, we will
encapsulate displays in a dialog box. You should be very familiar with
how dialog boxes work by now, so we won't spend too much time here. The
important concepts to remember are that we already have a global PERSON
structure p which contains the information we want to display. If we
have no records yet, we don't display anything. There is never any time
where PERSON p is empty unless we have no records. All we have to do is
add text strings and display the box. Not even any complex input to work
with.
Removing an entry is a little more complicated than I let on at first,
so we next should examine the selectEntry() function which allows the
user to select a different 'current' entry in the address book, so long
as there are at least 2 records in the book.
// select a different entry from the book by id
short int selectEntry(void) {
HANDLE dlg = H_NULL;
char buffer[81];
short int done = FALSE, entry;
// we need more than 2 records to select a different record
if (recordCount < 2) {
dlgError(RECORD_ERROR,NEED_MORE_ERROR);
return TRUE;
}
// open the dialog box
if ((dlg = DialogNewSimple(DLG_MAIN_WIDTH,DLG_MAIN_HEIGHT)) == H_NULL) {
return FALSE;
}
// add the dialog title
DialogAddTitle(dlg,"Select Entry",BT_OK,BT_CANCEL);
// add the dialog directions
sprintf(buffer,"Choose an entry between 1 and %hu",recordCount);
DialogAddText(dlg,5,15,buffer);
// add the entry request
DialogAddRequest(dlg,5,25,"Entry:",0,2,5);
// erase the buffer variable
memset(buffer,0,81);
// ask for the entry id until we get one in a valid range
while (!done) {
if (DialogDo(dlg,CENTER,CENTER,buffer,NULL) == KEY_ENTER) {
entry = atoi(buffer);
--entry;
if (entry >= 0 && entry < recordCount) {
done = TRUE;
}
} else {
// exit the loop without choosing an entry
entry = -1;
done = TRUE;
}
}
// free the memory used by the dialog box
HeapFree(dlg);
// if we choose one, then load it from the file
if (entry != -1) {
return loadEntry(entry);
}
return TRUE;
}
The first step in selecting an entry is to create a selection dialog
box. Maybe dialogs are overused, but they sure seem handy to me. So, we
create the dialog box with a single entry for which address record the
user wants to view. To keep it simple, we'll force the user to know
which ID number the record is. A better address book would let them
search by name or number or some such thing, but that would be a little
much for this little example.
When the user selects an entry, we do two things. First, all dialog
input comes in as a string, which you already knew, but we need it as a
number. To convert a string to an integer, we use the atoi() function.
Second, we are using numbers that start at 1. Most humans like to think
that numbering starts at 1 and goes up, but computer science knows that
numbers start at 0, so even though we tell the user numbers start at
one, we need to treat the numbers as starting from 0. So, subtract the
entry by 1 and we have a 0-based number. Now we can use math. This will
help greatly in a moment.
Now that we have a selection, we need to make sure it's a valid
selection. So just make sure it's between 0 and recordCount. That should
be simple enough. Now, the final act, assuming we have a valid
selection, is to load that entry from the file. We call the loadEntry()
function to do that for us.
// load an address book record from file
short int loadEntry(short int entry) {
FILE *f = NULL;
// return FALSE if we can't open the file
if ((f = fopen(ADDRESSBOOK,"rb")) == NULL) {
return FALSE;
}
// seek out the correct entry in the file
if (fseek(f,(sizeof(short int) + (entry * sizeof(PERSON))),SEEK_SET) == 0) {
// erase the person structure
memset(p,0,sizeof(PERSON));
fread(p,sizeof(PERSON),1,f);
}
// close the file
fclose(f);
// reset the record to the current record entry
record = entry + 1;
return TRUE;
}
The loadEntry() function introduces a new function for file I/O that is
very useful in files with multiple records. The fseek() function.
fseek() allows us to go to a certain position (called an offset) in the
file and start reading from there. Note that you should not overwrite
data in a file with a custom file tag. This is because of the way
fwrite() works. The reasons are complex, but if you overwrite data in a
file with a custom file tag, it will lose the tag, and possibly some
data in the file. This is just something you have to deal with. Our
workaround is to read in all our old data and copy it over replacing
data before it is added to the new file, instead of overwriting old
data.
The fseek() function takes three arguments, the file pointer (*f in this
function), how much data to skip, and where to skip from.
Since we have already covered the removal of an entry in the
writeRecords() function, let's examine the removeEntry() function to
complete the cycle. Remember that our file is stored in a very specific
way. This is done so that we can use the data effectively. Binary data
has NO meaning to a computer (er, calculator), so we need to know what
the data means. It is because we have a consistent file structure that
we know where in a file a certain record will be.
Remember that the file structure looks like this: first store the record
count, then store each record in sequential order. So, first record 1,
then 2, then 3, etc. So, if we want to find record 4, we need to skip
the record count and the first three records. To tell the fseek()
function this, we need to tell it how many bytes to skip. We know that
the recordCount is a short int (look in addybook.h), and each person
structure takes up some other amount of space. Well, the sizeof()
operator can tell us exactly how much space a variable takes up. So,
just tell it sizeof(short int) + (sizeof(PERSON) * entry). So, we skip x
entries and one short int and we're right at the part we want to read.
The last argument tells us where to start from in our seek. SEEK_SET
means start at the beginning of the file, SEEK_END means start at the
end of the file, and SEEK_CUR tells us to start wherever we are at
currently. We will start most often from the beginning, so we can just
use SEEK_SET. So, fseek(f,sizeof(short int) + (entry *
sizeof(PERSON)),SEEK_SET) means skip x entries plus the record count and
get to the entry we want to read. This is much easier than reading each
one individually until we reach the right one.
All that is left is to read the entry in using fread(). Finally, we
reset the current record pointer, since it will have changed after
selecting a different entry. Now we can move on to the final task,
removing an entry. We already covered the file I/O half, but we couldn't
take a look at the pretext until we covered the other functions.
// remove the current entry from the address book
void removeEntry(void) {
// we cannot remove from an empty list
if (recordCount == 0) {
dlgError(RECORD_ERROR,NO_RECORDS_ERROR);
return;
}
// display the entry they are about to remove
displayEntry();
// allow the user to cancel by pressing ESC
if (dlgError(CONFIRM_WARNING,CONFIRM_REMOVE) == KEY_ENTER) {
// rewrite the address book file
writeRecords(REMOVE_ENTRY);
// decrement the record count
--recordCount;
// decrement the current record
if (record > 0) {
--record;
}
}
// load the closest entry to that removed
if (recordCount > 0) {
// reload the entry closest to the one we removed
loadEntry((record > 0) ? (record - 1) : record);
}
}
The first step in removal is not to trust the user. Since people often
make mistakes, it's a good idea to tell them exactly what the program
will do if they press ENTER. In this case, we show them the data they
are about to remove (using the displayEntry() function), then ask them
if they are sure. Only then do we proceed.
The next steps are where it gets confusing though, because we are
removing and changing entry id's, record counts, and the current record.
But when we do all these things influences how the program will work. We
can't change anything until the entry has been removed, so don't change
the record count or the current record until after the call to
writeRecords() is finished. Assuming we have an entry to remove, we can
always decrement the record and recordCount, but what do we do now that
we've removed an entry. Remember that when we selected an entry, we need
to load another one from disk. Since we just removed the current record,
if we still have more records, we should load another one from disk too.
So, we call the loadEntry() function, but remember that in selectEntry()
we had to give it record - 1, so we could do math correctly. Well, this
is much the same, except that in our case, record might already be 0
(because if we were looking at record 1, and we decrement the record
earlier, now it's at 0), so we need to pass either record, or record -
1. We can use the ternary operator to make sure we don't load an invalid
entry. (record > 0) ? (record - 1) : record means if (record > 0),
then send record - 1, otherwise, send record.
We haven't used the ternary operator before, but it's kind of a compact
if statement. Its syntax is this: (condition) ? (do this if condition is
true) : (otherwise do this).
That's all there is to this program. See, it was simple. In case you
were wondering, this program took me roughly 8 hours to complete (that's
actual work-time, not time from start to finish, because I had a lot of
in-between time). This also includes debug time, not just write time.
Write time was about 2 hours. The other 6 were debug time, so don't feel
bad if you think it's taking forever to find a bug. I stuck this program
away after the first night when I couldn't fix the bugs. Then I lost it
for two months, then it took another 4 hours of debugging to get it
working proper. I have rewritten the write and read routines at least 5
times each. So, don't feel bad if you have a program error. You will
spend more than half your time debugging your work. Often times up to
90% of the development time is spent on debugging. Writing is not hard,
it's fixing it that's hard.