User Reports


A Console Stream Class For Borland C++

Comments By AI Williams


Al is the author of DOS 5: A Developer's Guide (M&T Press). He can be reached at 310 Ivy Glen Ct., League City, TX 77573; or on Compuserve at 72010, 3574.

For my first programming job under Borland C++, I wanted to write a program that used both stream I/O and text windows to handle I/O to and from the screen. Most C++ programmers prefer using stream I/O instead of the printf/scanf functions. To implement the text windows, I set out to develop a simple C++ window manager. I planned to use Borland's window() function, but soon found that the streams library did not support console I/O (analogous to the functions in CONIO.H).

Stream I/O

Some examples of stream output in Borland C++ are:

cout << "Hello World\n";
cout << "Your age is " << ur_age;
Both examples use cout, a predefined output stream corresponding to stdout. The first example sends the cliche string "Hello World\n" to cout. The second example prints a string and a numeric variable (ur_age).

The << operator, while usually the shift left operator, has been overloaded to return a stream or a stream reference. The operator used in this sense allows running multiple I/O statements together, as in the second example. The compiler interprets the statement as:

(cout << "Your age is ") << ur_age;
Since the first << operator returns a stream (cout, in fact), the statement is equivalent to:

cout << "Your age is ";
cout << ur_age;
Stream input is similar:

cin >> ur_age;
cin >> name >> phone_number;
C++ predefines the cin stream to be the same as stdin. Unlike scanf(), stream input doesn't require pointers to the input variables. In the previous examples, ur_age is just an unsigned integer variable. The equivalent scanf() calls are:

scanf ("%d" ,&ur_age);
scanf("%s %s",name,phone_number);
The C++ library defines input and output operators for the predefined types. It is simple to add custom << operators to print user-defined types.
Listing 1 shows a date class and an operator to print it on an output stream. Listing 1 also includes an overloaded >> operator to input a date. Of course, you can use the same techniques to add stream I/O for structures and unions, too.

The ostream class that Listing 1 uses is the base class of all output streams. The istream class is the base for input streams. These stream classes, which are subclasses of the ios class, supply methods for formatted and unformatted I/O.

Each stream has a buffer that sends and receives characters. For example, cout uses a buffer that sends characters to the stdout device. These buffers are either instances of the streambuf class, or instances of a subclass of streambuf. The buffer classes handle all the problems of managing a buffer and interfacing with the stream class. You are free to derive subclasses of the streambuf class that talk to different devices.

Since the new class will inherit methods from streambuf, you don't have to rewrite any of the buffer management methods that already exist. You must only describe how the new stream differs from its base class. This technique is known as "design by difference." Design by difference is part of what makes object-oriented programming so attractive.

The C++ library also supplies subclasses of streambuf. The filebuf class allows you to create buffers to access files. The strstreambuf class works with streams that operate on strings, similar to C's sscanf() and sprintf() functions.

Console Streams

VSTREAM.H and VSTREAM.CPP (Listing 2 and Listing 3) provide a console output stream called conout for your programs. These files create a class, Conbuf, that is a subclass of streambuf.

The only methods that must be defined for Conbuf are do_sputn()and overflow(). The methods that manage stream output don't change. The ostream class calls do_sputn() to output one or more characters. The overflow() method handles buffer overflow. Since the console stream does not require a buffer, the overflow() method simply prints the offending character using do_sputn().

VSTREAM.CPP creates a single instance of the Conbuf class (conbuf), and stores a pointer to it in the global variable conout. You can then use conout as cout was used in the earlier examples.

Deriving Conbuf is a good example of the design by difference technique. You don't have to worry about buffer management, formatting, and other idiosyncrasies of stream output. You only have to describe the difference between console output and the normal output streams.

The Window Manager

The window manager's design meets some simple goals:

The region class, shown in
Listing 4 and Listing 5, is the base class for windows. This class simply saves and restores portions of the screen using C++'s dynamic memory allocation.

The win class, built on top of the region class, provides all the basic window functions. The manager defines windows with the Borland library function window(). Therefore, all the output calls in CONIO.H and the new conout stream will automatically write to the top window.

The boxwin class is another example of design by difference. Containing just one function to draw a box, boxwin reuses all the code of its base class, win. Listing 6 and Listing 7 contain the window class library. Listing 8 is a demo program of the windows in action. Thanks to C++ constructors, you can simply define a window in your program, and it appears on the screen. When your program deallocates the window or it goes out of scope, the window vanishes just as easily.

Inside The Window Class

To understand the window class, you must first start with the class named region. The region class simply saves and restores a specified area of the screen. The constructor for this class takes four arguments that specify the x and y coordinates of the screen region to use. If the fifth argument, save, is zero, the constructor will set up the region, but not save the screen area. Otherwise, the region reads the contents of the screen (the default action). Setting save to zero is useful for programs like the window class that require finer control over the timing of region's actions.

The region class has three public methods besides the constructor and destructor methods. The reinit method causes the region to read the screen, discarding the previous contents of the region (if any). The restore method causes the region to redisplay the saved area of the screen. This call also discards the contents of the region. Finally, a call to discard will destroy the region's contents without changing the screen.

The window class win maintains a stack of windows. The class variable topwin points to the current top window. Notice that Listing 6 declares topwin as a static member of the class. This declaration allows all instances of win to share the same topwin variable. A similar variable, lastwin, points to the last window in the stack. Pointers thread the stack together in both directions. The pointers next (toward the bottom of the stack) and prev (toward the top of the stack) contain links to adjacent windows. Of course, these pointers are not static - each window has its own next and prev pointer.

The remaining data members of win are not static. They store the cursor location, the window's color, a pointer to the next window, and a margin value used for bordered windows.

The top window's region buffer remains empty. Only windows that are inactive store their contents in the region. This is also true of the cursor location values.

The win class has only one public method (excepting the constructor and destructor). This method, maketop, forces the given window to the top of the stack. If the window is already at the top, no action results.

More Fun With Windows

You will probably want to add functionality to the windows package. For instance, you might want different box types, or to be able to move and resize the windows. For serious use, you could add some simple methods to change various private members, such as a setcolor method to change a window's color.

The built-in stream classes can take manipulators that control certain formatting attributes. The standard headers IOSTREAM.H and IOMANIP.H declare these manipulators. The following statement, for example, uses the hex manipulator:

cout << hex << 16;
This statement prints 16 in hex (10) instead of decimal.

Adopting this syntax for managing the console stream would be quite natural. You could write

conout << color_red << "RED" << color_white << " ALERT";
or, alternately:

conout << concolor(0x70) << "Text";
Other manipulators could duplicate functions found in CONIO.H. For example, you could use a manipulator to set the cursor's location. Although this would be an ambitious addition, it would make the stream class much more natural to use.