September, 1995 - Vol. 2 No. 9
by Nat Ersoz
Microsoft Windows offers a set of Application
Programming Interface (API) functions for formatting and preparing
text for display to a window. Typically, a Windows programmer
will call functions such as TextOut() or ExtTextOut()
when writing text into a window's device contextbut
using these functions becomes tedious when you need to format
data and user-defined objects. For example, to display an int
value, a double value, and a user-defined myObject
object, you'd need to write code similar to the
following:
void TMyWindow::Paint( TDC& dc, bool erase, TRect& rect ) { char buf[80]; int n, x, y; // Other Paint code here
n = wsprintf( buf, "%d, %f", myInt, myDouble ); TextOut( dc, x, y, buf, n ); ostrstream os; os << ", " << myObject << ends; TextOut( dc, x, y, buf, strlen(os.str())); delete os.str(); }
Pardon me, but in my humble opinion: Yuck! In order to eliminate
the wsprintf() calls and ostrstream intermediate
usage, I wrote a class called odcstream. This class allows
you to rewrite the above code in the following manner:
void TMyWindow::Paint( TDC& dc, bool erase, TRect& rect ) { odcstream os( dc ); // Other Paint code here os << myInt << ", " << myDouble << ", " << myObject << endl; }
In other words, I output objects to a device context using the
rich formatting features provided by the iostream library. In
this article, we'll show how you can create and use the
odcstream class to provide stream-style output to a device
context.
The standard stream classes can be broken into two different class structures. The first set of classes defined in IOSTREAM.H derives from the class ios. This set includes istream, which supplies data, and ostream, which receives data. We'll derive the odcstream class from both the ostream class and the TDC class, which defines device context behavior, as shown in Figure A.
Figure A - The odcstream class inherits from the ostream and TDC classes.
The second set of classes defined in IOSTREAM.H derives from the
base class streambuf. The ios classes (istream
and ostream) usually contain a pointer to a streambuf
(or derivative) object, which buffers the data prior to formatting
it and outputting it to a device. In simplified terms, one can
say that ios-derived classes format the data, and streambuf-derived
classes hold the data.
Since streambuf objects act as temporary containers for data going to or from an ios object, we'll create a dcstreambuf class to buffer the data we want to write to a Windows device context. To do so, we'll construct a structure that manages the elements of the buffer shown in Figure B.
Figure B - Just like the streambuf class, the dcstreambuf class will manage the buffer using five pointers.
As you can see, members base_ and pbase_ both point to the starting address of the character buffer. Conversely, members ebuf_ and epptr_ both point to one element past the end of the character buffer. The member pptr_ points to the location where the stream will place new characters, and the stream increments this pointer each time the program adds a character to the buffer. Of these pointers, pptr_ is the only one that moves. The others remain fixed unless you reallocate the buffer.
We've declared each of these members as private to the streambuf class. To access each respective pointer, you'll use protected member functions that have the same name as each pointer, minus the trailing underscore. For example, to access the member pointer pbase_, the derived streambuf class can call pbase().
As you send data to an odcstream object, the class performs any necessary type conversion and then places the resulting characters into the dcstreambuf object at the location that pptr_ points to. The buffer will continue to fill up until it overflows (when pptr_ exceeds epptr_) or until you call the flush() member function to empty it. When one of these events occurs, the odcstream object sends the contents of the buffer to the output device, which for this class is a Windows device context.
By the way, because we're using the dcstreambuf
class only for buffering output to a device context, you'll
notice we're not using the gbase_, egptr_,
and gptr_ pointers. Stream classes that handle input
(those derived from istream) use these members to manage
the input buffer.
The sync() member function is the heart of the dcstreambuf operation. It performs the work of outputting the contents of the buffer to the device context.
If it weren't for the fact that it interprets special characters
held within the buffer, this is how the sync() member
function would look:
int dcstreambuf::sync() { if( !out_waiting() ) return TRUE;
unsigned int len = pptr() - pbase(); TextOut( hDC, x, y, pbase(), len ); setp( buf, buf+bufsiz ); return TRUE; }
This very simple dcstreambuf::sync() implementation would succeed in sending the formatted buffer of data to the device context. Note that two data members of dcstreambuf, x and y, provide the position information used by the TextOut() function. The x and y data members determine the horizontal and vertical placement, respectively, of the text.
There are some glaring deficiencies with this implementation of
the function dcstreambuf::sync(). One is that successive
calls to the sync() function would continuously print
the data over itself. To fix this problem, we need to update the
value of x every time the program sends data to the device
context. To accomplish this, we can insert the following line
of code after the TextOut() function call:
// increment x by the length of the string x += LOWORD( GetTextExtent( hDC, pbase(), len ));
Now, as the other member functions call dcstreambuf::sync(),
the horizontal position for text placement in the device context
will move to the right. This eliminates the overwriting of previous
text, at least in the horizontal direction. (By the way, in the
code fragment above, you'll notice that we show a call
to the GetTextExtent() API function. We make this call
to ensure that we calculate the length of the string correctly
when the window is using a proportional font.)
At this point, we've created a streamable interface that
replaces the TextOut() function and does a bit more.
It would be an even better interface if the class interpreted
special formatting characters such as \n, \t,
and \r in an intuitive manner. Let's look through
the portion of the buffer that pertains to the sync()
function and discuss the special characters we find there.
As we search the buffer, if we encounter a newline character (\n),
we'll send all contents of the buffer up to this character
to the device context. Then, we'll adjust the value of
y in order to position the subsequent characters on the
next line.
If we encounter a tab character (\t) as we search the buffer, we'll need to adjust the horizontal position to produce the effect of a tab operation. I found it useful to define two types of tab interpretation for my own work. The first type involves moving the text insertion point to the next available tab column, based on the last printable character's location. You'll find the next available tab column in the ansitab() member function, and I refer to it as 'ansi'-style tab indenting. This type of tab indenting reflects the way most text editors work.
The second type of tab indentingwhich I refer to as 'hard' tabbingmoves the text insertion point to a location that's defined by how many previous tabs have been used in the current horizontal line. It's implemented in the hardtab() member function. With this type of tab interpretation, it's possible for successive calls to TextOut to write on top of previous calls. For some applications, however, I've found it useful to place text in a specific column, regardless of the previous text length.
The location of tab columns is held in an array called tabs.
The size of the tabs array is held in the data member
ntabs, and the current tab level depth is held in the
integer itab.
Finally, the carriage return (\r) would be another useful
special character to interpret. In this case, it's conventional
to interpret a carriage return as setting the next insertion point
to occur at the beginning of the next horizontal line.
As you may have noticed, the dcstreambuf class performs most of the work involved in streaming data to the device context. Yet it's a class that's, for the most part, hidden from the programmer. All of its members and member functions are protected except for the constructor and destructor.
The odcstream class provides the interface to the Windows
programmer and contains a pointer to a dcstreambuf object.
This class formats the data (for example, the strings char
and int) and places it into the streambuf class.
This functionality is already present in the ostream
class and all classes that inherit from it. Therefore, the only
functions we need to add are those that relate to device-context
issues.
The odcstream class provides several position member functions for setting the text location in the device context. They are odcstream::x( int ), odcstream::y( int ), and odcstream::xy( int, int ). As you'd expect, a call to the odcstream::x( int ) function sets the horizontal position for the text within the device context. Similarly, a call to the odcstream::y( int ) function will set the vertical position in the device context. A call to the function odcstream::xy( int, int ) sets both the horizontal and vertical position.
What's even more interesting is how I've implemented
the iostream manipulators. Manipulators allow you to write code
using the following form:
ostream << hex << setfill( '0' ) << I >> endl;
In the above code fragment, the tokens hex, setfill,
and endl are manipulators. You can use manipulators to
modify the ostream (or, in my class, odcstream)
member via the left shift (<<) operator. Through
the use of the header file IOMANIP.H, you can create your own
manipulators. The odcstream class uses manipulators named
gotox(), gotoy(), and gotoxy().
To demonstrate how to implement manipulators, let's examine
the gotox manipulator. The DCSTREAM.H and IOMANIP.H header
files contain the following code related to implementing the gotox
manipulator:
// from the file iomanip.h: template<class typ> class omanip { private: ostream& (*_fn)(ostream&, typ); typ _ag; public: omanip(ostream& (*_f)(ostream&, typ), typ _z ) : _fn(_f), _ag(_z) { } friend ostream& operator << (ostream& _s, omanip<typ>& _f) { return(*_f._fn)(_s,_f._ag); } }; // from the file dcstream.h: inline ostream& odc_x_( ostream& os, int x ) { ((odcstream&)os).x( x ); return os; } inline omanip<int> gotox( int x ) { return omanip<int>( odc_x_, x ); }
All of this code is necessary for a simple manipulator. The Borland-supplied file IOMANIP.H contains the class template for omanip. The omanip class has a constructor that requires two arguments: a pointer to a function and an instantiated template-defined value. In the case of odcstream, the template-specific type is an int value, and the argument to the manipulator is gotox(int).
Fortunately, using a manipulator is much easier than writing one.
For example, you'll call a manipulator using the following
syntax:
odcstream os( hdc, dcbuf ); // typical constructor stuff os << gotox( 10 );
When you call gotox( 10 ), it returns a copy of omanip<int>.
At this point, the copy of omanip<int> is situated
on the right side of the first << operator. After
this substitution, the expression looks like this:
os << omanip<int>;
If you look inside the omanip<int> class, you'll
see a friend operator that knows how to handle sending omanip<typ>
out to the ostream class. Once more,
a substitution takes place. The operator calls the function to
which the first argument of the omanip stream pointed.
This, in fact, is the function odc_x_(),
which was the first argument to the omanip<int>
constructor. And, finally, odc_x_() calls the odcstream
member function x( int ), which then
sets the horizontal location for writing text onto the device
context.
To use the odcstream class in your own projects, you'll want to include a dcstreambuf object as a member of your TWindow-derived classes. If you examine the TMyWindow::Paint() member function in Listing C, you'll see that I create an odcstream object, passing in a handle to the device context (HDC) and a reference (or pointer) to the dcstreambuf object.
Listing C: STREAMER.CPP
#include#include #include #include "dcstream.h" const char *text = "\ This is the first line of text\n\ This is the second line of text\n\ Here is a tab\t<-- that's it right there\n\ \tThis line starts with a tab and ends in a return\r\ xxx\n\ This is the sixth line of text\n\ This is the seventh line of text\n\ This is the eighth line of text "; class TMyWindow: public TWindow { public: TMyWindow( void ): TWindow( NULL, "" ) { Attr.Style |= WS_HSCROLL | WS_VSCROLL; HDC hdc; TEXTMETRIC tm; hdc = GetDC( HWindow ); GetTextMetrics( hdc, &tm ); Scroller = new TScroller( this, tm.tmAveCharWidth, tm.tmHeight, 30 * tm.tmAveCharWidth, 10 * tm.tmHeight ); ReleaseDC( HWindow, hdc ); }; protected: dcstreambuf dcbuf; virtual void Paint( TDC& tdc, BOOL, TRect& ) { odcstream odc( tdc, dcbuf ); // Text BkGnd does not overwrite Rectangles odc.SetBkMode( TRANSPARENT ); // use ansi style tabbing odc.ansitab(); odc.tab(8); odc << text;} }; class TMyApp: public TApplication { public: TMyApp( void ): TApplication() {}; void InitMainWindow( void ) { SetMainWindow( new TFrameWindow( 0, "odcstream test", new TMyWindow() ) ); } }; int OwlMain( int, char*[] ) { return TMyApp().Run(); }
You can see how the odcstream class works by creating the three files that appear in Listings A, Listing B, and Listing C. (You'll need to create a new project that uses the OWL libraries, and add STREAMER.CPP and DCSTREAM.CPP to the project.) When you run the application, you'll see the streamed text appear in the window, as shown in Figure C.
Figure C - The streamed text appears directly in the window's device context.
At the beginning of this article, I noted that conventional Windows
programming requires the use of wsprintf (or sprintf)
calls, which translate data types into a character string. This
character string is then sent to the device context by calls to
TextOut. A similar operation actually occurs when you
use the odcstream/dcstreambuf method. The difference
is that the buffering and translation occur through a standardized
(and in my opinion, easy to use) streamable interface. I don't
think that there's any performance improvement or loss
associated with using odcstream. If you like streams,
you'll probably like this interface.
Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.