Quick ’n easy intro to
Vic McClung’s Printer/Preview Class
by David Stone.

The author teaches classes in both nutrition and computer basics at San Jose State University in California, and, in addition to writing the occasional real database app, has been writing nutrition-related software for many years using Clipper and now Visual dBASE. When it comes to report-writing in VdB, he willingly sacrifices drag-’n-drop and inflexibility for total control and flexibility — and a better preview.

VIC McCLUNG’S Printer/Preview class is a tour-de-force example of the use of WinAPI functions to improve the already considerable capabilities of VdB. Although this Printer/Preview class does not provide drag-and-drop ease of use — you actually have to code the reports by hand — please don’t be put off. Coding the reports is not that hard to do, especially when you develop some boilerplate code. The results are well worth it.

The advantages of using this class are what you might expect with any hand-coding approach — near-total flexibility of placement of data on the page, reports that are composed of virtually any combination of sections, related or not, and the ability to use your already-open queries/tables. Any table circumstances which occur in a form can be used as-is in a Vic report. There are extra goodies as well, the most spectacular of which is a zoomable preview which allows rapid bouncing from the first to the last page of any size report. There is also a wonderful function for printing an onscreen form, and the means to print graphic images. Finally, access is provided to most of the Windows shapes, pens, and brushes. Who knows, perhaps a customer will want you to print, in the middle of a report, a cross-section of an airfoil from an x:y coordinate file — very easy to do with the Polyline() function.

Vic has made this class work with both 16 and 32 bit VdB, so users of 5.x are not left out.

I believe that Vic’s Printer/Preview class goes far beyond most other custom classes provided by users, and can be properly considered a major addition to the VdB programming environment. Vic deserves much credit for producing such a classy product and then giving it to the dBase community at no cost.

Be sure to take advantage of Vic’s own instructions — he provides a help file (dbprint.hlp), several sample forms (test.prg, test1.prg, test2.prg, test3.prg), and there is much useful information to explore in his well-documented Printer.prg.

Download the Printer/Preview Class:Before trying out the Printer/Preview class, you’ll need to download it from dBASE Code Library. The current version uses InstallShield to install the class.

Set Up a Test Folder: After the install is complete, copy the following files to a test folder:
 
 
FILENAME.H
FONTS.H
GOTO.WFM
NVIEWLIB.DLL (or NVIEW16.DLL if using VdB5.x)
PAGES.WFM
PREVIEW.H
PREVIEW.MNU
PREVIEW.POP
PREVIEW.WFM
PREVIEW32.DLL (or PREVIEW16.DLL if using VdB5.x)
PRINT.H
PRINTDLG.WFM
PRINTER.H
PRINTER.PRG
STRUCTS.CC
VSTRUCT.H
WINAPI.PRG
   

Also download this zip file called bu08vic.zip and unzip it into your test folder. It contains VicIntro.wfm (screen shown below) and VicMore.wfm which include all the code listed in this article. The zip file also includes a sample table called CUSTOMER.DBF which is just a de-memo’d and de-MDX’d version of the Customer.dbf table that shipped with VdB5. The image Earth.bmp is also included to demonstrate the printing of images.

All of the code in VicIntro.wfm and VicMore.wfm employs XDML (instead of OODML) and VdB5 code conventions so users of VdB5 can use it as well. Under Properties | Desktop Properties, set the current directory in VdB to your test folder, and try out VicIntro.wfm as you read what follows.

The Speedtip on each button above tells which procedure is called by the OnClick() for the button. The More Goodies button opens VicMore.wfm.

What’s to follow

We’ll start with a structural overview, then a “Hello World” report, then add new code to introduce table data, multiple pages, and a few bells/whistles. Some possibly useful reference material is included in the Appendix.

Here’s a Table of Shortcuts:
 
   
  1. Overview of report structure
2. “Hello World” report
3. Add a table to a 1-page report
4. Expand the table data to a 2-page report
5. Define more fonts, some column position, and manage header/footer, etc.
6. More goodies 7. Appendix

Overview of report structure

Every Vic report has the following structure:

Notice the symmetry in the above structure — the second half mirrors the first half, except that the actions are opposite.

Code for the above is largely boilerplate except for the middle section — writing the table (or other) data, and non-generic header/footer functions.

Below is a “Hello World” example of a Vic report. To keep things simple, no table data is included. Several functions of the Printer class are used such as StartDoc(), DefineFont(), SetFont(), StartPage(), AtSay(), NextRow(), EndPage(),and EndDoc(). These are fairly self-explanatory, and are described as needed below. More info on positional functions is in the Appendix.

Hello World — as simple as it gets.
 
 
Procedure TestPrn1

* A logical var, lPreview, is used below. Normally you’d refer to
* the value of a form object such as a Radiobutton to decide whether to print or to preview.
* If you choose to preview, you can still print from the preview screen.
* Everything we do here will use the preview.

lPreview = .t.

set procedure to printer.prg additive && get the proc in memory

local p  &&  Set up a var to hold the new print object reference

* Now instantiate with the intention to either preview first, or directly print:
If lPreview = .t.
   p = new Printer(.f.,1)  && Setting this parameter ".f." means preview instead of
                           && printing (but you can print from the preview).
Else
   p = new Printer(.t.,1)  && ".t." means we are going to print with no preview.
Endif

* ------- A little set-up

if p.hDC = -1
   msgbox('User Cancelled Print Job', 'Cancel', 16)
   release object p
   return
endif

if p.CreateDC() == 0
   msgbox('Error Accessing Printer', 'Error', 16)
   release object p
   return
endif

* Let's define a font --- this is optional, since there is a default.
* In order, the parameters are

* 1. the # or other character tag by which you'll refer to the font,
* 2. the name of the font as it would be described in a font pull-down,
* 3. point size expressed as a minus number,
* 4. logical for choosing bold (or not),
* 5. logical for choosing underline (or not),
* 6. logical for choosing italic (or not)
p.DefineFont(1, "Arial", -11, .f., .f., .f.)
* The above font definition will thus establish a font called 1,
* and it will be an Arial typeface, 11 point, and neither bolded, underlined, nor italicized

* ----- end setup

* Now start the doc. You need this command only once per print job.
p.StartDoc()

* Set our pre-defined font as the default (we have only one to set at this point)
p.SetFont(1)

* Start the first page (same command for starting any new page)
p.StartPage()

* Optional — bump things down a couple of rows; looks nicer.
p.NextRow(2)

* Print the "Hello World" message at the current line, and 1" in from the left edge of paper
p.AtSay(p.nLine, 1, "Hello World of Easy Windows Printing! And, thanks a lot Vic!")
* About the p.nLine property...

* End the page.
p.EndPage()

* And end the doc. You also need only one of these commands per print job.
p.EndDoc()

* Release the Printer object and close the procedure
release object p
p = null
close procedure printer.prg
return

   

The Preview should look like what you see below (yes, this point size is illegible while zoomed out).

Move the mouse pointer over the image and it should change to a magnifying glass. By clicking on the image, you can zoom in and out.

While we’re here, let’s take a look at the toolbar:

The first three buttons provide some interesting and powerful options for managing the images viewed in the Preview. The first button provides a means of attaching the current image (as a metafile) to an e-mail message. The second button recalls to the preview screen a previously saved page image, and the third saves the current page image to a file. Since none of my apps uses these features, I remove these buttons by editing Preview.wfm.

The remaining buttons are more standard: they move between pages, zoom, print, and close the preview (without printing).

If you wish to print, press the print button to get the print dialog box shown below.

There are a couple of glitches concerning this dialog box — the “Number of Copies”option works if you have just come from the Preview, but not if you have printed directly, i.e., p = NEW printer(.t.). And, if your report has many pages and you wish to include a page range or a specific page that has a 0 (zero) in it, the error trapping routine will reject the entry, since it has been instructed to accept only digits 1-9.

The “Print to file” option can be useful, because if you set up a “Generic/text only” printer in Windows, the report will be sent to a readable ASCII file. The formatting of the text in that file will be distorted unless the original report was formatted using a mono-spaced font such as Courier, but this is nevertheless a handy way of getting ASCII data out of a report.

So much for the first simple report — pretty easy, right? Well, it’s also very easy to include table data. A very simple example of how this is done is shown below, still staying with just a 1-page report. Most of the code from the first example is still here, and newly added code is BOLD so you can easily see what’s been added.

Note: Keep in mind here that you can use tables/queries that you already have open in the form if that’s where the to-be-printed data is, or you may create new queries (or open tables) as needed. If you do use an existing query that is displayed in a grid, you may have to deal with some visual effects in the grid if you loop through the rowset. You may wish to use WINAPILockWindowUpdate to freeze the form during printing.

Add table data to a 1-page report

Here’s where you’ll use the Customer.dbf table that was included in the file you downloaded earlier, bu08vic.zip.

Note: When you run this report, you may notice that there is a Cancel button on the progress form. If you wish to cancel, and have time to press this Cancel button before the progress form disappears, the print job will be cancelled. For a very short report (such as the “Hello World” report), the progress screen will probably disappear too soon to allow you to press the Cancel button. You can code in a verification message for Cancellation, but it may be problematic.
 
 
Procedure TestPrn2

* A logical var, lPreview, is used below. Normally you’d refer to
* the value of a form object such as a Radiobutton to decide whether to print or to preview.
* If you choose to preview, you can still print from the preview screen.
* Everything we do here will use the preview.

lPreview = .t.

set procedure to printer.prg additive && get the proc in memory

local p  &&  Set up a var to hold the new print object reference

* Now instantiate with the intention to either preview first, or directly print:
If lPreview = .t.
   p = new Printer(.f.,1)  && Setting this parameter ".f." means preview instead of
                           && printing (but you can print from the preview).
Else
   p = new Printer(.t.,1)  && ".t." means we are going to print with no preview.
Endif

* ------- A little set-up

if p.hDC = -1
   msgbox('User Cancelled Print Job', 'Cancel', 16)
   release object p
   return
endif

if p.CreateDC() == 0
   msgbox('Error Accessing Printer', 'Error', 16)
   release object p
   return
endif

* Let's define a font — this is optional, since there is a default.
* In order, the parameters are

* 1. the # or other character tag by which you'll refer to the font,
* 2. the name of the font as it would be described in a font pull-down,
* 3. point size expressed as a minus number,
* 4. logical for choosing bold (or not),
* 5. logical for choosing underline (or not),
* 6. logical for choosing italic (or not)
p.DefineFont(1, "Arial", -11, .f., .f., .f.)
* The above font definition will thus establish a font called 1,
* and it will be an Arial typeface, 11 point, and neither bolded, underlined, nor italicized

* ----- end setup

* Get the table ready to use -- you can USE the table here or
* anywhere above in this routine; or you may use an already open
* table, start a new query, or use an existing query.
* Any table or query in the form will be seen by the print procedure within the form.

select select()
use customer

* Now start the doc. You need this command only once per print job.
p.StartDoc()

* Set our pre-defined font as the default (we have only one to set at this point)
p.SetFont(1)

* Start the first page (same command for starting any new page)
p.StartPage()

* Optional — bump things down a couple of rows; looks nicer.
p.NextRow(2)

* Print the "Hello World" message at the current line, and 1" in from the left edge of paper
p.AtSay(p.nLine, 1, "Hello World of Easy Windows Printing! And, thanks a lot Vic!")
* About the p.nLine property...

p.AtSay(p.nLine, 1, "Customers")
p.NextRow(3)

* Write the table contents
scan
   p.AtSay(p.nLine, 1, customer->name  && "1" means 1" from left edge
   p.AtSay(p.nLine, 2, customer->city  && "2" means 2" from left edge
   p.NextRow(1)  && single spaced records...fits them all on one page.

endscan
* Done....

select customer  && Close table
use

* End the page.
p.EndPage()

* And end the doc. You also need only one of these commands per print job.
p.EndDoc()

* Release the Printer object and close the procedure
release object p
p = null
close procedure printer.prg
return

   

The preview should look like this:

There is a header in the upper left corner, and the table data is beneath. Since this is still just a 1-page report, the buttons for moving from page to page are greyed out.

Expand table data to a 2-page report

All we need to do to get enough data to warrant additional pages is put a blank line or two between records… but then we also need to add some code to manage the situation when we run out of space at the end of a page. Simple to do… see BOLDed, Red-colored code below. The new code will test for remaining space via the RowsLeft() function. If there isn’t enough space for another record, it will end the current page, start a new one, and put the header in at the top. That’s all it takes.
 
 
Procedure TestPrn3

* A logical var, lPreview, is used below. Normally you'd refer to
* the value of a form object such as a Radiobutton to decide whether to print or to preview.
* If you choose to preview, you can still print from the preview screen.
* Everything we do here will use the preview.

lPreview = .t.

set procedure to printer.prg additive && get the proc in memory

local p  &&  Set up a var to hold the new print object reference

* Now instantiate with the intention to either preview first, or directly print:
If lPreview = .t.
   p = new Printer(.f.,1)  && Setting this parameter ".f." means preview instead of
                           && printing (but you can print from the preview).
Else
   p = new Printer(.t.,1)  && ".t." means we are going to print with no preview.
Endif

* ------- A little set-up

if p.hDC = -1
   msgbox('User Cancelled Print Job', 'Cancel', 16)
   release object p
   return
endif

if p.CreateDC() == 0
   msgbox('Error Accessing Printer', 'Error', 16)
   release object p
   return
endif

* Let's define a font — this is optional, since there is a default.
* In order, the parameters are

* 1. the # or other character tag by which you'll refer to the font,
* 2. the name of the font as it would be described in a font pull-down,
* 3. point size expressed as a minus number,
* 4. logical for choosing bold (or not),
* 5. logical for choosing underline (or not),
* 6. logical for choosing italic (or not)
p.DefineFont(1, "Arial", -11, .f., .f., .f.)
* The above font definition will thus establish a font called 1,
* and it will be an Arial typeface, 11 point, and neither bolded, underlined, nor italicized

* ----- end setup

* Get the table ready to use -- you can USE the table here or
* anywhere above in this routine; or use an already open
* table, start a new query, or use an existing query.
* Any table or query in the form will be seen by the print procedure within the form.

select select()
use customer

* Now start the doc. You need this command only once per print job.
p.StartDoc()

* Set our pre-defined font as the default (we have only one to set at this point)
p.SetFont(1)

* Start the first page (same command for starting any new page)
p.StartPage()

* Optional — bump things down a couple of rows; looks nicer.
p.NextRow(2)

* Print the "Hello World" message at the current line, and 1" in from the left edge of paper
p.AtSay(p.nLine, 1, "Hello World of Easy Windows Printing! And, thanks a lot Vic!")
* About the p.nLine property...

p.AtSay(p.nLine, 1, "Customers")
p.NextRow(3)

* Write the table contents
scan
* Manage additional pages---
* Note that we'll get to this code only if there are more records to print,
* i.e., not at eof() (or EndOfSet())
  If p.RowsLeft() < 2    && i.e., NOT ENOUGH space left for another record
                         && (based on current font height and page size)
     p.EndPage()         && so end the page...
     p.Startpage()       && and start a new page.
     p.NextRow(2)        && Do the same header stuff as before, if you like...
     p.AtSay(p.nLine, 1, "Customers")
     p.NextRow(3)
  Endif
  p.AtSay(p.nLine, 1, customer->name) && "1" means 1" from left edge
* (About the p.nLine property)
  p.AtSay(p.nLine, 2, customer->city) && "2" means 2" from left edge
  p.NextRow(2)     && More space between lines--this is how we "expand"
                   && the data to fill two pages...
endscan
* Done....

select customer   && and close table
use

* End the last page.
p.EndPage()

* And end the doc. You also need only one of these commands per print job.
p.EndDoc()

* Release the Printer object and close the procedure
release object p
p = null
close procedure printer.prg
return

   

Here’s the preview — it looks just like the previous one, except now the appropriate buttons are activated to move to page 2 (or to the end, which is also page 2) in the report.

Try out the buttons...

If you leave the preview image onscreen and use the Windows Explorer to examine your Windows temp folder, you will discover that there are some files there named ~wmf????.tmp, the wildcards representing random numbers. For this report, there should be two of them, representing the two page images. They are saved to disk until they are printed. This explains how, with a 150-page report, the Preview lets you zip back and forth between the first and last pages in a second or two (don’t try this with the VdB Report Class — it can take many minutes!). By the way, if you crash the Printer class before the ~wmf.tmp files have been erased, you’ll need to delete them yourself. It’s a good idea to check the temp folder once in a while. After you get your print routine working, this shouldn’t be necessary anymore.

Define more fonts, define some column positions, manage header/footer, and a few new functions

Define some fonts: Generally speaking, unless a report has no header, footer, or other ancillary info besides the data, it will need more than a single font. In this routine, we’ll create a function called DefFonts() (define fonts) which will define a series of fonts — specific typefaces, sizes, and appearances (normal/bold/underline/italics). We can then set the default font anywhere in the report to any one of these fonts whenever we like. The SayHeader() and SayFooter() functions below call several of these fonts in order to give the header/footer a distinctive look.

Define some column positions: It is also useful to define some column positions so that columnar data can be easily and consistently positioned, especially when you decide you need to change the position of a column or two… and you then merely change the column position definition instead of changing values in many AtSay() functions.

Write SayHeader() and SayFooter() functions: Although we employed a crude header in the two examples above, there is a better way to do it — write a header function, e.g., SayHeader(), to which we can pass the current Printer object reference (p) as a parameter so that the function has control over the printer object. So, when we wish the header to print on the current line, we just call the header function Form.SayHeader(p) or class::SayHeader(p). The SayHeader() function then writes its header info, then gives control back to the print routine.

We can treat footers the same way using a SayFooter() function.

A few new functions: The p.cTitle() function provides a means for telling Print Manager what “queue” name to give the document being printed. There is a tiny behavioral problem associated with this function: if you do a preview (rather than printing directly), the p.cTitle that you assign will be preceded by “Previewing - ” in the queue, as shown below. It won’t be if you print directly.

You can “fix” this behavior by locating the code for the EndDoc() function, on or about line 752 in Printer.prg, and changing
 
 
  f.Text = 'Previewing - '+this.cTitle
to
  f.Text = this.cTitle
   

An additional result of this change will be that the titlebar of the Preview window will not say “Previewing - etc.” either… it will contain only the queue name. This is probably OK, since it’s obviously a preview.

The SayHeader function below introduces p.cDate, which contains the current date, and p.cPage, which contains the current page number. The latter two Printer object properties can be used anywhere, but seem most appropriate in a header.

The p.Line() function is also used in SayHeader(). The p.line() function draws a line using the VertPos/HorizPos//VertPos/HorizPos (Y/X//Y/X) coordinate format and also specifies a thickness of line:
 
 
p.Line(10.57, 0.35, 10.57, p.nPageWidth, 7)
   

would draw a line beginning at a point 10.57 inches down the page and 0.35 inches in from the left edge, and ending at a point 10.57" down the page and the full width of the page. The line will be 7/100" thick. More on lines here.

The SayHeader() function below uses AtSayCenter() which centers the text around the specified horizontal position, and AtSayRight() which right-aligns text up to the specified horizontal position (second parameter):
 
 
AtSayCenter(2,4.25,"Center this string")
   

writes the text 2 inches down the page and centers it around a point 4.25" in from the left edge, which is the middle of an 8.5"-wide piece of paper.
 
 
AtSayRight(2,8.1,"Right-align this string")
   

writes the text 2 inches down the page and right aligns it to a point 8.1" in from the left edge.

Note on pressing the Cancel button: When Vic writes print routines, he usually inserts, after each p.EndPage(), the following:
 
 
if p.lAborted
   msgbox('Printing Canceled by User', 'Cancel', 16)
endif
   

This message merely verifies that the user has already pressed the Cancel button in an attempt to cancel printing. I have found that the use of this message in a print routine can have the unexpected consequence of the message being repeated on-screen many times over. The reason for this is not entirely clear to me, and I have avoided using this option and do not use it below.

New code below in BOLD.
 
 
Procedure TestPrn4

* A logical var, lPreview, is used below. Normally you'd refer to
* the value of a form object such as a Radiobutton to decide whether to print or to preview.
* If you choose to preview, you can still print from the preview screen.
* Everything we do here will use the preview.

lPreview = .t.

set procedure to printer.prg additive && get the proc in memory

local p  &&  Set up a var to hold the new print object reference

* Now instantiate with the intention to either preview first, or directly print:
If lPreview = .t.
   p = new Printer(.f.,1)  && Setting this parameter ".f." means preview instead of
                           && printing (but you can print from the preview).
Else
   p = new Printer(.t.,1)  && ".t." means we are going to print with no preview.
Endif

* ------- A little set-up

if p.hDC = -1
   msgbox('User Cancelled Print Job', 'Cancel', 16)
   release object p
   return
endif

if p.CreateDC() == 0
   msgbox('Error Accessing Printer', 'Error', 16)
   release object p
   return
endif

* Now use the DefFonts function to define several fonts at once — pass the Printer object
* reference so the function can control it...
Form.DefFonts(p)
* We now have 3 fonts to play with (see DefFonts() below for details of the 3 fonts)

* Now define some column positions, in inches---you could easily put this in
* function as well, but let's just do it here.
* These variables will be used as the 2nd parameter of the AtSay() function to specify
* horizontal positions on the page, e.g., p.AtSay(p.nLine, COL2, Customer->name)
* means "at the current line at the COL2 position [1.15" in this case] print
* the contents of the name field".
COL1 = 0.50 + p.nLeftOffSet
COL2 = 1.15 + p.nLeftOffSet
COL3 = 2.25 + p.nLeftOffSet

* Set the title of the doc for the queue in Print Manager
p.cTitle = "TestPrn4"

* ----- end setup
* Get the table ready to use -- you can USE the table here or
* anywhere above in this routine; or use an already open
* table, start a new query, or use an existing query.
* Any table or query in the form will be seen by the print procedure within the form.

select select()
use customer

* Now start the doc. You need this command only once per print job.
p.StartDoc()

* Set our pre-defined font as the default (we have only one to set at this point)
p.SetFont(1)

* Start the first page (same command for starting any new page)
p.StartPage()

* Optional---bump things down a couple of rows; looks nicer.
p.NextRow(2)

* Print the header by calling the SayHeader function and passing the
* printer object reference so SayHeader can control the printer object.
form.SayHeader(p)
p.NextRow(3)

* ---------- Write the table contents
scan

   * Manage additional pages---
   * Note that we'll get to this code only if there are more records to print,
   * i.e., not at eof() (or EndOfSet())
  If p.RowsLeft() < 5      && LEAVE ROOM NOW FOR FOOTER.
      form.SayFooter(p)    && Call the footer function
      p.EndPage()          && so end the page...
      p.Startpage()        && and start a new page.
      p.NextRow(2)         && Call the header function again, as above.
      form.SayHeader(p)
      p.NextRow(3)
   Endif
   p.AtSay(p.nLine, 1, customer->"First name") && "1" means 1" from left edge
* (About the p.nLine property...)
   p.AtSay(p.nLine, 2, customer->"Last name")  && "2" means 2" from left edge
   p.NextRow(2)&& THIS IS HOW WE EXPAND THE REPORT TO TWO (or more) PAGES...
endscan
* ----------- Done....

select customer  & Close table
use

* ------------ End the last page.

form.SayFooter(p) && Call the footer function
p.EndPage()

* ---------------And end the doc. You also need only one of these commands per print job.
p.EndDoc()

* Release the Printer object and close the procedure
release object p
p = null
close procedure printer.prg
return

Procedure DefFonts(PrnObject)
  * This function receives the object reference
  * to the current printer object.
  * We stick it back into a variable called p again,
  * just because it's familiar :-)
  local p
  p = PrnObject
  p.DefineFont(1, "ARIAL", -10, .f., .f., .f.)
  p.DefineFont(2, "ARIAL", -10, .t., .f., .f.)  && bold
  p.DefineFont(3, "ARIAL", -7, .f., .f., .t.)  && smaller, italics
  return

Procedure SayHeader(PrnObject)
  * This function receives the object reference of the current printer object
  * We stick it back into a variable called p again, just because it's familiar.
  local p
  p = PrnObject

  * Grab the current font name so we can restore it
  * at the end of the procedure.
  cFontTag = p.cFontTag

  p.SetFont(2) && bold font
  p.NextRow(2) && Skip down a couple of lines

  p.AtSay(p.nLine, COL11, "Date Printed:")  && this will be in bold

  p.SetFont(1) && switch back to un-bold font

  * The p.cDate parameter is the current date
  p.AtSay(p.nLine, COL1 + 1, p.cDate)

  * The AtSayRight() function right-aligns the text to
  * the horizontal position specified (2nd parameter).
  * The p.cPage parameter is the current page number
  p.AtSayRight(p.nLine, p.nPageWidth, p.cPage)

  * Now draw a line beneath the text
  p.Line(p.nLine +0.05, COL1, p.nLine + 0.05, p.nPageWidth, 7)

  * Make room under the header for the body of the report.
  p.NextRow(3)

  * Restore the original font
  p.SetFont(cFontTag)
  return

Procedure SayFooter(PrnObject)
local p
p = PrnObject
* Grab the current font name so we can restore it
* at the end of the procedure.
cFontTag = p.cFontTag
p.setfont(3)

* The line below will print a 7/100"-thick solid line near the base of the page
p.Line(10.57, COL1, 10.57, p.nPageWidth, 7)

* Then, just under the above printed line, all the following goes on 1 text line.
* You could include all this text in one AtSay(), but it's easier to get the placement
* you want by specifying the location of the beginning of each component.
p.AtSay(10.7, COL1,"My Cool Report Routine ")

* The AtSayCenter() function center-aligns the text around the specified horizontal position (2nd parameter)
p.AtSayCenter(10.7, 4.1,"Reports Up the Wazoo, Inc. ")

* The AtSayRight() function right-aligns the text to the specified horizontal position (2nd parameter)
p.AtSayRight(10.7, p.nPageWidth,"Copyright (C) 2000")

p.SetFont(cFontTag) && Restore the original font
return

   

The Testprn4 report is shown below. Notice the lines, header with page number, and footer.

Below is the header at a bit more legible size.

More goodies...

Vic’s Printer/Preview class provides a wide variety of additional tools that provide the means to produce almost any sort of printout that you can imagine. Below is a sampling.

Portrait/Landscape

It’s about as easy as can be to switch back and forth between portrait and landscape orientation. The default is portrait and does not need to be specified if portrait is what you want. Also, there’s no need to decide on one or the other for an entire document — you can switch mid-doc if you like, but you’ll need to do it in-between pages:
 
 
p.LandScape()
p.StartPage()
  * page contents...
p.EndPage()

p.Portrait()
p.StartPage()
  * page contents...
p.EndPage()

   

Support for pre-printed forms

If you wish to print to specialized pre-printed forms, you may wish to look at SetFormOffsets() in Vic’s help file. This function allows you to retrieve previously saved formatting specifications from the table called form_def.dbf. I have not used this function, but it appears that you would need to create a WFM with access to the form_def.dbf table in order to enter the values specific to a pre-printed form.

AtSayWrap() to print a memo field

This function provides a means of printing memo (or any other) text within a specified block on the screen. The function has the following 6 parameters (the last parameter, which is the font tag, can be left out — I will not describe it here or use it in code examples):
 
 
AtSayWrap(nRow, nStartCol, nEndCol, nRowsToPrint, cString, fontTag)
   

The first 4 parameters describe a rectangular area within which the text will be printed. The 5th parameter is the name of the variable that holds the text to print.

1. nRow: the distance from the top of the page to the top edge of the rectangle — you can use a specified distance in inches, or use p.nLine.

2. nStartCol: the distance from the left edge of the page to the left edge of the rectangle — specify in inches, or with preset Column position, or use p.LeftOffset to get as far to the left as printably possible.

3. nEndCol: the distance from the left edge of the page to the right edge of the rectangle — specify in inches or with preset Column position.

4. nRowsToPrint: the number of rows to print within the other specified boundaries — use an integer, or use the p.RowsLeft() function, which will ensure that you don’t over-shoot the bottom of the page. The number of lines available will depend on the value of nRow, the current font height, and the paper size.

5. cString: — a character variable to which has been stored the contents of the memo field or other data.

The function returns what’s left un-printed of cString. If the whole string got printed in the number of rows you specified, then the returned value will be empty. But if cString was too large to fit in the number of rows you specified, the return value will contain what’s left to print. This is useful since memo field contents can of course vary in size, and may unpredictably spill over the end of a page.

Vic has several nice examples of the use of this function in his dbprint.hlp help file that is included with the Printer class. Below is an example derived from his examples. The first page will print a simple example of text that predictably finishes printing within the designated area. The second page will print a larger memo that will spill to the next page, and so includes a loop that will guarantee enough pages to print all the text. You can tailor yours to suit your length-of-memo needs.
 
 
Procedure Wrapit
set procedure to printer.prg additive
local p
p = new Printer(.f.,1)

if p.hDC = -1
  msgbox('User Cancelled Print Job', 'Cancel', 16)
  release object p
  return
endif

if p.CreateDC() == 0
  msgbox('Error Accessing Printer', 'Error', 16)
  release object p
  return
endif

p.DefineFont(1,"Arial",-12,.f.,.f.,.f.)
p.StartDoc()

* First page — no spill-over.
p.StartPage()
p.SetFont(1)

p.nLine = p.nTopOffSet + p.nLineInc && Set pnLine as high as possible on page
p.AtSay(p.nLine, p.nLeftOffset,'Below, the text starts 2 lines down, 3" from left, is 3.5" wide, and uses less than the 20 line max. allotted..')
p.NextRow(2)
cAll = "Whether Sessions is on or off, you can see commands echoed to the Command window from actions performed on items in other windows. But when Sessions is on, commands typed in the Command window cannot affect items in windows that belong to other sessions. For example, when Sessions is on, you cannot open a table from the Navigator and then type commands in the Command window to work with that table. But if you open the table with the USE command in the Command window, you can continue to use the Command window to type other commands that will affect the table."
cString = cAll
p.AtSayWrap(p.nLine, 3, 6.5, 20, cString) && Not using the return value in this example
p.EndPage()

* Second page -- spill-over.
p.StartPage()
p.nLine = p.nTopOffSet + p.nLineInc && Set pnLine as high as possible on page
p.AtSay(p.nLine, 0.5, 'Below, the text starts 2 lines down, 3" from left, is 3.5" wide, and uses more than the rows left on the page.')
p.NextRow(2)
cString = cAll + cAll + cAll + cAll + cAll + cAll + cAll && Grow the text...
cRestOfString = "" &&Initialize empty
cRestOfString = p.AtSayWrap(p.nLine, 3, 6.5, p.RowsLeft(), cString) && # of rows is set to the max for this page

if .not. empty(cRestOfString) && Text remains, so not all of it got printed on this page and we're out of space
 do while .not. empty(cRestOfString)
   p.EndPage()
   p.StartPage()
   p.nLine = p.nTopOffSet + p.nLineInc
   p.AtSay(p.nLine, 0.5, "More memo text...")
   p.NextRow(2)
   cRestOfString = p.AtSayWrap(p.nLine, 3, 6.5, p.RowsLeft(), cRestOfString)
 enddo
endif

p.EndPage()
p.EndDoc()
p.release()
p = null
close procedure printer.prg

   

AtSayImage() — print a graphics file

Vic uses the NViewlib.dll (32 bit) or NView16.dll (16 bit) to print an image (JPG, JIF, GIF, BMP, DIB, RLE, TGA, PCX) within a specified area, and allows serious distortion/scaling if you so desire. Here is the function and the parameters:
 
 
AtSayImage(nRow, nCol, cBitMap, nWidth, nHeight, nTopStart, nLeftStart)
   

The first two parameters define the location of the upper left corner of the image, and the third tells the filename. That’s really all you need to get the image onscreen; but if you wish to play with the other parameters, you’ll find help in printer.prg and in dbprint.hlp.

You also have access to RESOURCE images with this function. For example,
 
 
p.AtSayImage(1,1,'RESOURCE #108') && should display the lighting bolt in RESOURCE.DLL
p.AtSayImage(1,1,'RESOURCE EXIT BITMAPS.DLL') && should display the exit bitmap in bitmaps.dll
   

The example below shows the Earth.bmp image. Using the nWidth and nHeight parameters can very quickly lead to very strange results, so use with caution.
 
 
Procedure AtSayImage
set procedure to printer.prg additive
local p
p = new Printer(.f.,1)
if p.hDC = -1
  msgbox('User Cancelled Print Job', 'Cancel', 16)
  release object p
  return
endif
if p.CreateDC() == 0
  msgbox('Error Accessing Printer', 'Error', 16)
  release object p
  return
endif

p.DefineFont(1,"Arial",-14,.f.,.f.,.f.)

p.StartDoc()
p.StartPage()
p.SetFont(1)

p.AtSay(0.5,0.5,"Here's Earth...")
p.AtSayImage(2, 3, "earth.bmp")

p.AtSay(6.5,0.5,"Here's Earth stretched a little...")
p.AtSayImage(7, 1.5, "earth.bmp", 200, 200)

p.EndPage()
p.EndDoc()
p.release()
p = null
close procedure printer.prg

   

AtSayWindow() — print a Form with class...

If you have ever needed to use the Form.Print() function in VdB and have actually used it, you will appreciate AtSayWindow(). No… you will LOVE AtSayWindow(). This function actually does what you probably imagined Form.Print() would do; namely, it gives you a perfect rendition of the specified on-screen form on the printer, just the way it looks on the screen. If you have a color printer, so much the better.

The routine below sets the image of the calling form at 2" down from the top and 2" in from the left of the page. Notice the parameters are the Y and X positions on the page in inches, and the object handle of the form.
 
 
Procedure PrintForm
set procedure to printer.prg additive
local p
p = new Printer(.f.,1)
if p.hDC = -1
  msgbox('User Cancelled Print Job', 'Cancel', 16)
  release object p
  return
endif
if p.CreateDC() == 0
  msgbox('Error Accessing Printer', 'Error', 16)
  release object p
  return
endif
p.NoCancel = .t. && Turns off the cancel button so it isn't captured
p.StartDoc()
p.StartPage()
p.DefineFont(1,"Arial",-12,.f.,.f.,.f.)
p.SetFont(1)
p.AtSayCenter(1,4.5,"This is what a printed form should look like!")
p.AtSayWindow(2,2,form.hwnd) && <---Here's the function
p.EndPage()
p.EndDoc()
p.release()
p = null
close procedure printer.prg
   

The only way in which this function is less useful than the built-in Form.Print() is that it doesn’t respect the object.Printable property of form objects (available in v7 only?), which includes/excludes the object from printing when Form.print() is called. So, to not print an object (like a Print button, for example) when using AtSayWindow(), you have to first set the object’s visible property to false.

Important note: When this function captures the image of the form to print, the form has to be open and visible on the screen; i.e., it is not enough to merely instantiate the form and leave it closed. Also, be fore-warned that any overlap by another form will be caught and included in the form’s image. So will speedtip banners. The mouse pointer is not caught. In order to avoid capturing the Print Cancel dialog box, Vic suggests turning off the Print Cancel Dialog before using this method by setting p.NoCancel=.t. You may not need to do this, however, depending on the location of the form on the screen.

Font rotation

This is really part of the DefineFont() function… the nDegrees parameter (below) specifies how many tenths of a degree to rotate the font. Since rotation is a font “appearance”, it has to be part of the font definition and can’t be done on-the-fly with already-defined fonts. You have to plan ahead.
 
 
DefineFont(Tag, cName, nPoints, lBold, lUnderLine, lItalic, nDegrees, nWidth)
   

See SetTextColor below (and Colors procedure in VicMore.wfm) for a demo.

SetTextColor(), and Font Rotation examples

Although not of much use unless you have a color printer, if you do, this and the next function are very nice.
 
 
p.SetTextColor(nRed, nGreen, nBlue) && where the colors nRed, nGreen, nBlue can be values from 0 to 255
   

Since text color is not part of a font definition, you can change text colors on-the-fly for your existing fonts.

Below is a procedure that demonstrates color changes of both text and text background (for which background mode must be changed to OPAQUE), and font rotation.
 
 
Procedure Colors
set procedure to printer.prg additive
local p
p = new Printer(.f.,1)
if p.hDC = -1
  msgbox('User Cancelled Print Job', 'Cancel', 16)
  release object p
  return
endif
if p.CreateDC() == 0
  msgbox('Error Accessing Printer', 'Error', 16)
  release object p
  return
endif

p.DefineFont(1,"Arial",-15,.t.,.f.,.f.)

* Define some additional fonts to demonstrate font rotation..
* the rotation parameter is in tenths of degrees (150 = 15 deg.)
p.DefineFont(2,"Arial",-15,.t.,.f.,.f.,-150)
p.DefineFont(3,"Arial",-15,.t.,.f.,.f.,150)
p.DefineFont(4,"Arial",-15,.t.,.f.,.f.,300)
p.DefineFont(5,"Arial",-15,.t.,.f.,.f.,450)
p.DefineFont(6,"Arial",-15,.t.,.f.,.f.,600)
p.DefineFont(7,"Arial",-15,.t.,.f.,.f.,900)

Col = 3 && Init a column value for use below
p.StartDoc()
p.StartPage()

p.SetFont(1) && basic Arial, not rotated
p.nLine = 2  && Initialize p.nLine 2" down from top

* Change text colors a few times
p.SetTextColor(255,128,255)
p.AtSay(p.nLine,Col,"Text")
p.NextRow()

p.SetTextColor(0,255,0)
p.AtSay(p.nLine,Col,"colors")
p.NextRow()

p.SetTextColor(128,128,255)
p.AtSay(p.nLine,Col,"are")
p.NextRow()

p.SetTextColor(255,128,0)
p.AtSay(p.nLine,Col,"nice")
p.NextRow()

p.SetTextColor(218,168,37)
p.AtSay(p.nLine,Col,"to change")
p.NextRow(3)

* You must change the background to opaque
* before background colors will appear.
* Also see SetBkMode below
p.SetBkMode(2)  && 2 = OPAQUE, 1 = TRANSPARENT

* Change background colors a few times
p.SetTextColor(255,255,255)
p.SetBkColor(255,128,255)
p.AtSay(p.nLine,Col,"Background")
p.NextRow()

p.SetBkColor(0,255,0)
p.AtSay(p.nLine,Col,"colors")
p.NextRow()

p.SetBkColor(128,128,255)
p.AtSay(p.nLine,Col,"are")
p.NextRow()

p.SetBkColor(255,128,0)
p.AtSay(p.nLine,Col,"nice")
p.NextRow()

p.SetBkColor(218,168,37)
p.AtSay(p.nLine,Col,"to change")
p.NextRow(3)

* Show rotated fonts by including the font tag in the call to AtSay()
p.AtSay(p.nLine,Col,"Rotated -15 degrees",2)
p.NextRow(2)
p.AtSay(p.nLine,Col,"Rotated 15 degrees",3)
p.NextRow(2)
p.AtSay(p.nLine,Col,"Rotated 30 degrees",4)
p.NextRow(2)
p.AtSay(p.nLine,Col,"Rotated 45 degrees",5)
p.NextRow(2)
p.AtSay(p.nLine,Col,"Rotated 60 degrees",6)
p.NextRow(2)
p.AtSay(p.nLine,Col,"Rotated 90 degrees",7)

p.EndPage()
p.EndDoc()
p.release()
p = null
close procedure printer.prg

   

SetBkColor()

The background color on which text is printed can also be controlled. The default is of course white (no color). Without a color printer, there may not be much reason to employ this, although white letters over a dark grey or black background, perhaps as a header or footer, can look pretty nice.
 
 
p.SetBkColor(nRed, nGreen, nBlue) && where the colors nRed, nGreen, nBlue can be values from 0 to 255
   

See SetTextColor above (Colors procedure in VicMore.wfm) for a demo.

Scaling an image

This one is pretty amazing — it lets you scale the size of the entire page up or down to any degree you like. You can easily hook it up to spinbox controls that let the user decide how much to scale. The ScalePg() procedure below fills most of a page with text on page 1, then on page 2 it sets the scale to 50% in both the X and Y axes, then re-writes the same text.

The function is p.SetScale(X-axis %,Y-axis %) where 100% for each is the default.

In experimenting with this function in the display of text, the Y setting (second parameter) seems to do most of the shrinking/enlarging — you get about the same result with p.SetScale(100,50) as you do with p.SetScale(50,50). Perhaps this is because of the nature of fonts — if the point size decreases (Y axis), the X-axis automatically decreases as well. With images, however, the X parameter stretches or shrinks the image horizontally (and separately from the Y-axis, if you wish) as you might expect. It may be risky to assume that a nicely formatted combination of text and graphic images can be safely shrunk to 1/2 size and retain the formatting, but it’s worth a try.
 
 
Procedure ScalePg
set procedure to printer.prg additive
local p
p = new Printer(.f.,1)
if p.hDC = -1
   msgbox('User Cancelled Print Job', 'Cancel', 16)
   release object p
   return
endif
if p.CreateDC() == 0
   msgbox('Error Accessing Printer', 'Error', 16)
   release object p
   return
endif

p.DefineFont(1,"Arial",-14,.f.,.f.,.f.)

* First page, with scaling defaulting to 100% in both X and Y axes.
p.StartDoc()
p.StartPage()
p.SetFont(1)
do while p.rowsLeft() >2
   p.AtSay(p.nLine,0.5,"Page 1 has normal scaling, page 2 has the page image scaled down to 50%.")
   p.NextRow()
enddo
p.EndPage()

* Second page, with scaling set to 50% in both X and Y axes.
p.SetScale(50,50)
p.StartPage()
do while p.rowsLeft() >2
   p.AtSay(p.nLine,0.5,"Page 1 has normal scaling, page 2 has the page image scaled down to 50%.")
   p.NextRow()
enddo
p.EndPage()

p.EndDoc()
p.release()
p = null
close procedure printer.prg
return

   

SetBkMode(): Use transparent background to allow superimposition of text on text or image

Most folks probably don’t routinely intend to write more than a single chunk of text to the same location on a page, but sometimes you may want to — I use it to plaster “Please Register!” in a very large (75pt or so) light-grey font in the background of reports in a shareware program. So, the Printer class allows you to specify whether the text background is transparent (the background of new text does not obliterate existing text on which it is superimposed), or opaque (the background of new text obliterates existing text on which it is superimposed).

Vic probably didn’t intend it this way, but the “default” setting for background transparency depends on whether you use the Preview (in which case it defaults to transparent) or not (in which case it defaults to opaque). Since you can control the background setting, be aware of what your needs are and use the code below after instantiating the printer object. Then, background behavior is consistent regardless of whether you use Preview or direct printing.

p.SetBkMode() can accept either a 1 or a 2 as its parameter. 1 means “Transparent”, and 2 means “Opaque”. Although you can just use the numbers as parameters, it’s probably best to establish two #defines to equate these two words with their respective values (although if you are using Printer.h, they are already defined for you). Then you can use the words —  it’s easier to keep things unconfused. This setting can be changed anywhere in the code after instantiation of the printer object. I do the #defines in the SetFont() function, or as in the code below, shortly after instantiation of the printer object (so you haven’t forgotten by the time you need them):
 
 
#define TRANSPARENT 1
#define OPAQUE 2
   

Then, to set the background to transparent (assuming Printer object called “p”):
 
 
p.SetBkMode(TRANSPARENT)
   

and to set the background to opaque:
 
 
p.SetBkMode(OPAQUE)
   

Regardless of background setting, characters themselves always over-write existing characters in the same space, so any text that is intended to be in the background must be sent first; text sent subsequently will appear “on top of” the text sent first.

Here’s an example (Opaque|Transparent Background button in VicMore.wfm):
 
 
Procedure BGs
set procedure to printer.prg additive
local p
p = new Printer(.f.,1)
if p.hDC = -1
  msgbox('User Cancelled Print Job', 'Cancel', 16)
  release object p
  return
endif
if p.CreateDC() == 0
  msgbox('Error Accessing Printer', 'Error', 16)
  release object p
  return
endif

#define TRANSPARENT 1
#define OPAQUE 2

p.DefineFont(1,"Arial",-14,.f.,.f.,.f.)
p.DefineFont(2,"Arial",-30,.t.,.f.,.f.)
p.DefineFont(3,"Arial",-75,.t.,.f.,.f.,-300) && Big up-rotated font

p.StartDoc()
p.StartPage()

p.SetBkColor(255,255,255) && Set background color to white
p.SetBkMode(TRANSPARENT)  && Set transparent

p.SetFont(3)
p.SetTextColor(174,174,174) && Set text color to light grey
p.AtSay(8,1,"In the background..")

p.SetTextColor(0,0,0)     && Back to black
p.SetFont(1)

* Reset p.nLine to top of printable part page
p.nLine = p.nTopOffSet + p.nLineInc

* Fill page with text......
do while p.rowsLeft() >1
  p.AtSay(p.nLine,0.5,"Filling up the page so we can see the effect of SetBkMode on text printed on text.")
  p.NextRow()
enddo

* Background is already set to transparent
p.SetFont(2)
p.AtSay(3,1,"Transparent background")

* Now set background to opaque
p.SetBkMode(OPAQUE)  && Opaque
p.AtSay(6,1,"Opaque background")

p.SetBkMode(1) && Set back to transparent again...

p.SetFont(3)   && Big rotated font
p.SetTextColor(174,174,174) && Make it light grey

p.AtSay(10.5,1,"In the foreground..")
p.SetTextColor(0,0,0)    && Back to black
p.SetBkMode(TRANSPARENT) && Set back to transparent

p.EndPage()
p.EndDoc()
p.release()
p = null
close procedure printer.prg

   

Shapes

Vic provides us with access to many of the WINAPI shapes — polygons, polylines, ellipses, rectangles, etc., as well as to the pens and brushes with which to render them. A pen is used for line and outline drawing; a brush is used to fill a shape. You can use the shape functions with default settings if you like, in which case there is no need to define a pen or a brush.

But since it’s fun to fool with other-than-default settings for these tools, let’s talk pens and brushes first.

Note: if you set procedure to printer.h additive, you will have all the #defines listed below.

Pens: Here’s the function to define a pen (analogous to defining a font):
 
 
p.DefinePen(Tag, nStyle, nColor, nWidth)
   

1. The Tag parameter is just like the tag in DefineFont() — it can be any descriptive name that pleases you, and you will use it later as the parameter for SetPen() to specify the pen to use. If the tag is character, e.g., “Pen #1”, use quotes. If you use an integer, e.g., 1, don’t use quotes.

2. The nStyle parameter requires either a number or a #define’d description from the list below:
 
 
* Pen Styles *
#define PS_SOLID 0
#define PS_DASH 1
#define PS_DOT 2
#define PS_DASHDOT 3
#define PS_DASHDOTDOT 4
#define PS_NULL 5
#define PS_INSIDEFRAME 6
   

3. The nColor parameter needs a color in this format: RGB(255, 0, 0) so you’ll need the values of the color you want.

4. The nWidth parameter is in inches, so avoid whole numbers unless you want very thick lines and outlines. You’d generally want 0.1 or less.

To set a particular pen as the default, use
 
 
p.SetPen(Tag)
   

whose parameter is the tag for the pen you want. Use quotes around the tag if the tag is not an integer, e.g., SetPen("MyFirstPen").

Brushes: Here’s the function to define a brush:
 
 
p.DefineBrush(Tag, nStyle, nColor, nHatch)
   

Analogous to defining a pen as described above, nStyle is the Brush Style, and nHatch is the Hatch Style. Below are #define’d choices: [I did preliminary tests of a few of these, but did not figure them all out — take a look in the WinAPI Help for more info]
 
 
* Brush Styles *
#define BS_SOLID 0   && Solid color
#define BS_NULL 1    && No fill
#define BS_HOLLOW BS_NULL   && Ditto...
#define BS_HATCHED 2 && Hatch-types are described below
#define BS_PATTERN 3
#define BS_INDEXED 4
#define BS_DIBPATTERN 5

* Hatch Styles && You must choose the BS_HATCHED brush style (see above) for these to work
#define HS_HORIZONTAL 0
#define HS_VERTICAL 1
#define HS_FDIAGONAL 2
#define HS_BDIAGONAL 3
#define HS_CROSS 4
#define HS_DIAGCROSS 5

   

Shapes: Let’s start with the simplest shape — a line. Vic has two functions, one called Line() and one called Line1(). He advises us to use the latter function, since the former is being phased out.

Below is the Line1() syntax:
 
 
Line1(nTop, nLeft, nBottom, nRight, penTag)
   

The first two parameters define the y/x (vert/horiz) position of the start of the the line, the next two parameters define the y/x position of the end of the line, and the last parameter lets you specify a penTag for drawing the line. No brush is needed, since lines don’t have anything to fill.

To work with shapes such as boxes and ellipses, you’ll need a pen to draw the outline and a brush to fill (if you wish to fill with something other than the default). If you wish to have the option to not fill a shape, you’ll need to define a brush with the BrushStyle set to 1 (null).
 
 
Ellipse(nTop, nLeft, nBottom, nRight, PenTag, BrushTag)
   

Using 2, 2, 4, and 4 inches, and 2, 5.5, 2.5, 7.5 inches for the positional parameters, here’s a circle and a flattened circle, respectively:


 
 
PolyLine(aPoints, pentag)
   

You’ll need a 2-dimensional array for the Polyline() function, each pair of values in the array representing the two coordinates (y/x, or vert/horiz) of a point. There is no brush (fill) for a Polyline — see Polygon() below if you need fill. The following example
 
 
aPoints = NEW ARRAY(1,2)

* First point
aPoints[1,1] = 7   && 7" down
aPoints[1,2] = 2   && 2" in from left edge

* Second point
aPoints.Grow(1)
aPoints[2,1] = 10  && 10" down
aPoints[2,2] = 5   && 5" in from left edge

* etc...
aPoints.Grow(1)
aPoints[3,1] = 8
aPoints[3,2] = 7

aPoints.Grow(1)
aPoints[4,1] = 7.5
aPoints[4,2] = 4

aPoints.Grow(1)
aPoints[5,1] = 6
aPoints[5,2] = 4

aPoints.Grow(1)
aPoints[6,1] = 7
aPoints[6,2] = 2

* Then do the polyline....
p.polyline(aPoints, "THINNER_PEN"

   

produces this polyline:

The points have been numbered in this GIF file… note that points 1 and 6 have the same value, so the shape is closed.
 
 
Polygon(aPoints, pentag, brushtag)
   

Analogous to Polyline above, but if the points do not return to the origin (in the example above, they do return to the origin), the system will create a line that connects the last and first points. The polygon will be filled with the current brush pattern.
 
 
Arc(nTop, nLeft, nBottom, nRight, nyStart, nxStart, nyEnd, nxEnd, PenTag)
   

The first four parameters are the upper-left/lower-right coordinates of a bounding rectangle which will enclose the arc. The next four parameters are the two coordinates that define the starting and ending points of the arc.

Here’s a description of the Arc() function from the Win SDK: “The arc drawn by using the Arc function is a segment of the ellipse defined by the specified bounding rectangle. The starting point of the arc is the point at which a ray drawn from the center of the bounding rectangle through the specified starting point intersects the ellipse. The end point of the arc is the point at which a ray drawn from the center of the bounding rectangle through the specified end point intersects the ellipse. The arc is drawn in a counterclockwise direction. Since an arc is not a closed figure, it is not filled.”

Here is code for an arc, and the resulting image:
 
 
p.Arc(6.7, 3, 7.7, 4, 3.5, 6.7, 7, 3, 2)
   


 
 
Chord(nTop, nLeft, nBottom, nRight, nyStart, nxStart, nyEnd, nxEnd, PenTag, BrushTag)
   

Chord() appears to be the same as Arc() except that the ends are joined by a line, and the resulting closed shape is filled.

Here is code for a chord, and the resulting image:
 
 
p.Chord(6.7, 5, 7.7, 6, 5.5, 6.7, 7, 5, 2, 1)
   

 


 
 
Pie(nTop, nLeft, nBottom, nRight, nyStart, nxStart, nyEnd, nxEnd, PenTag, BrushTag)
   

Here’s the means to write your own pie-chart function! The Pie function also uses a bounding rectangle, and (from the SDK) “The Pie function draws a pie-shaped wedge by drawing an elliptical arc whose center and two endpoints are joined by lines." and "The center of the arc is the center of the bounding rectangle.”

Here is code for a pie slice, and the resulting image:
 
 
p.Pie(6.7, 1, 7.7, 2, 1.5, 6.7, 7, 1, 2, 1)
   
 


 
 
Rectangle(nTop, nLeft, nBottom, nRight, PenTag, BrushTag)
   

Pretty self-explanatory — provide coordinates, pen, and brush and get a rectangle.
 
 
RoundRect(nTop, nLeft, nBottom, nRight, nEllipseWidth, nEllipseHeight, PenTag, BrushTag)
   

Same as Rectangle(), but with rounded corners defined by the width and height of an ellipse. This may not be so easy to imagine, so here is an example of varying ellipse width/heights:

The code that produced these three rounded rectangles is shown below (also in the Shapes() procedure, below):
 
 
p.RoundRect(2, 1, 3, 3, 0.25, 0.25, 'DEFAULT', 'DEFAULT')
p.RoundRect(3.2, 1, 4.2, 3, 0.25, 0.5, 'DEFAULT', 'DEFAULT')
p.RoundRect(4.4, 1, 5.4, 3, 0.5, 0.25, 'DEFAULT', 'DEFAULT')
   

Widths and heights are in italics. The first has symmetrical (identical) width/height values (0.25 each), while the other two are asymmetrical, having different width/height values (0.25 and 0.5, or the reverse).

Here’s the Shapes() procedure from VicMore.wfm:
 
 
Procedure Shapes
#include printer.h

set procedure to printer.prg additive  && get the proc in memory
local p && Set up a var to hold the new print object reference

p = new Printer(.f.,1) && Setting this parameter ".f." means preview instead
                       && of printing (but you can print from the preview).

* ------ A little set-up
if p.hDC = -1
  msgbox('User Cancelled Print Job', 'Cancel', 16)
  release object p
  return
endif

if p.CreateDC() == 0
  msgbox('Error Accessing Printer', 'Error', 16)
release object p
  return
endif

p.StartDoc()

*------------Page 1--Lines
p.StartPage()

p.line1(1,1,1,7)  && Uses defaults....thin black pen

* DefinePen(Tag, nStyle, nColor, nWidth)
p.DefinePen("THICKER_PEN",PS_SOLID,RGB(255,0,0),0.1)  && 1/10 inch and red...

p.SetPen("THICKER_PEN")
p.line1(2,1,2,7)

p.DefinePen("THINNER_PEN",PS_DASH,RGB(200,0,255),0.003)  && 3/10 inch and purple
p.SetPen("THINNER_PEN")
p.line1(3,1,3,7)

* Two crossed lines using the two pens.
p.SetPen("THICKER_PEN")
p.line1(4,1,8,7)

p.SetPen("THINNER_PEN")
p.line1(8,1,4,7)

p.EndPage()
*------------End Page 1

*------------Page 2--Rectangles and circles/elipses
p.StartPage()
p.DefineBrush('Fill1', BS_SOLID, RGB(0,250,0), HS_CROSS)
p.SetBrush("Fill1")
p.Pie(6.7, 1, 7.7, 2, 1.5, 6.7, 7, 1, 2, 1)

p.Rectangle(1, 6, 3, 8, "THICKER_PEN", "Fill1")

p.ellipse(1, 1, 5, 5, "THICKER_PEN", "Fill1")
p.ellipse(3, 3, 4, 4, 1, 1)
p.ellipse(1, 5, 2, 7, 2, 1)

p.RoundRect(5, 1, 6, 3, .25, .25, 'DEFAULT', 'DEFAULT')
p.RoundRect(5, 3.5, 6, 5.5, .25, .50, 1, 1)
p.RoundRect(5, 6, 6, 7.5, .25, .25, 2, 1)

p.Pie(6.7, 1, 7.7, 2, 1.5, 6.7, 7, 1, 2, 1)

p.Arc(6.7, 3, 7.7, 4, 3.5, 6.7, 7, 3, 2)

p.Chord(6.7, 5, 7.7, 6, 5.5, 6.7, 7, 5, 2, 1)

p.EndPage()
*------------End Page 2

*------------Page 3--Ellipses and a Polyline
p.StartPage()

p.ellipse(2, 2, 4, 4, "THINNER_PEN", "Fill1")
p.ellipse(2, 5.5, 2.5, 7.5, "THINNER_PEN", "Fill1")

* Set up array for the Polyline( ) function
aPoints = NEW ARRAY(1,2)
aPoints[1,1] = 7
aPoints[1,2] = 2

aPoints.Grow(1)
aPoints[2,1] = 10
aPoints[2,2] = 5

aPoints.Grow(1)
aPoints[3,1] = 8
aPoints[3,2] = 7

aPoints.Grow(1)
aPoints[4,1] = 7.5
aPoints[4,2] = 4

aPoints.Grow(1)
aPoints[5,1] = 6
aPoints[5,2] = 4

aPoints.Grow(1)
aPoints[6,1] = 7
aPoints[6,2] = 2

* Do the Polyline( )
p.polyline(aPoints, "THINNER_PEN")

release aPoints

* Arc and chord examples
p.arc(1,7,5,8,5,7,5,8, "THINNER_PEN")
p.chord(2,7,6,8,6,7,6,8, "THINNER_PEN","Fill1")

p.EndPage()

*------------End Page 3

*------------Page 4-- Compare Rounded rectangles
p.StartPage()

p.RoundRect(2, 1, 3, 3, .25, .25, 'DEFAULT', 'DEFAULT')
p.RoundRect(3.2, 1, 4.2, 3, .25, .5, 'DEFAULT', 'DEFAULT')
p.RoundRect(4.4, 1, 5.4, 3, .5, .25, 'DEFAULT', 'DEFAULT')

p.EndPage()

*------------End Page 4

p.EndDoc()

release object p
p = null
close procedure printer.prg
return

   

Final remarks

In this article I have covered most of the features in Vic McClung’s Printer/Preview class. As you can see, it is a powerful addition to the VdB repertoire, and can add substantial usefulness and market value to your programs. I hope you will consider trying this class in an application, not necessarily to wholly replace Crystal or the Report class in VdB7.x, but for those occasional reports or print jobs which cannot be easily produced, or produced at all, by the latter. I find that many of my printing needs are not met by the standard reporting tools, and thus I find Vic’s class to be indispensable.


Note: I would like to thank Flip Young, my proof-reader, for the improvements brought to this text.