HOME - - - - - - - TUTORIALS INDEX - - - - - - - - - - - - Other material for programmers

Delphi: Writing to the printer a line at a time



Tutorial written and tested in Delphi 2, on a Win98 machine, but the code should be pretty generally applicable.

Problem: I had a fine program from the days of Turbo Pascal / DOS / line oriented printers. I wanted to port it to Windows with minimal fuss.

Good news: It didn't do anything too clever with printer control codes.... I (mostly) assembled a line, then sent it to the printer.

The line of code in Turbo Pascal (TP) which printed a line of text was writeln(lst,"Line of text") (The first parameter is LST, not "First". LST for LiSTing device, aka LPT1) If you sent...

write(lst,'Line ')
write(lst,'of ')
writeln(lst,'text')

... then the first two lines of code (N.B. "write", not "writeln") sent stuff to the printer where it was held in a buffer. As soon as you did a writeLN(lst,... you got what was in the buffer, plus anything specified by the writeln.

The objective was to create a Delphi equivalent to be called SendLine which had a single string type parameter, i.e. the Delphi equivalent of writeln(lst,'Line of text') will be SendLine('Line of Text'), and equivalent of write(lst,'Part of line of text') will be AddToLine('Part of line of Text'). The editor's search and replace facility should make converting the program code easy... change all {writeln(lst,} to {SendLine(} should do it!

Windows may require fancier "Init printer" and "Close printer" stuff... we'll see. In TP you just used the printer without explicitly opening the channel to it. You did, however, often send "control codes" to set the printer's font, etc. There was no "Close printer" code needed, other than ensuring that your program had a writeln(lst, after any near-the-end writeln(lst, commands.

For the sake of this tutorial, imagine that the old program was....

InitForPrinterOutput;
StartANewPage;
writeln(lst,'1st line of text');
writeln(lst,'2nd line of text');
writeln(lst,'3rd line of text');
StartANewPage;
writeln(lst,'Pg2 1st line');
writeln(lst,'Pg2 2nd line');
write(lst,'A bit of a third line ');
writeln(lst,'of text ');
writeln(lst,'The end');
FinishPrinting;


Start a project in the usual way. I called my DD40

To start with, we'll attempt the time honored job of outputting "Hello World".. but outputting it to the printer when a button is pushed.

Here's a quote from the Delphi 2 helpfile that may have stuff some users will need...

"The Printer function creates an instance of a global TPrinter object the first time it is called. Use the Printer function when you want to print using the TPrinter object. Printer is declared in the Printers unit. Whenever you use the Printer function and the TPrinter object, you must add Printers to the uses clause of your unit.

"Note: : Code which assigned to the Printer global variable in Delphi 1.0 will need to be changed to call the SetPrinter function instead. This allows the entire contents of the Printers unit to be smart linked out if nothing in the program explicitly uses printers or calls Form.Print."

Add "Printers" to the form's uses clause.

Put a button on the form

Double click it

Insert the following code (again, taken from the Delphi 2 help file) in the event handler....
Printer.BeginDoc;
Printer.NewPage; (*See comment*)
Printer.Canvas.TextOut(300,300, 'Hello World');
Printer.EndDoc;
That was pretty painless! Don't worry all of this will soon descend into the painful depths you know so well.

Minor comment on NewPage (just ignore this, leave NewPage in if in doubt): I think you can leave NewPage out if you know that there is nothing left over from previous uses of the printer within a running program. I was getting a blank sheet for each run of the program which I think traces to the NewPage.

First point: When there has been a call of BeginDoc, there should be a call of EndDoc later. (No output is seen, by the way, until the EndDoc is processed, unlike the old DOS days when typically lines were printed as soon as they were ready.) Of course, in the example, there's little danger of a BeginDoc happening without the an EndDoc. In a more realistic case, you may want to have a boolean variable, say "boDocBegunNotEnded which you set false during formcreate, true after a BeginDoc, and false again after an EndDoc. The help file does tell us that "The Close procedure calls the EndDoc method."... but I like to be sure about these things.

Second point: Because you can print anywhere you want on the page (Good! Next to impossible in DOS, and hard even to simulate it) you can end up putting things on top of each other (Usually bad!) Not only would that happen if you did....
Printer.Canvas.TextOut(300,300, 'Hello World');
Printer.Canvas.TextOut(300,300, 'On top of other');
.... but it would (obviously) also happen if you did....
Printer.Canvas.TextOut(300,300, 'Hello World');
Printer.Canvas.TextOut(301,301, 'On top of other');
Not so obvious: What happens if you do....
Printer.Canvas.TextOut(300,300, 'Hello World');
Printer.Canvas.TextOut(300,400, 'On top of other?');
...? That depends, of course on what point in the two strings is placed at 100,100 and 100,200 (likely the upper left corner) and on the size of the writing.

Now... vertical spacing is pretty easy to manage by trial and error. (Better methods could be addressed in a more advanced tutorial. Anyone reading this? Interested? See bottom of page.) Horizontal spacing, usually in Windows, is much more difficult.
Printer.Canvas.TextOut(300,300, 'Hello Worldiiiiiiiiiiiiiiiiiiiiiii');
Printer.Canvas.TextOut(1200,300, 'On same line');
might come out quite nicely... it did on my system. However, in Windows, what you get depends on a whole bunch of settings... in this case the typeface and character size will have a lot to do with what result you get. Even if the above came out ok, the following.... with the same number of "W"s as the previous had "i"s did not print nicely on my system.
Printer.Canvas.TextOut(300,300, 'Hello WorldWWWWWWWWWWWWWWWWWWWWWWW');
Printer.Canvas.TextOut(1200,300, 'On same line');
Again... "good" answers to these problems are matters for a more advanced tutorial. For now, we're going to "solve" the problems by using a font with a fixed pitch: Courier New.

Replace the two TextOut lines with the following. I am assuming (always a bad idea, and reveals another weak spot in the program) that you have the Courier New font on your system. If not, substitute another non-proportional font.
  Printer.Canvas.font.name:='Courier New';
  Printer.Canvas.font.size:=12;
  Printer.Canvas.TextOut(300,300, 'y23456789012345678901234567890');
  Printer.Canvas.TextOut(1200,300, 'X');
  Printer.Canvas.TextOut(700,400, 'World');
  Printer.Canvas.TextOut(300,400, 'Hello');
There are a couple of lessons in the output: "Hello World": Notice we "printed" the second word first? There's no need to use code to generate a page from top left to bottom right. You might "print" headings with a fixed procedure, and then fill in the fields in a later procedure.

In the first line, a 6 is missing, replaced by an X. You don't see the 6 under the X, as you would if you double typed with an old fashioned typewriter. There is probably a way to get that effect if you want it, though. (Tell me the details if you chase them down? Just the setting of a pen property, I'd guess.)

It seems that with 12pt text, line spacing of 100 is adequate, if a little tight. The "y" was there to be sure that descenders aren't blotted out by subsequent printing of the next lower line.

(A good source of info: Search the help systems for "canvases", then select the subtopic "using the canvas". Of course, if your Delphi is not version 2, the linking of canvas material may have been improved.)

So! We haven't covered all of "the best way to print to paper with Windows"... but we do have the essentials for what I wanted to do.

The Delphi program can't be quite as simple as the TP program it is replacing. For each page, printing will now be in three parts...

Start a page, inside the pc. Fill the internal version with the whole page's text. ... and when everything that's going on the page has been put on the internal version, Print out the assembled page as real ink, real paper.

Happily, though the concept is slightly different, the outline of the code is the same as long as you always issue a "Finish Printing" command after the last "writeln(lst," or "write(lst" equivalent... something you didn't need to do in TP programs with a "writeln(lst" finishing off the printing. Our earlier hypothetical TP program translates as follows:
InitForPrinterOutputWV; (*WV: Windows version*)
StartANewPageWV;
SendLine('1st line of text');
SendLine('2nd line of text');
SendLine('3rd line of text');
StartANewPageWV;
SendLine('Pg2 1st line');
SendLine('Pg2 2nd line');
AddToLine('A bit of a third line ');
SendLine('of text ');
SendLine('The end');
FinishPrintingWV;
A real program would need more error checking than I am including here. For instance, it would be necessary to check that lines were not too long, and that not too many lines were sent to a page.

By the way- a detail: writeln was able to take a variable number of parameters, i.e. to print out the contents of three string variables, you could do....
writeln(lst,sVar1,sVar2,sVar3);
For our program, that would convert to....
SendLine(sVar1+sVar2+sVar3);
And... continuing with the detail... writeln could print out the contents of various types of variable whereas SendLine requires a string. Not a problem, thanks to Delphi's rich vocabulary....

writeln(lst,nANumber); becomes SendLine(inttostr(nANumber);

(See Delphi helpfile notes on "floattostr" and "floattostrf" for the fancy stuff. End of detail's resolution)

We're going to have some global variables and constants to help things along. All will have LPR in their names, derived from "Line Printer Replacement"
const
bLPRheight=100; (*Line pitch, vertical*)

variables (Insert the following just after the "Private" keyword in the Form's type definition. This makes them "fields" of the form, but you can think of them as variables.)

     sLPRBuffer:string;(*Holds line as it is being assembled from misc AddToLines*)
     bLPRYPos:byte;(*Count of line which is next to use. First line: "0"*)
Revise the "Print" button as follows:
procedure TForm1.buPrintStuffClick(Sender: TObject);
begin
InitForPrinterOutputWV; (*WV: Windows version*)
StartANewPageWV;
SendLine('1st line of text');
SendLine('2nd line of text');
SendLine('3rd line of text');
//StartANewPageWV;
//SendLine('Pg2 1st line');
//SendLine('Pg2 2nd line');
//AddToLine('A bit of a third line ');
//SendLine('of text ');
//SendLine('The end');
FinishPrintingWV;
Create first attempts at the four procedures as follows. For each, you will also have to make an entry in the form's declaration. I.e., put the following in to the form's class definition, just before the word "Public". (Procedure declarations have to follow the field declarations in both the public and the private sections of the class declaration, by the way.)
procedure InitForPrinterOutputWV;
procedure SendLine(sTmp:string);
procedure AddToLine(sTmp:string);
procedure FinishPrintingWV;
And then, just above the final "end." add the following. the lines starting with "//" will have no effect for the moment... get what does have an effect working first.
procedure TForm1.InitForPrinterOutputWV;
begin
(*Not a lot needed in this simple example... but you might want, for instance,
to assign text to a page header string that gets printed at the start of each
page. This would be the place to do it. In a more complicated case, you
might also want to alter the font, and other things might need changing
as a result, and you would do those things here, too.*)
//Printer.Canvas.font.name:='Courier New';
//Printer.Canvas.font.size:=12;
//Printer.BeginDoc;
end;

procedure TForm1.StartANewPageWV;
begin
sLPRBuffer:='';
bLPRYPos:=0;
//Printer.NewPage;
end;

procedure TForm1.SendLine(sTmp:string);
begin
sLPRBuffer:=sLPRBuffer+sTmp;
showmessage('Would print: '+sLPRBuffer);
//Printer.Canvas.TextOut(300,bLPRYPos*bLPRHeight,sLPRBuffer);
inc(bLPRYPos);
sLPRBuffer:='';
end;

procedure TForm1.AddToLine(sTmp:string);
begin
sLPRBuffer:=sLPRBuffer+sTmp;
end;

procedure TForm1.FinishPrintingWV;
begin
if sLPRBuffer<>''then SendLine(sLPRBuffer);
//Printer.EndDoc;
end;
When the above works, take out most of the "//"s, but, for now, leave in those in the six line block starting with...
StartANewPageWV;

... and take out the... showmessage('Would print: '+sLPRBuffer);

As I've worked on this, I've become more convinced that the "Printer.NewPage;" command is not needed before the first page is printed, but that it is needed between pages. The only problem with the program as it stands is that you get a blank sheet before the first sensible page.

And, to finish off with, here are some quotes from the Delphi documentation which might help you better understand the issues raised in this tutorial. Always remember the Delphi "Help" files when you are stuck. Finding what you want takes practice, but there is a lot of useful material in those files. When you're working on a program, if you need help on "whatsit", all you have to do is click on the word, and then do ctrl-f1.
============
Fonts Property:
Applies to

TPrinter object; TScreen component

Declaration- property Fonts: TStrings;

Description:

Run-time and read-only. The Fonts property for the screen component returns a list of fonts supported by the screen.
The Fonts property for a printer object holds a list of fonts supported by the printer. The list contains TrueType fonts even if the printer doesn't support them natively because the Windows Graphics Device Interface (GDI) can draw TrueType fonts accurately when a print job uses them.

============
procedure TextRect(Rect: TRect; X, Y: Integer; const Text: string);

Description:

The TextRect method displays text inside a clipping rectangle. Any portions of the text passed in the Text parameter that fall "

====
Screen Variable:

Unit: Forms

Declaration- Screen: TScreen;

Description:

The Screen variable is a TScreen component that normally represents your screen device. By default, your application creates a screen component based on information from Windows about the current screen device and assigns it to Screen.

Example:

The following code sets the width of a form called Form1 to half the width of the screen:

Form1.Width := Screen.Width div 2;

======
NewPage
Description:

The NewPage method forces the current print job to begin printing on a new page in the printer. It also increments the value of the PageNumber property and resets the value of the Pen property of the Canvas back to (0, 0).

{end quotes}
Regarding NewPage: Not to niggle... but in case less experienced users are puzzled..... Part of that is believed to contain an error... probably should be resets PENPOS back to 0,0 And: The Delphi2 helpfile entry for TPrinter doesn't list Canvas as a property, but it does imply that there is one. Also TCanvas has a link to TCanvas for TPrinter

Forgive this rather abrupt end?
   Search this site or the web        powered by FreeFind
 
  Site search Web search
Site Map    What's New    Search
The search engine is not intelligent. It merely seeks the words you specify. It will not do anything sensible with "What does the 'could not compile' error mean?" It will just return references to pages with "what", "does", "could", "not".... etc.


Click here if you're feeling kind! (Promotes my site via "Top100Borland")
Ad from page's editor: Yes.. I do enjoy compiling these things for you... hope they are helpful. However.. this doesn't pay my bills!!! Sheepdog Software (tm) is supposed to help do that, so if you found this stuff useful, (and you run an MS-DOS or Windows pc) please visit my freeware and shareware page, download something, and circulate it for me? Links on your page to this page would also be appreciated!

Click here to visit editor's freeware, shareware page.


Link to Tutorials main page
How to email or write this page's editor, Tom Boyd



Valid HTML 4.01 Transitional Page WILL BE tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org


If this page causes a script to run, why? Because of things like Google panels, and the code for the search button. Why do I mention scripts? Be sure you know all you need to about spyware.

....... P a g e . . . E n d s .....