The Functions of the HomeUtility namespace

Now that we've covered that new construct, let's take a look at the functions declared in the HomeUtility namespace, starting with ReadDoubleFromLine, which is shown in Figure 13.10.

Figure 13.10. HomeUtility::ReadDoubleFromLine (from codehmutil1.cpp)
bool HomeUtility::ReadDoubleFromLine(istream& is, double& Result)
{
 double temp;
 xstring garbage;

 is >> Result;
 
 return CheckNumericInput(is);
}

This function isn't terribly complicated. It uses the standard library input operator >> to read the data from an input stream into a double called Result, calls a function named CheckNumericInput (shown in Figure 13.15 on page 931), and returns the result of that function to the user.

However, there is one interesting detail about the way this function works: the result of the input operation is not the return value of the function. Instead, the function returns true or false to indicate the success or failure of the operation, and the actual data is returned to the caller via a reference argument.

Returning More Than One Value from a Function

Why did I design it this way? Because a function can have only one return value, but in this case I need to return two separate pieces of information: whether the operation was successful, and what the value read actually was. So I decided to return the status (i.e., success or failure) as the return value of the function and use a reference argument to return the actual value read.

The next function, ReadLongFromLine, which is shown in Figure 13.10, is almost identical to the previous one, except of course for the type of data it reads, so I don't think we need to analyze it further.

Figure 13.11. HomeUtility::ReadLongFromLine (from codehmutil1.cpp)
bool HomeUtility::ReadLongFromLine(istream& is, long& Result)
{
 long temp;
 xstring garbage;

 is >> Result;

 return CheckNumericInput(is);
}

Reading a Date Field

The next function is HomeUtility::ReadDateFromLine, which is shown in Figure 13.12.

Figure 13.12. HomeUtility::ReadDateFromLine (from codehmutil1.cpp)
bool HomeUtility::ReadDateFromLine(istream& is, long& Date)
{
 bool result = ReadLongFromLine(is, Date);

 if (result == false)
   return false;

 if ((Date < 18000101) && (Date != 0))
    {
    xstring Message = "Date must be either 0 (if unknown)";
    Message = Message + " or a number in the form YYYYMMDD";
    Message = Message + "
";
    Message = Message + "which is on or after 18000101";
    Message = Message + " (January 1st, 1800)";
    HandleError(Message);
    result = false;
    }

   return result;
}

This isn't a very complicated function either. It starts by calling ReadLongFromLine to get the “date” from the user. If that function returns false, that means the user typed in something illegal. In that case, ReadDateFromLine returns the bad news to the calling function in the form of a false return value. If the input value is a valid number, the second if statement checks whether it is in the range that I've decided to allow (after consultation with Susan): on or after January 1, 1800, or 0. The latter is needed to handle the very real possibility that Susan pointed out; the user may not know when the object was acquired. If the date fails this test, we return false to indicate that it's invalid; otherwise, the return value is true, and the actual date is set via the reference argument Date.

Even though I had discussed the range of legal dates with Susan, she had a couple of additional questions about this notion.

Susan: Why do we need a limit on the date at all?

Steve: To reduce the likelihood of an error. The chance that the user has had the object for more than a couple of hundred years is smaller than the chance that the user typed the date in incorrectly. At least, that's the way I figure it.

Susan: What about collectors who have extremely old objects?

Steve: Well, that is certainly possible. However, the field we're discussing here represents the date that the user acquired the object, not how old the object is. However, that might very well be a useful piece of information, especially for collectibles, so I'll add an exercise to include such a field in a collectibles type.

The next function we will look at is IgnoreTillCR, which is shown in Figure 13.13.

Figure 13.13. HomeUtility::IgnoreTillCR (from codehmutil1.cpp)
void HomeUtility::IgnoreTillCR(istream& is)
{
 is.ignore(numeric_limits<streamsize>::max(),'
'),
}

This is a simple function that just calls the ignore function of the istream class to ignore as many characters as might potentially be in cin, or up to the first newline (or “carriage return”) character, whichever comes first. In fact, it's so simple that you might wonder why I even bothered making it a function.

The reason I made this a function wasn't to reduce typing but to localize the knowledge of how it works in one place. That way, if I decided to use a different mechanism to ignore excess data, I could change just this one function rather than trying to find every place that I had used the ignore function for that purpose.

Susan had some questions about this function.

Susan: When do we need this function again?

Steve: To ignore any extra characters that the user might type in before hitting the ENTER key. Otherwise, those characters will be interpreted as commands after ENTER is pressed.

Susan: OK, but what does numeric_limits<streamsize>::max() mean?

Steve: That's a function from the standard library that returns the maximum number of characters that could ever be in a stream. If we ignore that many characters, we can be sure that there aren't any left in the stream.

Susan: Yes, but how many characters is that?

Steve: Using the compiler on the CD in the back of the book, 2147483647. I don't suppose any user is going to type that many characters before he hits the ENTER key, do you?

Next, we have HomeUtility:HandleError, which is the common error-handling function used in the rest of the program whenever we simply want to display an error message, wait for the user to hit ENTER, and then continue with the program. The code for HandleError is shown in Figure 13.14.

Figure 13.14. HomeUtility::HandleError (from codehmutil1.cpp)
void HomeUtility::HandleError(const xstring& Message)
{
 cout << endl;
 cout << Message << endl;
 cin.clear();
 IgnoreTillCR();
 cout << "Please hit ENTER to continue." << endl;
 cin.get();
}

This is a simple function too. It starts by moving the cursor to the next line on the screen (so the user can tell where the error message begins). Then it displays the message, using the cin.clear() function call to clean up any errors that might have occurred in the cin input stream and calling IgnoreTillCR() to ignore any random characters that might be left in cin from the user's last input. Next, it displays the message “Please hit ENTER to continue” and waits for the user to hit a key, which is accepted via the cin.get() function call.

Susan had a couple of comments and questions about this function:

Susan: So, you have to clean up the garbage in the input stream before you can use it again?

Steve: Yes, we have to reset the status of the stream before we can read from it again. This prevents a program from continuing to read garbage from the stream without realizing it.

Susan: What error message does this function display?

Steve: Whatever error message the calling function specifies.

Susan: Oh, I see. It's generic, not specific to a particular situation.

Steve: Yes, that's exactly right.

The next utility function is HomeUtility::CheckNumericInput, which is called after the user has typed in a numeric value. The code for this function is shown in Figure 13.15.

Figure 13.15. HomeUtility::CheckNumericInput (from codehmutil1.cpp)
bool HomeUtility::CheckNumericInput(istream& is)
{
 int NextChar = is.peek();
 if (NextChar == '
' || NextChar == ' ')
 IgnoreTillCR(is);
 else
  {
  xstring garbage;
  is.clear();
  is >> garbage;
  xstring Message = "Illegal data in numeric value: ";
  Message = Message + garbage;
  HandleError(Message);
  return false;
  }

 return true;
}

While this is a bit more complex than the functions we've looked at so far in this namespace, it shouldn't be hard to follow. After declaring some variables, the code starts by calling the peek function to see whether the next character in the input stream is a newline (' ') character or a space (' '). If it is, we can tell that the entry is a valid number, so we don't have to deal with an error. How do we know this?

Reading Numeric Data from a Line Oriented File

We know this because the versions of operator >> that read numeric (short, int, long, float, or double) values stop when they get to a character that doesn't belong in a number. In the current case, we've tried to read a number either from the keyboard or an input file, which is supposed to be terminated by a newline (ENTER). If the data that we've read is a valid numeric value, all of the characters up to (but not including) the ENTER key will already have been used by operator >> in setting the new value of the numeric variable to the right of the >>. Therefore, the next character in the input stream should be the ENTER (which is represented in C and C++ by the newline character, ' '). However, if the data includes one or more characters that don't belong in a number, the first of those characters will be the next character in the input stream after operator >> finishes reading the value for the numeric variable to its right. Therefore, if the next character isn't a newline or a space, we know the user typed in something illegal.

In that case, we should let the user know exactly what the illegal characters were. Therefore, after we call the clear function to clear the error status of the istream, the next statement, cin >> garbage, uses operator >> to read the rest of the characters in the input line into a xstring called garbage, which will end up holding everything from the first illegal character up to but not including a newline. Then we construct the whole error message by concatenating the illegal characters to the end of the message “Illegal data in numeric value: ”. Finally, we call HandleError to display the message and wait for the user to press ENTER.

A Tradeoff between Convenience and Error Control

If you're following this closely, you may have noticed that I'm not enforcing the rules for input very strictly here. Why do I allow a space at the end of a number when the number is supposed to be followed by a newline?

There's an interesting design issue here. Spaces are invisible, which means the error message “Illegal data in numeric value: ” wouldn't be very informative to the user. And who cares if the user hits the space bar by accident after entering his numeric value?

But there's another reason that I allow a space at the end of a numeric value: this function is also used to read data from a file, not just interactively. Imagine how annoying it would be if you had to try to figure out which line in a big data file ends with a space! I decided being overly strict on this would cause more trouble than it prevented.

Susan wanted to go over this in more detail.

Susan: Let me see if I understand this. If the user typed in illegal characters, the input operation would stop at that point?

Steve: Right.

Susan: Would that cause an error message?

Steve: No, not by itself; it just sets an error condition in the input stream. It's up to us to produce the error message and that's what we're doing here.

Susan: So the garbage characters have been left in cin?

Steve: Yes. That's why we have to call clear before we can use cin again.

Single Keystroke Data Entry

The next function we're going to examine in the HomeUtility namespace, GetNumberOrEnter (Figure 13.16), is somewhat more complicated than the ones we've been looking at so far. That's because it deals with getting input from the user one keystroke at a time.

Figure 13.16. HomeUtility::GetNumberOrEnter (from codehmutil1.cpp)
short HomeUtility::GetNumberOrEnter(bool AllowArrows)
{
 int key;
 char keychar;
 short FoundItemNumber;

 cout.flush();

 for (;;)
  {
  key = windos::getkey();
  keychar = key;

  if (key == K_Return)
    return e_Return;

  if (AllowArrows)
     {
     if (key == K_Up)
       return e_Up;
     if (key == K_Down)
       return e_Down;
     }

   if ((key < '0') || (key > '9'))
     continue;

   cout << keychar;
   cout.flush();

   FoundItemNumber = key - '0';

   for (;;)
    {
    key = windos::getkey();
    keychar = key;

    if (key == K_BackSpace)
      {
      cout << keychar;
      cout.flush();
      cout << ' ';
      cout << keychar;
      cout.flush();
      FoundItemNumber /= 10;
      continue;
      }

   if (key == K_Return)
     {
     cout << keychar;
     cout.flush();
     return FoundItemNumber;
     }

   if ((key < '0') || (key > '9'))
      continue;

    cout << keychar;
    cout.flush();

    FoundItemNumber = FoundItemNumber * 10 + (key - '0'),
    }
 }
}

The first thing to note about this function is its argument, which is a bool called AllowArrows. If you look at the definition of the interface for the HomeUtility namespace (Figure 13.5 on page 921), you'll notice that this argument has a default value, which is false. The purpose of this argument is to determine whether the up and down arrow keys will be accepted as valid inputs; if the argument is false, they will be ignored, whereas if the argument is true and the user presses one of these keys, the function will return a code indicating which one. As you'll see, accepting the arrow keys will be useful in the last function in this namespace, SelectItem.

The function starts by declaring some variables called key, keychar, and FoundItemNumber. The first of these is an int, which is a type we haven't used very much because it varies in size from one compiler to another.[7] However, in this case I'm going to use an int variable to hold the return value from windos::getkey(), which is the function that returns the key code for a key that has been pressed by the user. Since windos::getkey is defined to return an int value, that's the appropriate type for a variable that holds its return value.[8]

[7] Actually, as I have pointed out previously, all of the data types in C++ can vary in size from one compiler to another. However, int is more likely to be different in size from one compiler to another than, for example, short.

[8] Note that windos::getkey is not a standard library function.

This windos::getkey function is very useful because it allows us to get input from the user without having to wait for him or her to hit ENTER. Under most circumstances, it's much easier to use the >> operator to get input from the user via the keyboard, but that approach has a serious limitation; it prevents us from giving the user immediate feedback. Such feedback is essential if we are going to allow the user to access varying segments of a large quantity of information in an intuitive manner, as you'll see when you try the program. Forcing the user to hit ENTER before getting any feedback would be very inconvenient, and we have to worry about how easy our programs are to use if we want happy users. Therefore, I've written this GetNumberOrEnter function to allow the user to receive immediate gratification when using our home inventory program.

This might be a good time for you to try the program out for yourself so you can see what I'm talking about. First, you have to compile it by following the compilation instructions on the CD. Then type hmtst8 to run it. Try out the program for a while and see how you like it before coming back to the discussion. Pay special attention to the “select” and “edit” functions, which allow you to see some of the items in the list, and use the up and down arrows to see more of the list. That behavior is implemented partly by GetNumberOrEnter and partly by the SelectItem function.

Now that we've seen how GetNumberOrEnter is used, let's get back to its implementation. As I've already mentioned, this is a somewhat complicated function, because it has to deal with the intricacies of reading data from the keyboard one character at a time. First, we call flush to make sure that any characters that have been written to cout have actually been sent to the screen. Then we start the “endless” loop that will allow the user to type as many keys as necessary to enter the data item. Why do I say “data item” rather than “number”? Because the user can type keys that aren't numbers at all, including the up or down arrow to select a different position in the list of items that appears on the screen.

Susan had a question about the flush function.

Susan: I don't remember seeing this flush function before. What does it do?

Steve: It makes sure that everything we were planning to display on the screen is written out before we ask for any input from the user. Ordinarily, characters that are written to a stream are not sent immediately to the output device to which the stream is attached, because that is extremely inefficient. Instead, the characters are collected in an output buffer until there are enough of them to be worth sending out; this technique is called buffering.[9] However, in this case we have to make sure that any characters that were supposed to be displayed have been displayed already, because we are going to be taking characters from the user and displaying them immediately. Any leftover characters would just confuse the user.

[9] This buffering technique is also applied to input. Rather than read one character at a time from an input file, the operating system reads a number of characters at a time and gives them to the program when it asks for them. This greatly improves the efficiency of reading from a file; however, it is much less useful when reading data from the keyboard, as the user doesn't know what to type before we provide a prompt.

Now let's continue with the analysis of the code in GetNumberOrEnter. The first statement inside the “endless” loop is key = windos::getkey();. This statement calls a function called windos::getkey, which waits for the user to type a key and then reads it. The return value of getkey is the ASCII code for the key the user hit. Therefore, that statement should be relatively simple to understand.

Echoing the Typed Character to the Screen

However, the same is not true of the statement keychar = key;. Why would we want to assign one variable the same value as that of another? Because of the way that getkey works. Unlike normal cin input, getkey input is “silent”; that is, keys that are pressed by the user do not produce any visible results by themselves, so we have to display each character on the screen as the user types it. But to display a character on the screen via cout, the variable or expression to be displayed must have the type char, whereas the variable key is an int. If we were to write the statement, cout << key;, the program would display the ASCII numeric value for the key the user pressed. Needless to say, this would not be what the user expected; therefore, we have to copy the key value from an int variable to a char variable before we display it so that the user sees something intelligible on the screen.

Susan thought such cryptic output might have some use. Also, she wanted to know what would happen if the key value wouldn't fit in a char.

Susan: If we displayed the ASCII value instead of the character, it would be sort of a secret code, wouldn't it?

Steve: Sort of, although not a very secure one. However, it would be fairly effective at confusing the user, which wouldn't be good.

Susan: OK. Now, about copying the value from an int to a char: a char is smaller than an int, right? What if the value was too big?

Steve: The answer is that the part that wouldn't fit would be chopped off. However, in this case we're safe because we know that any key the user types will fit in a char.

The next order of business in this function is to check whether the user has hit the ENTER key. If so, we simply return the enum value e_Return to the calling function to inform it that the user has hit the ENTER key without typing a value.

Assuming that the user has not hit the ENTER key so far, we check the AllowArrows argument to see whether the arrow keys are allowed at this time. If they are, we check to see if either the up arrow or the down arrow has been hit. If it has, we return the appropriate code to tell the calling function that this has occurred so it can scroll the display if necessary.

The next statement after the end of the arrow-handling code is an if statement that checks whether the key that we are handling is in the range '0' to '9'. If the key is outside that range, we use the continue statement to skip back to the beginning of the outer for loop, essentially ignoring any such key. However, if the key is within the numeric digit range, we proceed by using the operator << to send it to the screen. Then we use the flush function of the cout object to ensure that the key has actually been displayed on the screen.

Susan wanted to know why we had to worry about whether the user typed a valid key:

Susan: Why do we have to worry about the user typing in the wrong key? Can't the user take some personal responsibility for typing in the data correctly?

Steve: Anyone can make a mistake, especially when typing in a lot of information in a row. How would you like it if programs would let you type in any kind of garbage without telling you what you are doing was illegal? For example, if you were trying to list something on an internet auction site and you typed the price in as “$1x”, wouldn't you want the auction listing program to tell you that was invalid?

By this point, we have seen the first digit of the value, so we continue by setting FoundItemNumber to the numeric value of that digit, which can be calculated as the ASCII value of the key minus the ASCII value of '0'.

Getting the Rest of the Numeric Value

Now we're ready to enter the inner for loop that gathers all the rest of the numeric digits of the number. This starts with the same “endless” condition, (;;), as the outer loop because we don't know how many times it will have to be executed. Therefore, rather than specify the loop count in the for statement, we use a return statement to exit from the loop (and the function) as soon as the user presses ENTER.

The first two statements in this inner loop are exactly the same as the first two statements in the outer loop, which should not be a surprise as they serve exactly the same function — to get the key from the user and copy it into a char variable for display later. However, the next segment of code is different, because once the user has typed at least one digit, another possibility opens up — editing the value by using the backspace key to erase an erroneous digit. That's the task of the next part of the code, which was a bit more difficult to develop than you might think. The problem is that simply echoing the backspace key to the screen, as we do with other keys, does not work properly because it leaves the erroneous digit visible on the screen.

Susan had a comment about the effect of hitting backspace:

Susan: Every time I hit backspace in a program, it erases what I've just typed in. So why do you say it doesn't?

Steve: Programs have to be written specifically to take care of this issue. Obviously, you've never seen one that doesn't do it correctly, which isn't too surprising, because that sort of error should be caught very early in the development of a program.

Even after we solve this problem, however — by writing a space character on the screen to erase the erroneous digit and backing up again to position the cursor at the correct place for entering the new digit — we have another problem to deal with. Namely, we have to correct the value of the FoundItemNumber variable to account for the erased digit. This requires only that we divide the previous value of that variable by 10 because the remainder will be discarded automatically by the integer division process, effectively eliminating the contribution of the erased digit. Once we have taken care of these details, we are finished with this particular keystroke, so we use a continue statement to proceed to the next execution of the loop.

Susan had a question about this part of the processing also:

Susan: I don't understand what you're doing with the digit values here.

Steve: Well, whenever the user types in a new digit, we have to recompute the value of the number that is being entered. For example, if a 3 is the first digit, the value so far is three. If the second digit is 2, then the total value so far is 32. But how do we calculate that? By multiplying the previous value entered by 10 before adding the new digit. So once the 2 is entered, we multiplying the previously existing value (3) by 10, giving the value 30. Then we add the value of the new digit, 2, giving the value 32.

So what happens if the user hits the backspace key? After erasing the latest digit, we need to correct the stored value of the number being entered so it will correspond to what is on the screen. To do this, all we have to do is divide the current value by 10, as that will effectively eliminate the last digit of the value.

The next possibility to be handled is that of the ENTER key. When we see that key we display it on the screen, which of course causes the cursor to move to the next line. Then we return the value of the FoundItemNumber variable to the calling function, which ends the execution of this function.

By this point in the function, we shouldn't be seeing anything but a digit key. Therefore, any key other than a digit is ignored, as we use the continue statement to skip further processing of such a key.

We're almost done. The last phase of processing is to display the digit key we have received and use it to modify the previous value of the FoundItemNumber variable. The new value of the FoundItemNumber is 10 times the previous value plus the value of the new digit, and that's exactly how the last statement in this function calculates the new value.

Clearing Part of the Screen

I'm sure you'll be happy to hear that the next function we will discuss is a lot simpler than the one we just looked at. This is the ClearRestOfScreen function, which is shown in Figure 13.17. It is used in the final function in the HomeUtility namespace, SelectItem, to clear the part of the screen that function uses for its item display.

Figure 13.17. HomeUtility::ClearRestOfScreen (from codehmutil1.cpp)
void HomeUtility::ClearRestOfScreen(short StartingRow)
{
 short i;

 short HighestRowNumber = windos::ScreenRows();

 for (i = StartingRow; i <= HighestRowNumber; i ++)
   {
   gotoxy(1,i);
   clreol();
   }

  gotoxy(1,StartingRow);
}

This is the first function we've seen that uses a couple of screen-handling functions from the conio library, gotoxy and clreol.[10] The first of these functions moves the cursor to the column (X) and row (Y) specified by its arguments. The first argument is the column number, which for some reason doesn't follow the standard C and C++ convention of starting with 0 but starts at 1. The same is true of the row number, which is the second argument to the function.

[10] Please note that the conio library is not part of standard C++. However, it is impossible to do anything other than the most primitive form of screen output using standard C++ exclusively. Therefore, we have no choice but to use a non-standard library in this case.

The second conio library function that we haven't seen before is the clreol function, which erases everything on a given line of the screen from the cursor position to the end of the line. We call this function for each line from StartingRow to the end of the screen.

Before we can clear the screen one line at a time, however, we need to know when to stop. That's why we need to call the other non-standard library function in this function: windos::ScreenRows. As its name suggests, it returns the number of rows on the screen that we can use for displaying data.[11]

[11] As implemented in the code on the CD, this function returns the value “25”, which obviously is less than the number of screen rows you're likely to have. I left it that way because it helps in testing the program, as you'll see later.

Susan had a few questions about this function.

Susan: What is conio?

Steve: It stands for “console I/O”. Before PCs, it was common for programmers to use a terminal that consisted of a video display screen and a keyboard; this combination was referred to as a “console”.

Susan: How do you pronounce gotoxy?

Steve: It's pronounced “go-to-X-Y”.

Now that we've seen how ClearRestOfScreen works, I should tell you why we need it: to allow the SelectItem function to keep its working area clear of random characters. Of course, it could be used in other situations, but that's how we're using it here.

The Implementation of HomeUtility::SelectItem

The final function in the HomeUtility namespace is SelectItem, whose code is shown in Figure 13.18.

Figure 13.18. The HomeUtility::SelectItem function (from codehmutil1.cpp)
short HomeUtility::SelectItem(const Vec<short>& Number,
 const Vec<xstring>& Name)
{
short FoundItemNumber;
int Row;
int Column;
int RowCount = windos::ScreenRows();
short ItemCount = Name.size();

windos::ScreenGetCursor(Row,Column);
Row ++;

// Max number of rows in scroll area is 1/2 available rows on screen

int RowsAvail = RowCount / 2;

if (RowsAvail > ItemCount)
  RowsAvail = ItemCount;

if (RowsAvail == 0)
  {
  HandleError("No items found.");
  return 0;
  }

short offset = 0;
for (;;)
  {
  ClearRestOfScreen(Row);

  for (short i = offset; i < offset + RowsAvail ; i++)
    cout << setw(5) << Number[i] + 1 << ".  " << Name[i] << endl;

  cout << endl;

  cout << "Type item number to select or ENTER to end." << endl;

  if (ItemCount > RowsAvail)
    cout << "Hit down arrow or up arrow to scroll." << endl;

  cout << endl;

  FoundItemNumber = GetNumberOrEnter(true);
  if (FoundItemNumber == e_Return)
    return 0;

  if (FoundItemNumber == e_Up)
    {
    if (ItemCount > RowsAvail)
      {
      offset --;
      if (offset < 0)
        offset = 0;
      }
    continue;
    }

  if (FoundItemNumber == e_Down)
    {
    if (ItemCount > RowsAvail)
      {
      offset ++;
    if (offset >= (int)(Name.size()-RowsAvail))
       offset = Name.size()-RowsAvail;
    }
   continue;
   }

  for (short i = 0; i < ItemCount; i ++)
    {
    if (FoundItemNumber == Number[i]+1)
      return FoundItemNumber;
    }

   IgnoreTillCR();
   cout << FoundItemNumber <<
    " is an invalid entry. Hit ENTER to continue." << endl;
   IgnoreTillCR();
   return 0;
   }
}

This function, as its name indicates, is the heart of the item selection process. Its arguments are the Number Vec, which contains the indexes into the inventory list of the particular items from which the user is selecting, and the Name Vec, which contains the names of these items and sometimes other information about them (e.g., the category in which each item is found).

The first operation to be performed in this function is determining how many lines there are on the “screen”; I have put the word “screen” in quotes because what we are actually concerned with is only the console window in which our program is running, not the actual physical screen of the monitor. The reason that the number of lines on the screen is important is that we may want to calculate the number of items to be displayed in the “scroll area” (the area where we will be displaying all or part of the list of items) based on the amount of space available on the screen.

Susan had some questions about the way we're handling the screen.

Susan: What's the difference between the monitor and the console window?

Steve: The monitor is the physical device that displays text and graphics. The console window is the virtual device that our programs run in. It's the window that says “Command prompt” at the top.

Susan: So, you even have to tell the program how big the screen is? Doesn't it know anything?

Steve: You have to realize that the same program may run on different machines that are set up differently. Even if everyone were running the same operating system, some people have their console windows set for 25 lines, some for 50 lines, and possibly other numbers of lines as well. It's not very difficult to handle all these different possibilities just by changing the return value of the ScreenRows function.

The next operation is to determine the number of entries in the list of items we're going to display, which we can do by calling the size member function of the Name argument. Of course, we could just as well call the size member function of Number, because that argument has to have the same number of elements as Name has; if they have different numbers of elements, the calling function has made a serious error!

After finding out how many items we are going to handle, the next operation is to determine the current position of the cursor so that we can position the “scroll area” properly below the heading that was displayed by the calling function. To find the current position of the cursor, we call another non-standard library function, windos::ScreenGetCursor. This function requires two arguments, both of which are references to variables. The first argument is a reference to a variable (in this case Row) that will receive the current row number. The second is a reference to a variable (in this case Column) that will receive the current column number. As soon as we have determined the current cursor row, we increment the Row variable to skip a row between the heading and the beginning of the scroll area.

The next segment of code figures out how many items to display at one time; the current version of this function, as I've already mentioned, ignores this calculation and sets the number of items to 5. This makes my (and Susan's) testing more effective because most of the complexity of this routine is in the code that deals with the possibility of having to scroll the items onto and off the screen. This code is used only when there are more items than will fit in the limited space allocated for listing them, so if we have fewer items than will fit on the screen, the code that scrolls the list is not used. As I've mentioned before, code that is never used is never tested and therefore must be assumed not to work.

Once we have decided how many items to list at once and have assigned the appropriate value to the RowsAvail variable, we then check whether the number of items we can display (as specified in that variable) is greater than the total number of items we need to display (as specified by the ItemCount variable). If this is the case, we set the total number to be displayed to the latter value.

If there are no items to be displayed, the user has requested a set of items that doesn't exist, so we call the HandleError routine to tell the user about this situation, and return to the calling function.

Assuming that we have some items to display, we are ready to start displaying them. First, we initialize a variable called offset, which keeps track of what part of the whole list is currently being displayed in the scroll area. It begins at 0 because we start by displaying the first portion of the list, which consists of the number of items that will fit in the scroll area. Of course, if all of the items fit in the scroll area, they will all be displayed.

Once we have initialized offset, we enter the “endless” for loop that displays the elements and asks the user for input until he or she selects something. This for loop begins by calling the ClearRestOfScreen function to clear everything on the screen beyond the current cursor location.

The next two lines of code constitute the for loop that displays the items that are currently in the scroll area. The formatting of this display is somewhat interesting, at least to me, because it took me several tries to get it right. The elements of the display line include the item number, which is one more than the index of the item being displayed (to account for the first index being 0) and the item name. Initially, I simply accepted the default formatting for the item number and the name. However, as soon as the item numbers exceeded 9, I discovered that the names no longer lined up properly because the extra character in the two-digit item number pushed the name over one extra position. To solve this problem, I decided to use the setw manipulator to force the size of the item number to five digits; because the item number is a short, the program is limited to 32767 items, so five digits will be sufficient.

Susan was surprised to hear that I had overlooked this, which led to the following exchange.

Susan: I thought you left bugs in the program on purpose so I would find them.

Steve: No, as a matter of fact, I thought it was working every time I gave it to you to test (all five times). I suppose that illustrates the eternal optimism of software developers!

After displaying the items along with their corresponding item numbers, one on each line, we display the message “Type item number to select or ENTER to end.” Then, if we have more items than will fit in the scroll area, we display another message telling the user about the availability of the up and down arrow keys for scrolling.

Next, we call the GetNumberOrEnter function to get the item number from the user. Note that the argument to that function is true, which means that we want the function to accept the up and down arrows so that the user can hit those keys to tell us to scroll the item list either up or down. Of course, if all the items fit in the scroll area, hitting either of those keys will have no visible effect on the list.

Once we have received the value returned by the GetNumberOrEnter function, we examine it. If it is the value e_Return, the user has decided not to select any of the items. Therefore, we return the value 0 to indicate this situation.

However, assuming that the user hit something more than just the ENTER key, we have to check what the exact return value was. If it was the value e_Up or e_Down, and if we have more items than will fit in the scroll area, we change the value of the offset variable accordingly. Of course, we have to be careful not to try to display items whose indexes are before the beginning or beyond the end of the Name Vec; the code that handles both of the arrow keys ensures this doesn't happen.

Finally, if we get past the handling of the arrow keys without returning to the calling function, we must have gotten a numeric value from the user. Therefore, we check that the value the user typed is actually an entry in the Number Vec. Assuming that this is the case, we return that value to the calling function.

However, if the user entered an item number that is not found in the Number Vec, we create an error message and display it. Finally, we return the value 0 to indicate to the calling function that the user did not make a valid selection.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset