home *** CD-ROM | disk | FTP | other *** search
/ Programmer 7500 / MAX_PROGRAMMERS.iso / CLIPPER / DATABASE / PRN_LIB.ZIP / PRN_LIB.DOC < prev    next >
Encoding:
Text File  |  1988-08-11  |  25.5 KB  |  614 lines

  1.  
  2.  
  3. Tutorial on Printer Control                                     TWIN CITIES
  4. By Craig Yellick                                  dBASE COMPILER USER GROUP
  5. August, 1988
  6.  
  7.  
  8. In this tutorial I present my method for controlling the printer
  9. and the printed page.  I am a professional software developer
  10. and do all of my database programming in Clipper, along with the
  11. Tom Rettig library and a few other minor extensions.  My full
  12. library of printer control functions is much more complicated
  13. than I'd care to explain in a tutorial, besides the fact that
  14. I'd rather keep the really sexy stuff proprietary (what ya want
  15. fer free?), so I'm presenting the core functions to which you
  16. can add the fancy extensions.
  17.  
  18. I've been programming in dBASE-like languages since dBASE-II ran
  19. under CP/M on 8-inch disks.  My printer control techniques have
  20. been evolving for all these years and really took off when
  21. user-defined functions were pioneered by Nantucket's first
  22. release of Clipper.  I'm telling you all this because I want you
  23. to take a hard look at these methods.  Even if you already have
  24. a satisfactory method for messing with printers you may get some
  25. ideas for improving it.  I have dozens of wildly different
  26. systems up and running at customer sites all over the country
  27. and they all implement a common library of control functions.
  28. Believe me when I tell you these techniques have survived heavy
  29. abuse by ham-fisted end users who change their specs on an
  30. hourly basis.
  31.  
  32. I will present my techniques in source code that should run
  33. under any dBASE-syntax compiler that supports user-defined
  34. functions and at least one-dimensional arrays.  My style is
  35. admittedly biased towards Clipper.  There is a companion
  36. diskette containing the actual source code for the functions and
  37. some examples.
  38.  
  39. Design goals:
  40.  
  41.      1)  Effortlessly and transparently support lots of printer models 
  42.      2)  Make it easy to use printer features in your program
  43.      3)  Make the function library do the most of the busy-work
  44.      4)  Reduce the number of lines of code required
  45.      5)  Make the function calls self-documenting in context 
  46.      6)  Make that sucker bullet proof!
  47.  
  48. The functions are divided into three classes.
  49.  
  50.      Initialize      Setting everything up
  51.      Control Codes   Sending control codes to the printer
  52.      Page Headings   Handling page headings and margins
  53.  
  54.  
  55.                                                    Printer Control Tutorial
  56.                                                                      Page 2
  57. === INITIALIZE ===
  58.  
  59. Before printing anything we need to establish the environment
  60. for the printing functions to work in.  This is done once when
  61. your application is first started and does not need to be
  62. repeated.  (If memory is really tight and you prefer to get rid
  63. printer related items when they are not needed you may have to
  64. perform this initialization process before each printed report.)
  65.  
  66. This is a good time to introduce you to the memory variables I
  67. use.  All start with the prefix PRN_ so they can easily be saved
  68. and restored in MEM files, and can be quickly identified in
  69. debugger and source code dumps.
  70.  
  71. I support six print-style attributes.
  72.  
  73.      NORM   Normal, the printer's default style
  74.      COND   Condensed, typically 132 columns across page
  75.      BOLD   Boldfaced, usually a "double-strike" version of NORMAL
  76.      ITAL   Italic, not supported on all printers
  77.      LARG   Large or extended, usually "double-wide" version of NORMAL
  78.      UND    Underscore, exact effect depends on printer model
  79.  
  80. I use an "on" and "off" method for sending printer codes, so
  81. coupled with the PRN_ prefix the complete set of twelve control
  82. strings takes the following form.
  83.  
  84.      PRN_attrib 1   to turn the attribute on
  85.      PRN_attrib 0   to turn it off again
  86.      
  87.      Example:  PRN_COND1 turns condensed on,
  88.                PRN_COND0 turns it off.
  89.  
  90. Why use separate ON and OFF codes?  And what about bold-italic
  91. or condensed- underscore combinations?
  92.  
  93. If you think about it you'll answer one question with the other.
  94. This list of codes covers just about every printer model's range
  95. of capabilities.  Most printers can turn attributes on and off
  96. independently so you can have any combination you can stomach,
  97. like bold-italic-underscore.  The printer initialize and exit
  98. strings allow you to switch into different fonts and back out
  99. again without too much fuss, although since font changes aren't
  100. common to all printers (and not a very good idea for database
  101. programming, anyway) their support is limited.  The init/exit
  102. strings are intended for throwing printers in and out of draft
  103. and letter-quality modes for the duration of a report.  This
  104. allows you to put network printers back into their proper
  105. "default" state if you need to change them.
  106.  
  107.  
  108.                                                    Printer Control Tutorial
  109.                                                                      Page 3
  110. What about printers that don't support italics or some other
  111. attribute?
  112.  
  113. Simply use a reasonable alternate, like boldface.  You normally
  114. would use italics to make something stand out, so choose
  115. something appropriate.  If boldface isn't available then use
  116. "normal" for both.  You can leave any attribute string null
  117. (empty) with no problems.  Most matrix printers can get into
  118. condensed at a minimum.  Impact printers are stuck with which
  119. ever type element is installed and there's not much you can do
  120. about it.
  121.  
  122. What about printers that don't use ON/OFF-style control codes?
  123.  
  124. Just pretend that the OFF strings don't exist.  Use only the ON
  125. strings, but keep in mind that if your application switches type
  126. styles on the same line you may get strange results unless each
  127. ON string turns all the other attributes OFF.  For example, the
  128. NORMAL-ON string would have to turn all the other attributes
  129. OFF, since NORMAL-ON wouldn't necessarily know which others were
  130. in use.  This results in longer control strings but accommodates
  131. almost all printers on the market and drives them at their
  132. highest level of performance.
  133.  
  134. I use three more strings for sending other controls-
  135.  
  136.      INIT      Initialization string, sent once before report
  137.      EXIT      Exit string, sent once when report is done
  138.      EJECT     Page eject string
  139.      
  140.      These are also prefixed with PRN_
  141.  
  142. Why use an eject string when the EJECT command does the same thing?
  143.  
  144. Some printers need more than the simple ASCII chr(12) or "form feed"
  145. character to properly perform a page eject.  I always stick a carriage
  146. return after the chr(12) so the printer buffer flushes and the eject is
  147. performed right away, some printers will leave the last page in the platen
  148. until the next report starts printing.  Applications using a cut-sheet
  149. feeder sometimes need an additional command to load another sheet.  A
  150. variable eject string also allows you to set up a "debugging" printer
  151. model which prints "---PAGE EJECT---" instead of a real page eject, for
  152. testing a report without wasting paper.
  153.  
  154. The INIT and EXIT strings are also useful for inserting additional page
  155. ejects before and/or after a report.  In some situations an extra eject at
  156. the end of a report makes it much easier to detach the paper, and adds
  157. more physical space between batches of reports.  The extra eject at the
  158. start of a report may be wasteful, but sometimes required if another
  159. application has the bad habit of leaving the printer half way through a
  160. page.  You would not believe the number of so called "professional"
  161. software packages that just stop printing when a report is complete with
  162. no final eject.  Aaaaargh.  And to top it off, YOUR application gets the
  163. blame for printing on someone else's reports!
  164.  
  165.  
  166.                                                    Printer Control Tutorial
  167.                                                                      Page 4
  168. A final, non-printing string is named PRN_MODEL and is used
  169. solely for identifying which set of codes is loaded.
  170.  
  171. We're not done yet.  Next come the page format controls, all prefixed with
  172. PRN_.  All values are assumed to be measured in "normal" lines and column
  173. positions.
  174.  
  175.      LMAR      Left margin, default is zero
  176.      RMAR      Right margin, default is 80
  177.      XMAR      Extended right margin, default is 132
  178.      TMAR      Top margin, default is 3
  179.      BMAR      Bottom margin, default is 5
  180.      PLEN      Page length, default is 66 lines
  181.  
  182. LMAR, TMAR, BMAR and PLEN are used by the function library for inserting a
  183. left margin and calculating when it's time to eject a page and print a
  184. heading.  The RMAR and XMAR are for your application's use when attempting
  185. to right-justify output according to the user-defined margins.  RMAR is
  186. for "normal" 80 column text and XMAR is for an extended right margin for
  187. condensed, 132 column printing.  You may also use RMAR and XMAR for
  188. trimming long lines or splitting wide reports across two sheets of paper
  189. (like spreadsheets), which is easier to do than you might think.
  190.  
  191. Three other variables are used by the printer control functions to
  192. communicate with each other.
  193.  
  194.      LINE      Current page line
  195.      PAGE      Current page number
  196.      FIRST     Flag for "is this the first page?"
  197.  
  198. There are two methods for getting the printer control variables into
  199. memory.  You can save a set to a MEM file (they all start with PRN_ so
  200. they can be saved easily), or you can read them from a database file full
  201. of different model-specific codes.  In either case you'll need to get them
  202. into memory variables where they can be manipulated without messing with a
  203. DBF file.  I prefer to use a MEM file unless the printer model needs to be
  204. changed constantly.
  205.  
  206. To ensure that no matter what configuration stuff is missing out on the
  207. hard disk the application can still print something, I use a double
  208. install procedure which starts with reasonable defaults and then looks for
  209. more specific values in a PRINTER.MEM file.  If you prefer a DBF full of
  210. settings, you could modify the process fairly easily to assign the memvar
  211. with DBF field values instead of restoring from a MEM file.  We'll keep it
  212. simple and use a MEM file in this tutorial library.  But, here's an
  213. example for using a DBF file for those who prefer.
  214.  
  215.      ***  Example in pseudo-code
  216.      < init all variables with defaults >
  217.      Is the DBF available?
  218.           Yes- open it and locate the appropriate model
  219.           No-  return, we'll have to use the defaults
  220.      For each field in the DBF do
  221.           prn_xxx= DBF->xxx
  222.      enddo
  223.      return
  224.  
  225.  
  226.                                                    Printer Control Tutorial
  227.                                                                      Page 5
  228. I place the following code in a function called PRN_INIT().
  229. The function accepts an optional model-ID to load.  The function 
  230. returns a logical value:  TRUE if a printer controls were loaded
  231. and FALSE if the defaults are being used.
  232.  
  233.      function PRN_INIT in pseudo-code
  234.      parameters  model
  235.      
  236.      ***  Establish all as public and start with defaults
  237.      *
  238.      public <all of the PRN_ variables>
  239.      store [] to <all of the printer control strings>
  240.      prn_model= "DEFAULTS"
  241.      prn_eject= chr(12) +chr(13)  && Form-feed plus CR
  242.      prn_cond1= chr(15)           && ^O  condensed on most printers
  243.      prn_cond0= chr(18)           && ^R  normal on most printers
  244.      prn_xxxxx= nnn  <establish margin defaults>
  245.      
  246.      ***  Attempt to load a custom set
  247.      *
  248.      If a PRINTER.MEM file exists, RESTORE ADDITIVE.
  249.      Or, if the MODEL parameter was specified, try to
  250.      restore it, instead.
  251.      
  252.      Return TRUE if a custom set was successfully loaded, or
  253.      return FALSE if the defaults are being used.
  254.  
  255. This function needs to be called only once, unless you are
  256. loading different sets of codes or really need the memory space
  257. in a bad way.  To get rid of everything related to printing-
  258.  
  259.      RELEASE ALL LIKE PRN_*
  260.  
  261. If the defaults end up being used you will be able to switch
  262. between NORMAL and CONDENSED on most printer models.  The other
  263. attributes will have no effect.
  264.  
  265.  
  266. === SENDING CONTROL CODES ===
  267.  
  268. There are several functions for putting all the printer controls
  269. to productive use.
  270.  
  271.      Attrib      Send text with ATTRIB, limited to duration of text
  272.      Attrib_On   Send ATTRIB-on string
  273.      Attrib_Off  Send ATTRIB-off string
  274.      
  275.      (Where Attrib = NORM, COND, BOLD, LARGE, ITALIC, UNDSCR)
  276.      
  277.      PrnEject    Send the page-eject string
  278.      PrintOn     Get ready to start printing, send INIT string
  279.      PrintOff    Done printing, send EXIT string
  280.  
  281.  
  282.                                                    Printer Control Tutorial
  283.                                                                      Page 6
  284. Other functions do the actual printing of the report contents.
  285.  
  286.      NextLine  Start printing indented to left margin on next line
  287.      SameLine  Print at current row and column position
  288.  
  289. Let's look at the ATTRIB(), ATTRIB_ON() and ATTRIB_OFF()
  290. functions in detail.  The ON and OFF functions take no
  291. parameters, they merely send the appropriate attribute string.
  292.  
  293.      ***  Example of ON and OFF functions
  294.      *
  295.      PrintOn()
  296.                * device is set to printer and PRN_INIT string is sent
  297.      Norm_On()
  298.                * printed lines will be in "normal" style
  299.      Italic_On()
  300.                * lines are now in italic
  301.      Cond_On()
  302.                * lines are now in condensed-italic
  303.      Italic_Off()
  304.                * lines are now in condensed, only
  305.      PrnEject()
  306.                * take a wild guess
  307.      PrintOff()
  308.                * EXIT string is sent and device is set back to screen
  309.  
  310. The purpose of the ON and OFF functions are to turn an attribute
  311. on and leave it on until you shut it off.  The ATTRIB()
  312. functions are just as easy to use.  They effect only the string
  313. sent as an argument, an ON and OFF is attached to the beginning
  314. and end of the string.  They do not effect other current
  315. ATTRIB_ON() commands.
  316.  
  317.      ***  Example of ATTRIB functions
  318.      *
  319.      PrintOn()
  320.      @ prow() +1, 0 say Bold("This is in boldface")
  321.      @ prow() +2, 0 say Large("And this is in large type")
  322.      @ prow(), pcol() +2 say Italic(UndScr("Both italic & underscored"))
  323.      PrnEject()
  324.      PrintOff()
  325.  
  326. I'll bet you are tired of typing  @ PROW(), PCOL() SAY  all the
  327. time.  And if you are using some kind of variable left margin,
  328. you type even more.
  329.  
  330.      @ prow(), pcol() +left_margin say "something"
  331.  
  332.  
  333.                                                    Printer Control Tutorial
  334.                                                                      Page 7
  335. This is where the NEXTLINE() and SAMELINE() functions really
  336. make life easier.  NEXTLINE() moves down a specified number of
  337. rows, inserts a number of "normal" spaces as a left margin, and
  338. prints whatever data was sent as a parameter.
  339.  
  340.      function NextLine
  341.      parameters  how_many, what
  342.      
  343.      do case                &&  Both parameters are optional
  344.      case pcount() = 0      &&  So take reasonable defaults
  345.           how_many= 1
  346.           what= []
  347.      case pcount() = 1
  348.           what= []
  349.      endcase
  350.      
  351.      @ prow() +how_many, 0 say Norm(space(prn_lmar))  && Left margin
  352.      @ prow(), pcol() say what
  353.      
  354.      prn_line= prn_line +how_many   && Update the line counter
  355.      
  356.      return prow()  && Return the internal row counter
  357.  
  358. SAMELINE() stays on the current row and column and prints the
  359. data with an optional picture template.  Please note that in
  360. both functions the "what" parameter is type- less, any data type
  361. (char, num, date etc) can be used.
  362.  
  363.      function SameLine
  364.      parameters cols_over, what, with_pict
  365.      
  366.      if pcount() < 2
  367.           @ prow(), pcol() +cols_over say []  && Just move the print head
  368.      else
  369.           if pcount() < 3
  370.                @ prow(), pcol() +cols_over say what
  371.           else
  372.                @ prow(), pcol() +cols_over say what pict "&with_pict."
  373.           endif
  374.      endif
  375.      
  376.      return pcol()  && Return the internal column counter
  377.  
  378. Here is some sample source code.
  379.  
  380.      PrintOn()
  381.      NextLine("Here is the first line")
  382.      NextLine(2, "Two lines further on...")
  383.      SameLine(3, "...and three cols over")
  384.      NextLine(2, Italic("ABCDEFG"))
  385.      SameLine(0, UndScr(Italic("HIJKLM")))
  386.      Cond_On()
  387.      SameLine(3, "OPQRST")
  388.      Italic_On()
  389.      SameLine(3, "TUVWXYZ")
  390.      NextLine(2, Bold("Formatting Example:"))
  391.      SameLine(2, 123456789, "999,999,999.99")
  392.      PrnEject()
  393.      PrintOff()
  394.  
  395.  
  396.                                                    Printer Control Tutorial
  397.                                                                      Page 8
  398. Here are some things to keep in mind when using NEXTLINE(),
  399. SAMELINE() and the various attribute functions.
  400.  
  401. o  NEXTLINE() inserts a "normal" left margin.   Be aware when
  402. counting columns that you don't think of the left margin as
  403. being "condensed" along with the body of the report.
  404.  
  405. o  If you want to print formatted data in the first column,
  406. don't use NEXTLINE() to print the data.  Instead use NEXTLINE()
  407. to move down a line and insert the margin, followed by a
  408. SAMELINE() that does the actual data printing.  e.g.
  409.  
  410.      NextLine()
  411.      SameLine(0, DBF->Number, "999,999.99")
  412.  
  413. o  In the same manner, if you want to print the entire line in a
  414. particular style- don't wrap every SAMELINE() with an ATTRIB().
  415. Issue an ATTRIB_ON() followed by as many SAMELINE() as needed. e.g.
  416.  
  417.      *** Good                      *** Not Good
  418.      NextLine()                    NextLine(1, Cond("Column 1"))
  419.      Cond_On()                     SameLine(1, Cond("Column 2"))
  420.      SameLine(0, "Column One")     * etc... for 20 columns
  421.      SameLine(1, "Column Two")
  422.      * etc... for 20 columns
  423.  
  424.  
  425. === PAGE HEADINGS ===
  426.  
  427. The final component of this tutorial is a page heading function.
  428. Everyone has a method they will defend to their death.  I am
  429. quite happy with mine because it is simple.  It does the job for
  430. a wide variety of applications without complicated "custom"
  431. hacks.
  432.  
  433. My page header technique is to call the function whenever the
  434. report considers it acceptable.  The function only issues a
  435. header when the conditions are right.  This usually means
  436. calling the function on each pass through the main report loop,
  437. before each line is printed.  When printing subtotals or other
  438. "chunks" of lines you DON'T call the header function if you
  439. DON'T want the possibility of the chunk being broken by a new
  440. page.  Very simple to implement, and the code returns quickly so
  441. there is no penalty for calling it so often.
  442.  
  443.  
  444.                                                    Printer Control Tutorial
  445.                                                                      Page 9
  446. One thing I do that you may not have seen before-  the page
  447. heading function turns the printer on, NOT the report procedure.
  448. This prevents a header with no data beneath it from being printed in the
  449. case where no data qualifies for the report.  The printer isn't even
  450. turned on until there is something to print.  I have other functions that
  451. check the printer for "on-line" etc long before the main report body
  452. starts running.  For example, suppose you have an "Overdue Accounts"
  453. report, and it would take a lot of run-time to determine if there really
  454. ARE any accounts overdue.  You could print the entire report in the time
  455. it would take to figure out that yes, there are some.  With my method you
  456. can just jump right in, and if no data is found nothing gets printed.  You
  457. can display a message saying that nothing was found rather than printing
  458. an empty report.  Certainly there are situations where you WANT a header
  459. printed even though there is no data, perhaps with a line saying "No
  460. accounts are overdue".  No problem, just call the header routine and print
  461. the line.  The point is- you have an option.  I've found that my customers
  462. appreciate the printer sitting quietly unless there is something useful
  463. coming out of it, especially if the printer is on a network with a busy
  464. spooler and is housed 200 yards down the hall!
  465.  
  466. Here is the outline for such page heading function.
  467.  
  468.      function PageHead in pseudo-code
  469.      
  470.      if the current line plus the required bottom margin
  471.      is greater than the page length it is time to print a
  472.      page heading.  Otherwise return without doing anything.
  473.      
  474.      Increment the page counter.
  475.      Reset the line counter to line zero.
  476.      
  477.      If this is the first page printed so far, don't need to eject
  478.      but we DO need to turn the printer on and send the INIT string.
  479.      
  480.      If this isn't the first page, eject the current page.
  481.      
  482.      Now we're at the top, so move done beyond the top margin.
  483.      
  484.      For as many heading lines as are currently defined...
  485.           Print a heading line
  486.      ... Done.
  487.      New current line is equal to the top margin
  488.      plus the number of header lines.
  489.      
  490.      Return: YES or NO- did we print a header?
  491.  
  492.  
  493.                                                    Printer Control Tutorial
  494.                                                                     Page 10
  495. Another function, PGHD_INIT(), is used to initialize the three
  496. header function variables to their proper starting values.  It is a very
  497. simple function used purely for convenience.
  498.  
  499.      function PgHd_Init
  500.      prn_first= .t.     && Yup, first page so far
  501.      prn_page= 0        && PageHead increments counter so start at zero
  502.      prn_line= 99999    && Force a "need an eject" condition
  503.      return []
  504.  
  505. You should call this function once before starting the main reporting
  506. loop.  You can use the status of PRN_FIRST to tell if anything was printed-
  507. it will be FALSE if data was printed, TRUE if not.  If you want to start
  508. numbering at different page you can assign the PRN_PAGE variable after the
  509. call to PGHD_INIT().  There is one more requirement for setting up a
  510. header, which we will see in the following sample code.  Here is an
  511. example of a full-blown report.
  512.  
  513.      Procedure A_Report
  514.      *** We assume that the PRN_* stuff is already loaded.
  515.      *** Check that the printer is ready to roll, abort right now if not.
  516.      PgHd_Init()
  517.      declare header_[7]
  518.      header_[1]= Large("BILL COLLECTING SYSTEM")
  519.      header_[2]= Italic("Acme Corp, Inc. Ltd.")
  520.      header_[3]= []  && Blank line
  521.      header_[4]= Bold("People on the List")
  522.      header_[5]= []
  523.      header_[6]= Norm("Name        Address     Owes Us  $")
  524.      header_[7]= Norm("----------  ----------  ----------")
  525.      total_owed= 0
  526.      select 1
  527.      use DATA
  528.      do while .not. eof()
  529.           PageHead()
  530.           Norm_On()
  531.           NextLine(1, DATA->Name)
  532.           SameLine(2, DATA->Address)
  533.           if DATA->Owes_Us > 0
  534.                Bold_On()
  535.                SameLine(2, DATA->Owes_Us, "999,999.99")
  536.                Bold_Off()
  537.           else
  538.                SameLine(2, Italic("Paid up"))
  539.           endif
  540.           total_owed= total_owed +DATA->Owes_Us
  541.           skip
  542.      enddo
  543.      close databases
  544.      if prn_first
  545.           @ 10, 20 say "Nobody is on the list!"
  546.      else
  547.           NextLine(2, "Total Owed to Us:")
  548.           SameLine(1, total_owed, "@B 999,999,999.99")
  549.           PrnEject()
  550.           PrintOff()
  551.      endif
  552.      return
  553.  
  554.  
  555.                                                    Printer Control Tutorial
  556.                                                                     Page 11
  557. As you can see from this report, you must declare an array with
  558. as many elements as there are lines in your heading.  The array
  559. name must be HEADER_. (I like to stick a trailing underscore
  560. character after all array names so I can tell just by looking
  561. which variables are arrays and which are not.)  The PageHead()
  562. function then uses the array, with any attributes you have
  563. included, to print the header.
  564.  
  565. PageHead() is called once each pass, at the top of the printing
  566. loop.  You'll get a page eject and header at the appropriate
  567. spot.
  568.  
  569. Note the use of the PRN_FIRST flag to determine whether or not
  570. it makes sense to eject and shut the printer off, since it
  571. wasn't turned on if there was no data.  Yes, yes, I could have
  572. checked the record count or something and figured this out right
  573. after the DBF was opened.  Remember that there are times when
  574. you just can't tell in advance, and this method handles both
  575. with a simple IF..ENDIF at the end of the report.
  576.  
  577.  
  578. === SUMMARY ===
  579.  
  580. Variations on the printer control functions just described have
  581. served me well for many years.  They suit my programming style
  582. and cover the wide range of printing routines I develop most
  583. often.  They cut down on boring, repetive and error-prone runs
  584. of code and make the purpose and structure of a report more
  585. obvious.
  586.  
  587. These functions serve as a foundation for a much more complex
  588. set of extended printing functions and options that I use to
  589. develop commercial software.  Such extensions include printing
  590. to file, printing to screen, displaying a "live" window on what
  591. the printer is printing, suppressing printer control codes for
  592. more useful print-to-file output, and others.  All extensions
  593. require absolutely no changes in the way the report is coded,
  594. the library handles everything.
  595.  
  596. I welcome any comments and suggestions you have, even criticism
  597. is welcome because it can either strengthen my resolve when I
  598. shoot your opinion down or improves the library when you have a
  599. superior technique.
  600.  
  601. I can be reached at:
  602.  
  603.      Yellick Computing
  604.      509 Maple Square
  605.      Wayzata, MN 55391-1036
  606.      (612) 473-1805
  607.  
  608. Or, for E-Mail:
  609.  
  610.      The Source          BEB817
  611.      CompuServe          71121,2164
  612.  
  613.                                                     *** eof PRN_LIB.DOC ***
  614.