Printing
Word-Wrapped Text
with Vic McClung's Printer/Preview Class
by Vic McClung
IN THIS ARTICLE, I will demonstrate how to print word-wrapped text in your report using my Printer/preview class.  Here, once again, is the basic code for printing using this class (with a string added for demonstrating word-wrapping).  Just cut and paste this code and you are ready to start printing.  Down in the middle of this code you will see two lines that start with asterisks (*) and are followed with a line of equal signs (=) refered to as the ‘code socket’ in the article below.  All of the examples I will use must be plugged into this location in the code to work properly.  This will prevent having to show the following code repeatedly.  Now skip on down below this code.
 
 
* ------------- copy and paste this code ---------------------------
* first you must load the printer.prg
* set search path to the directory containing the code
* for the Printer/preview class
set path to C:\DBPRINT
* also be sure that the necessary dll's are in your computer's
* search path (prev32.dll and nviewlib.dll)
set procedure to printer.prg additive // don't forget the 'additive'
* create a memory variable to hold the object reference
* local variable will work most of the time
local p // reference to the printer object
local cString, cRestOfString
cString = "Call me Ishmael. Some years ago -- never mind "    +;
          "how long precisely -- having little or no money "  +;
          "in my purse, and nothing particular to interest "  +;
          "me on shore, I thought I would sail about a "      +;
          "little and see the watery part of the world. It "  +;
          "is a way I have of driving off the spleen, and "   +;
          "regulating the circulation.  Whenever I find "     +;
          "myself growing grim about the mouth; whenever "    +;
          "it is a damp, drizzly November in my soul; "       +;
          "whenever I find myself involuntarily pausing "     +;
          "before coffin warehouses, and bringing up the "    +;
          "rear of every funeral I meet; and especially "     +;
          "whenever my hypos get such an upper hand of me, "  +;
          "that it requires a strong moral principle to "     +;
          "prevent me from deliberately stepping into the "   +;
          "street, and methodically knocking people's hats "  +;
          "off -- then, I account it high time to get to sea "+;
          "as soon as I can. This is my substitute for "      +;
          "pistol and ball.  With a philosophical flourish "  +;
          "Cato throws himself upon his sword; I quietly "    +;
          "take to the ship. There is nothing surprising "    +;
          "in this. If they but knew it, almost all men in "  +;
          "their degree, some time or other, cherish very "   +;
          "nearly the same feelings towards the ocean with "  +;
          "me."

* use the new operator to create the object from the class
* p = new printer(.t.)  // the parameter is for the printer selection dialog
p = new preview(true, 1) // use this line instead of above for preview
* if the user cancels, a -1 is returned
if p.hDC = -1
   msgbox('User Canceled Print Job', 'Cancel', 16)
   p.release()
   return
endif
* give it a title
* this is what will show up in the print manager
p.Title = 'Test of Printer Class'
* next we must create the printer device context
* this is sort of like a piece of paper in memory for printing on
* if it returns 0 there was an error ( I have never had this happen)
* or in the case of the latest 32-bit version 8.0, if you have no 
* printers installed, you will get a 0 returned here.
if p.CreateDC() == 0
   msgbox('Error Accessing Printer', 'Error!', 16)
   return
endif
* next we need to define the fonts we want to use
* this is not absolutely necessary, as there is a default font.
* look in the printer.prg at the definefont method for the complete
* documentation for defining fonts.  You should define all of your fonts
* before you start printing, but this is not absolutely necessary.
p.defineFont('Font 1','Times New Roman', -12, false, false, false)
p.defineFont(2, 'Times New Roman', -8, true, false, false)
* next you select one of the fonts to be default
p.SetFont('Font 1')
* the next line starts the print spooler
* only one of these per document, please!
p.StartDoc()
* now start the page
p.StartPage()
* printing commands go here (this is the 'code socket')
* ===================================================================
* ===================================================================
* now eject the page, you can have as many StartPage/EndPages as
* you have pages in your report.
p.EndPage()
* the following code should be inserted between pages or in long loops
* as it will break out of the printing if the user presses the 'cancel'
* button on the 'Printing' dialog box
if p.lAborted
   msgbox('Printing Canceled by User', 'Cancel', 16)
endif
* after all pages are printed, issue the EndDoc to end the print
* spooling and send it to the printer.
p.EndDoc()
* clean up to prevent memory leaks
release object p
p = null
* ------------- copy and paste this code ---------------------------

   

Wrapping Text

Since Windows uses mostly proportional fonts, figuring out where to break or wrap lines of text is somewhat more complicated than it was in DOS.  Of course in DOS you could just count the characters and backup until you reached a line break character (delimiter).  In Windows you have to measure the width of each character in order to calculate the width of the line of text you are about to print.  The Printer/preview class has a method that takes care of all of this math.

The class method for wrapping text is AtSayWrap().  It has a total of 6 parameters or arguments all of which are required except the last one (tag).  The parameters are :
 
 
nRow, nStartCol, nEndCol, nRowsToPrint, cString, tag
   

The nRow parameter tells the printer class where to place the bottom edge of the first line of text.  The nStartCol parameters specifies where the left edge of the first character of each line of text is to be printed.  The nEndCol is the right edge of the text, in other words, nEndCol - nStartCol will equal the width in inches ( or mm, cm) of the column of text.  The nRowsToPrint argument tells the number of rows of text to print and of course cString is the character string to print. cString may contain more text than you want to print and the previous parameters will control how much of it is printed.  The final and optional parameter is tag and with it you can specify a previously defined font other than currently ‘set’ font.  One other note on the arguments, if nRowsToPrint is set to zero (0), then AtSayWrap() prints until it runs out of text in the cString character string.

The AtSayWrap() methods returns a character string consisting of all of the original string except the characters that were printed.  In other words, anything that didn’t get printed is returned by the method.  I tend to use the variable name cRestOfString to designate the return string from the method.

There are several class properties or variables that are used exclusively by the text wrapping method (atSayWrap()).  These are:
 
 
// delimiters for AtSayWrap() line break
this.cDelimiters = ' ,;/.?!"$%&*(){}[]'+"'"
this.nLastLine = 0    // this is the last line AtSayWrap() printed on
this.nRowsPrinted = 0 // this is the number of rows printed by AtSayWrap
this.nTabWidth = .25
this.nTabChars = 8
this.EOL_CHAR = CRLF
this.lTextJustification = false
   

The first of these is cDelimiters, a string of characters that will be assumed to be separating words in a paragraph.  These may be changed if needed.  Second in the list is nLastLine, which specifies the position from the top of the page where the last line of text was printed by the AtSayWrap() method.  For each line that is printed the p.nLine is incremented by p.nLineInc.  That is to say, each line that prints adds the height of the current font to the vertical printing position.

Next comes nTabWidth, which is not used by the printer class but has been there from the beginning.  Actually, if a tab is encountered in the text, it is replaced by p.nAvgCharWidth * nTabChars of white space within the string.  The p.nAvgCharWidth specifies the average width of characters of the currently selected font.  For example if p.nAvgCharWidth is equal to .07 inches and nTabChars is equal to 8 (the default) , the tab character in text will be replaced by .56 inches of white space in the printout.  All of this is taken care of by the Printer/preview class and all the developer has to be concerned with is the value of the property p.nTabChars.  If 8 suits you then you have nothing to do.

The EOL_CHAR is the character or character string to look for as the end-of-line character(s).  In Windows programs this will most likely be the chr(13) + chr(10) string (carriage return plus line feed).  If you were printing a string that used only the line feed as the end-of-line marker you could set this equal to chr(10), i.e.: p.EOL_CHAR = chr(10).

Last but certainly not least is the lTextJustification property.  If this is set to true, the spacing of characters in the text is equal and the right hand side of the column is ragged because of the different lengths of the words.  If this property is set to true, then the text is ‘justified’, i.e., the right side of the column of text is straight.  Let’s look at a quick and simple demonstration of a column of word-wrapped text for starters:

Cut and paste the code at the beginning of this article into your editor and then insert the following code into the code socket as directed in the first paragraph of this article:
 
 
p.AtSay(.5, .5, 4.5, 0, cString)
   

Save this file as “WordWrp1.prg” (or whatever you want to name it) and run the program.  First preview it and then print it on your printer.  What you should see is the string from ‘Moby Dick’ printed on the top left side of a sheet of paper starting ½ inch from the left with the bottom edge of the first line of text starting ½" from the top and the width of the column should be 4 inches.  If you have a ruler handy, measure it.  If you don’t get ½ inch on both measurements, then you printer driver lied to the Printer/preview class when it asked the driver for the p.nTopOffset and p.nLeftOffset properties of the printer.  I have found that most printer drivers give approximately correct answers but are not good enough for doing precision work.  Maybe in a later article I will get into a method of making it print at exactly where you told it to print, but that is beyond the scope of this discussion.

Notice the right side of the text?  Jagged, right?  If you want the text justified or all lines of text exactly the same length, plug this code into the code socket and save and run the program:
 
 
p.lTextJustification = true
p.AtSay(.5, .5, 4.5, 0, cString)
p.lTextJustification = false
   

You may notice in the Preview window that the text is not perfectly aligned on the right and that it is almost perfect on the paper printout.  This can be attributed to the higher resolution of the printer as compared to the screen.  The printer is several hundred dots per inch and the screen is probably less than one hundred dots per inch.  Another factor is that Windows API calls do only integer arithmetic and not floating point so some precision is lost there.  Enough said about that.

Now, as we have seen, it is a fairly simple task to word-wrap that text string when we know that if we start at the top left side of the page everything will fit perfectly.   What if we didn’t know how long the string was either in characters or inches?  The AtSayWrap() method uses the p.nLineInc property to space the text vertically but does not change the p.nLine property.  You must do this yourself, if you desire, by setting the p.nLine equal to p.nLastLine.  Also after performing its task, the AtSayWrap() method sets the p.nRowsPrinted to the number of lines printed by the method.  This property is useful when setting the nRowsToPrint argument to zero (0).  You can use the p.RowsLeft() method to return the number rows of p.nLineInc height remaining on the page.  All of this sounds overly complicated, so let’s look at an example of printing two word-wrapped columns of the ‘Moby Dick’ string printed at the bottom of the page.  Let’s start at 8 inches down on the page and ½ inch from the left and print two columns 1.5 inches wide with a ¼ inch space between columns.  We will also choose the 8 point font by setting the tag parameter in the AtSayWrap method to 2.

Here’s the code to plug into the code socket:
 
 
* here we have to set p.nLine = 8 (same as our nRow value) to get
* p.RowsLeft() to work
p.nLine = 8
nRowsLeft = p.rowsLeft()
cRestOfString = p.AtSayWrap(8, .5, 2, nRowsLeft, cString, 2)
cRestOfString = p.AtSayWrap(8, 2.25, 3.75, nRowsLeft, cRestOfString, 2)
if not empty(cRestOfString)
   p.EndPage()
   p.StartPage()
   cRestOfString = p.AtSayWrap(.5, .5, 2, 0, cRestOfString, 2)
endif
   

Again, run the program and preview the results.  What you have is two columns printed at the bottom left of the first page and the remaining text printed in a single column along the left side of the second page.  Not very pretty, but that could be improved by making the columns a little wider.  Probably a column 2 to 2½ inch wide would look better.  You will have to do some experimentation to make that decision.  The bottom line is this: with the AtSayWrap() method of the Printer/preview class, you can have much greater control over the printout of memo text in your report.


Note: The author would like to thank Flip Young and David L. Stone, my proof-readers, for the improvements they brought to this text.