home *** CD-ROM | disk | FTP | other *** search
-
-
- Tutorial on Printer Control TWIN CITIES
- By Craig Yellick dBASE COMPILER USER GROUP
- August, 1988
-
-
- In this tutorial I present my method for controlling the printer
- and the printed page. I am a professional software developer
- and do all of my database programming in Clipper, along with the
- Tom Rettig library and a few other minor extensions. My full
- library of printer control functions is much more complicated
- than I'd care to explain in a tutorial, besides the fact that
- I'd rather keep the really sexy stuff proprietary (what ya want
- fer free?), so I'm presenting the core functions to which you
- can add the fancy extensions.
-
- I've been programming in dBASE-like languages since dBASE-II ran
- under CP/M on 8-inch disks. My printer control techniques have
- been evolving for all these years and really took off when
- user-defined functions were pioneered by Nantucket's first
- release of Clipper. I'm telling you all this because I want you
- to take a hard look at these methods. Even if you already have
- a satisfactory method for messing with printers you may get some
- ideas for improving it. I have dozens of wildly different
- systems up and running at customer sites all over the country
- and they all implement a common library of control functions.
- Believe me when I tell you these techniques have survived heavy
- abuse by ham-fisted end users who change their specs on an
- hourly basis.
-
- I will present my techniques in source code that should run
- under any dBASE-syntax compiler that supports user-defined
- functions and at least one-dimensional arrays. My style is
- admittedly biased towards Clipper. There is a companion
- diskette containing the actual source code for the functions and
- some examples.
-
- Design goals:
-
- 1) Effortlessly and transparently support lots of printer models
- 2) Make it easy to use printer features in your program
- 3) Make the function library do the most of the busy-work
- 4) Reduce the number of lines of code required
- 5) Make the function calls self-documenting in context
- 6) Make that sucker bullet proof!
-
- The functions are divided into three classes.
-
- Initialize Setting everything up
- Control Codes Sending control codes to the printer
- Page Headings Handling page headings and margins
-
-
- Printer Control Tutorial
- Page 2
- === INITIALIZE ===
-
- Before printing anything we need to establish the environment
- for the printing functions to work in. This is done once when
- your application is first started and does not need to be
- repeated. (If memory is really tight and you prefer to get rid
- printer related items when they are not needed you may have to
- perform this initialization process before each printed report.)
-
- This is a good time to introduce you to the memory variables I
- use. All start with the prefix PRN_ so they can easily be saved
- and restored in MEM files, and can be quickly identified in
- debugger and source code dumps.
-
- I support six print-style attributes.
-
- NORM Normal, the printer's default style
- COND Condensed, typically 132 columns across page
- BOLD Boldfaced, usually a "double-strike" version of NORMAL
- ITAL Italic, not supported on all printers
- LARG Large or extended, usually "double-wide" version of NORMAL
- UND Underscore, exact effect depends on printer model
-
- I use an "on" and "off" method for sending printer codes, so
- coupled with the PRN_ prefix the complete set of twelve control
- strings takes the following form.
-
- PRN_attrib 1 to turn the attribute on
- PRN_attrib 0 to turn it off again
-
- Example: PRN_COND1 turns condensed on,
- PRN_COND0 turns it off.
-
- Why use separate ON and OFF codes? And what about bold-italic
- or condensed- underscore combinations?
-
- If you think about it you'll answer one question with the other.
- This list of codes covers just about every printer model's range
- of capabilities. Most printers can turn attributes on and off
- independently so you can have any combination you can stomach,
- like bold-italic-underscore. The printer initialize and exit
- strings allow you to switch into different fonts and back out
- again without too much fuss, although since font changes aren't
- common to all printers (and not a very good idea for database
- programming, anyway) their support is limited. The init/exit
- strings are intended for throwing printers in and out of draft
- and letter-quality modes for the duration of a report. This
- allows you to put network printers back into their proper
- "default" state if you need to change them.
-
-
- Printer Control Tutorial
- Page 3
- What about printers that don't support italics or some other
- attribute?
-
- Simply use a reasonable alternate, like boldface. You normally
- would use italics to make something stand out, so choose
- something appropriate. If boldface isn't available then use
- "normal" for both. You can leave any attribute string null
- (empty) with no problems. Most matrix printers can get into
- condensed at a minimum. Impact printers are stuck with which
- ever type element is installed and there's not much you can do
- about it.
-
- What about printers that don't use ON/OFF-style control codes?
-
- Just pretend that the OFF strings don't exist. Use only the ON
- strings, but keep in mind that if your application switches type
- styles on the same line you may get strange results unless each
- ON string turns all the other attributes OFF. For example, the
- NORMAL-ON string would have to turn all the other attributes
- OFF, since NORMAL-ON wouldn't necessarily know which others were
- in use. This results in longer control strings but accommodates
- almost all printers on the market and drives them at their
- highest level of performance.
-
- I use three more strings for sending other controls-
-
- INIT Initialization string, sent once before report
- EXIT Exit string, sent once when report is done
- EJECT Page eject string
-
- These are also prefixed with PRN_
-
- Why use an eject string when the EJECT command does the same thing?
-
- Some printers need more than the simple ASCII chr(12) or "form feed"
- character to properly perform a page eject. I always stick a carriage
- return after the chr(12) so the printer buffer flushes and the eject is
- performed right away, some printers will leave the last page in the platen
- until the next report starts printing. Applications using a cut-sheet
- feeder sometimes need an additional command to load another sheet. A
- variable eject string also allows you to set up a "debugging" printer
- model which prints "---PAGE EJECT---" instead of a real page eject, for
- testing a report without wasting paper.
-
- The INIT and EXIT strings are also useful for inserting additional page
- ejects before and/or after a report. In some situations an extra eject at
- the end of a report makes it much easier to detach the paper, and adds
- more physical space between batches of reports. The extra eject at the
- start of a report may be wasteful, but sometimes required if another
- application has the bad habit of leaving the printer half way through a
- page. You would not believe the number of so called "professional"
- software packages that just stop printing when a report is complete with
- no final eject. Aaaaargh. And to top it off, YOUR application gets the
- blame for printing on someone else's reports!
-
-
- Printer Control Tutorial
- Page 4
- A final, non-printing string is named PRN_MODEL and is used
- solely for identifying which set of codes is loaded.
-
- We're not done yet. Next come the page format controls, all prefixed with
- PRN_. All values are assumed to be measured in "normal" lines and column
- positions.
-
- LMAR Left margin, default is zero
- RMAR Right margin, default is 80
- XMAR Extended right margin, default is 132
- TMAR Top margin, default is 3
- BMAR Bottom margin, default is 5
- PLEN Page length, default is 66 lines
-
- LMAR, TMAR, BMAR and PLEN are used by the function library for inserting a
- left margin and calculating when it's time to eject a page and print a
- heading. The RMAR and XMAR are for your application's use when attempting
- to right-justify output according to the user-defined margins. RMAR is
- for "normal" 80 column text and XMAR is for an extended right margin for
- condensed, 132 column printing. You may also use RMAR and XMAR for
- trimming long lines or splitting wide reports across two sheets of paper
- (like spreadsheets), which is easier to do than you might think.
-
- Three other variables are used by the printer control functions to
- communicate with each other.
-
- LINE Current page line
- PAGE Current page number
- FIRST Flag for "is this the first page?"
-
- There are two methods for getting the printer control variables into
- memory. You can save a set to a MEM file (they all start with PRN_ so
- they can be saved easily), or you can read them from a database file full
- of different model-specific codes. In either case you'll need to get them
- into memory variables where they can be manipulated without messing with a
- DBF file. I prefer to use a MEM file unless the printer model needs to be
- changed constantly.
-
- To ensure that no matter what configuration stuff is missing out on the
- hard disk the application can still print something, I use a double
- install procedure which starts with reasonable defaults and then looks for
- more specific values in a PRINTER.MEM file. If you prefer a DBF full of
- settings, you could modify the process fairly easily to assign the memvar
- with DBF field values instead of restoring from a MEM file. We'll keep it
- simple and use a MEM file in this tutorial library. But, here's an
- example for using a DBF file for those who prefer.
-
- *** Example in pseudo-code
- < init all variables with defaults >
- Is the DBF available?
- Yes- open it and locate the appropriate model
- No- return, we'll have to use the defaults
- For each field in the DBF do
- prn_xxx= DBF->xxx
- enddo
- return
-
-
- Printer Control Tutorial
- Page 5
- I place the following code in a function called PRN_INIT().
- The function accepts an optional model-ID to load. The function
- returns a logical value: TRUE if a printer controls were loaded
- and FALSE if the defaults are being used.
-
- function PRN_INIT in pseudo-code
- parameters model
-
- *** Establish all as public and start with defaults
- *
- public <all of the PRN_ variables>
- store [] to <all of the printer control strings>
- prn_model= "DEFAULTS"
- prn_eject= chr(12) +chr(13) && Form-feed plus CR
- prn_cond1= chr(15) && ^O condensed on most printers
- prn_cond0= chr(18) && ^R normal on most printers
- prn_xxxxx= nnn <establish margin defaults>
-
- *** Attempt to load a custom set
- *
- If a PRINTER.MEM file exists, RESTORE ADDITIVE.
- Or, if the MODEL parameter was specified, try to
- restore it, instead.
-
- Return TRUE if a custom set was successfully loaded, or
- return FALSE if the defaults are being used.
-
- This function needs to be called only once, unless you are
- loading different sets of codes or really need the memory space
- in a bad way. To get rid of everything related to printing-
-
- RELEASE ALL LIKE PRN_*
-
- If the defaults end up being used you will be able to switch
- between NORMAL and CONDENSED on most printer models. The other
- attributes will have no effect.
-
-
- === SENDING CONTROL CODES ===
-
- There are several functions for putting all the printer controls
- to productive use.
-
- Attrib Send text with ATTRIB, limited to duration of text
- Attrib_On Send ATTRIB-on string
- Attrib_Off Send ATTRIB-off string
-
- (Where Attrib = NORM, COND, BOLD, LARGE, ITALIC, UNDSCR)
-
- PrnEject Send the page-eject string
- PrintOn Get ready to start printing, send INIT string
- PrintOff Done printing, send EXIT string
-
-
- Printer Control Tutorial
- Page 6
- Other functions do the actual printing of the report contents.
-
- NextLine Start printing indented to left margin on next line
- SameLine Print at current row and column position
-
- Let's look at the ATTRIB(), ATTRIB_ON() and ATTRIB_OFF()
- functions in detail. The ON and OFF functions take no
- parameters, they merely send the appropriate attribute string.
-
- *** Example of ON and OFF functions
- *
- PrintOn()
- * device is set to printer and PRN_INIT string is sent
- Norm_On()
- * printed lines will be in "normal" style
- Italic_On()
- * lines are now in italic
- Cond_On()
- * lines are now in condensed-italic
- Italic_Off()
- * lines are now in condensed, only
- PrnEject()
- * take a wild guess
- PrintOff()
- * EXIT string is sent and device is set back to screen
-
- The purpose of the ON and OFF functions are to turn an attribute
- on and leave it on until you shut it off. The ATTRIB()
- functions are just as easy to use. They effect only the string
- sent as an argument, an ON and OFF is attached to the beginning
- and end of the string. They do not effect other current
- ATTRIB_ON() commands.
-
- *** Example of ATTRIB functions
- *
- PrintOn()
- @ prow() +1, 0 say Bold("This is in boldface")
- @ prow() +2, 0 say Large("And this is in large type")
- @ prow(), pcol() +2 say Italic(UndScr("Both italic & underscored"))
- PrnEject()
- PrintOff()
-
- I'll bet you are tired of typing @ PROW(), PCOL() SAY all the
- time. And if you are using some kind of variable left margin,
- you type even more.
-
- @ prow(), pcol() +left_margin say "something"
-
-
- Printer Control Tutorial
- Page 7
- This is where the NEXTLINE() and SAMELINE() functions really
- make life easier. NEXTLINE() moves down a specified number of
- rows, inserts a number of "normal" spaces as a left margin, and
- prints whatever data was sent as a parameter.
-
- function NextLine
- parameters how_many, what
-
- do case && Both parameters are optional
- case pcount() = 0 && So take reasonable defaults
- how_many= 1
- what= []
- case pcount() = 1
- what= []
- endcase
-
- @ prow() +how_many, 0 say Norm(space(prn_lmar)) && Left margin
- @ prow(), pcol() say what
-
- prn_line= prn_line +how_many && Update the line counter
-
- return prow() && Return the internal row counter
-
- SAMELINE() stays on the current row and column and prints the
- data with an optional picture template. Please note that in
- both functions the "what" parameter is type- less, any data type
- (char, num, date etc) can be used.
-
- function SameLine
- parameters cols_over, what, with_pict
-
- if pcount() < 2
- @ prow(), pcol() +cols_over say [] && Just move the print head
- else
- if pcount() < 3
- @ prow(), pcol() +cols_over say what
- else
- @ prow(), pcol() +cols_over say what pict "&with_pict."
- endif
- endif
-
- return pcol() && Return the internal column counter
-
- Here is some sample source code.
-
- PrintOn()
- NextLine("Here is the first line")
- NextLine(2, "Two lines further on...")
- SameLine(3, "...and three cols over")
- NextLine(2, Italic("ABCDEFG"))
- SameLine(0, UndScr(Italic("HIJKLM")))
- Cond_On()
- SameLine(3, "OPQRST")
- Italic_On()
- SameLine(3, "TUVWXYZ")
- NextLine(2, Bold("Formatting Example:"))
- SameLine(2, 123456789, "999,999,999.99")
- PrnEject()
- PrintOff()
-
-
- Printer Control Tutorial
- Page 8
- Here are some things to keep in mind when using NEXTLINE(),
- SAMELINE() and the various attribute functions.
-
- o NEXTLINE() inserts a "normal" left margin. Be aware when
- counting columns that you don't think of the left margin as
- being "condensed" along with the body of the report.
-
- o If you want to print formatted data in the first column,
- don't use NEXTLINE() to print the data. Instead use NEXTLINE()
- to move down a line and insert the margin, followed by a
- SAMELINE() that does the actual data printing. e.g.
-
- NextLine()
- SameLine(0, DBF->Number, "999,999.99")
-
- o In the same manner, if you want to print the entire line in a
- particular style- don't wrap every SAMELINE() with an ATTRIB().
- Issue an ATTRIB_ON() followed by as many SAMELINE() as needed. e.g.
-
- *** Good *** Not Good
- NextLine() NextLine(1, Cond("Column 1"))
- Cond_On() SameLine(1, Cond("Column 2"))
- SameLine(0, "Column One") * etc... for 20 columns
- SameLine(1, "Column Two")
- * etc... for 20 columns
-
-
- === PAGE HEADINGS ===
-
- The final component of this tutorial is a page heading function.
- Everyone has a method they will defend to their death. I am
- quite happy with mine because it is simple. It does the job for
- a wide variety of applications without complicated "custom"
- hacks.
-
- My page header technique is to call the function whenever the
- report considers it acceptable. The function only issues a
- header when the conditions are right. This usually means
- calling the function on each pass through the main report loop,
- before each line is printed. When printing subtotals or other
- "chunks" of lines you DON'T call the header function if you
- DON'T want the possibility of the chunk being broken by a new
- page. Very simple to implement, and the code returns quickly so
- there is no penalty for calling it so often.
-
-
- Printer Control Tutorial
- Page 9
- One thing I do that you may not have seen before- the page
- heading function turns the printer on, NOT the report procedure.
- This prevents a header with no data beneath it from being printed in the
- case where no data qualifies for the report. The printer isn't even
- turned on until there is something to print. I have other functions that
- check the printer for "on-line" etc long before the main report body
- starts running. For example, suppose you have an "Overdue Accounts"
- report, and it would take a lot of run-time to determine if there really
- ARE any accounts overdue. You could print the entire report in the time
- it would take to figure out that yes, there are some. With my method you
- can just jump right in, and if no data is found nothing gets printed. You
- can display a message saying that nothing was found rather than printing
- an empty report. Certainly there are situations where you WANT a header
- printed even though there is no data, perhaps with a line saying "No
- accounts are overdue". No problem, just call the header routine and print
- the line. The point is- you have an option. I've found that my customers
- appreciate the printer sitting quietly unless there is something useful
- coming out of it, especially if the printer is on a network with a busy
- spooler and is housed 200 yards down the hall!
-
- Here is the outline for such page heading function.
-
- function PageHead in pseudo-code
-
- if the current line plus the required bottom margin
- is greater than the page length it is time to print a
- page heading. Otherwise return without doing anything.
-
- Increment the page counter.
- Reset the line counter to line zero.
-
- If this is the first page printed so far, don't need to eject
- but we DO need to turn the printer on and send the INIT string.
-
- If this isn't the first page, eject the current page.
-
- Now we're at the top, so move done beyond the top margin.
-
- For as many heading lines as are currently defined...
- Print a heading line
- ... Done.
- New current line is equal to the top margin
- plus the number of header lines.
-
- Return: YES or NO- did we print a header?
-
-
- Printer Control Tutorial
- Page 10
- Another function, PGHD_INIT(), is used to initialize the three
- header function variables to their proper starting values. It is a very
- simple function used purely for convenience.
-
- function PgHd_Init
- prn_first= .t. && Yup, first page so far
- prn_page= 0 && PageHead increments counter so start at zero
- prn_line= 99999 && Force a "need an eject" condition
- return []
-
- You should call this function once before starting the main reporting
- loop. You can use the status of PRN_FIRST to tell if anything was printed-
- it will be FALSE if data was printed, TRUE if not. If you want to start
- numbering at different page you can assign the PRN_PAGE variable after the
- call to PGHD_INIT(). There is one more requirement for setting up a
- header, which we will see in the following sample code. Here is an
- example of a full-blown report.
-
- Procedure A_Report
- *** We assume that the PRN_* stuff is already loaded.
- *** Check that the printer is ready to roll, abort right now if not.
- PgHd_Init()
- declare header_[7]
- header_[1]= Large("BILL COLLECTING SYSTEM")
- header_[2]= Italic("Acme Corp, Inc. Ltd.")
- header_[3]= [] && Blank line
- header_[4]= Bold("People on the List")
- header_[5]= []
- header_[6]= Norm("Name Address Owes Us $")
- header_[7]= Norm("---------- ---------- ----------")
- total_owed= 0
- select 1
- use DATA
- do while .not. eof()
- PageHead()
- Norm_On()
- NextLine(1, DATA->Name)
- SameLine(2, DATA->Address)
- if DATA->Owes_Us > 0
- Bold_On()
- SameLine(2, DATA->Owes_Us, "999,999.99")
- Bold_Off()
- else
- SameLine(2, Italic("Paid up"))
- endif
- total_owed= total_owed +DATA->Owes_Us
- skip
- enddo
- close databases
- if prn_first
- @ 10, 20 say "Nobody is on the list!"
- else
- NextLine(2, "Total Owed to Us:")
- SameLine(1, total_owed, "@B 999,999,999.99")
- PrnEject()
- PrintOff()
- endif
- return
-
-
- Printer Control Tutorial
- Page 11
- As you can see from this report, you must declare an array with
- as many elements as there are lines in your heading. The array
- name must be HEADER_. (I like to stick a trailing underscore
- character after all array names so I can tell just by looking
- which variables are arrays and which are not.) The PageHead()
- function then uses the array, with any attributes you have
- included, to print the header.
-
- PageHead() is called once each pass, at the top of the printing
- loop. You'll get a page eject and header at the appropriate
- spot.
-
- Note the use of the PRN_FIRST flag to determine whether or not
- it makes sense to eject and shut the printer off, since it
- wasn't turned on if there was no data. Yes, yes, I could have
- checked the record count or something and figured this out right
- after the DBF was opened. Remember that there are times when
- you just can't tell in advance, and this method handles both
- with a simple IF..ENDIF at the end of the report.
-
-
- === SUMMARY ===
-
- Variations on the printer control functions just described have
- served me well for many years. They suit my programming style
- and cover the wide range of printing routines I develop most
- often. They cut down on boring, repetive and error-prone runs
- of code and make the purpose and structure of a report more
- obvious.
-
- These functions serve as a foundation for a much more complex
- set of extended printing functions and options that I use to
- develop commercial software. Such extensions include printing
- to file, printing to screen, displaying a "live" window on what
- the printer is printing, suppressing printer control codes for
- more useful print-to-file output, and others. All extensions
- require absolutely no changes in the way the report is coded,
- the library handles everything.
-
- I welcome any comments and suggestions you have, even criticism
- is welcome because it can either strengthen my resolve when I
- shoot your opinion down or improves the library when you have a
- superior technique.
-
- I can be reached at:
-
- Yellick Computing
- 509 Maple Square
- Wayzata, MN 55391-1036
- (612) 473-1805
-
- Or, for E-Mail:
-
- The Source BEB817
- CompuServe 71121,2164
-
- *** eof PRN_LIB.DOC ***