|
Printing Directly from Delphi |
Covers the basics of using Write and TextOut
to print from Delphi, giving consistent
results independent of printer.
Don Macrae
ADUG Presentation
15 October, 1996
![]()
Resources
Valuable material is contained in the series of articles on printing by Xavier Pacheco in the July and August issues of Delphi Magazine.
Also, Dtopics No 104 contains code for a useful program to print the contents of a memo.
Disclaimer
The author is not an expert. This is for what it's worth.
![]()
My requirement was to print a one page, formatted report to present the results of some calculations. The formatting was not extensive, but it did include a column of right-justified figures. The application was not associated with a database, and was to be run on any old sort of Windows PC and printer.
My initial intention was to use QuickReport. This turned out to be impractical because it turns out that QuickReport will not run unless the BDE is present, even if it doesn't actually use it. There is a version of QR currently in beta which will run without the BDE, but a beta version was not appropriate for distribution.
In relation to QuickReport, I note in passing that the report did appear to vary in size when printed on different printers. I don't know why, and didn't follow up on that.
Mark Weston had suggested to me that outputting to the printer's canvas was not too difficult, so I did that. Thank you, Mark.
My initial attempt was sad, resulting in such things as text about 1 mm high, or inconsistent results when run on different printers. Much of the problem turned out to caused by an actual bug in Delphi, I think, and this made the exercise seem complicated. In hindsight, once this problem was recognized and handled, the process was quite basic.
Tprint.DPR is a demo project which is the basis of this article. It contains a form with several buttons, with click event handlers which produce reports or generate and report on statistics related to the printing, such as PageHeight. You may want a copy, although the key code is included here.
There are two approaches which can be taken to printing from a Delphi program:
This is demonstrated in the following program:
procedure TPrintForm.WritelnButnClick(Sender: TObject);
var
PrintText:TextFile;
i,Th,Ph,Lpp,CurrentLine:integer;
const
TopMargin=4;
LeftMargin=' ';
begin
{Preliminary}
Printer.Canvas.Font.Name:='Book Antiqua';
Printer.Canvas.Font.Size:=14;
Th:=Printer.Canvas.TextHeight('P'); {Height of current font in pixels}
Ph:=Printer.PageHeight; {Page Height (printable) in pixels}
Lpp:=Ph div Th; {Lines Per Page}
AssignPrn(PrintText);
Rewrite(PrintText); {Open print file}
CurrentLine:=1;
{Top Margin}
for i:=0 to TopMargin-1 do
begin
Write(PrintText,#10); {#10 is a newline. Could have used Writeln(PrintText,'')}
Inc(CurrentLine);
end;
{Print report}
Write(PrintText,LeftMargin+'TextHeight = '+IntToStr(Printer.Canvas.TextHeight('P')));
Writeln(PrintText,#09#09#09'Line number = '+IntToStr(CurrentLine)); {#09 is a horizontal tab}
Inc(CurrentLine);
Write(PrintText,#10#10#10);
Inc(CurrentLine,3);
Writeln(PrintText,LeftMargin+'Another Line. Line number '+IntToStr(CurrentLine));
Inc(CurrentLine);
Writeln(PrintText,'');
Inc(CurrentLine);
For i:=1 to (Lpp-CurrentLine) do WriteLn(PrintText,'');
Write(PrintText,LeftMargin+'The last line on the page. Line number '+IntToStr(Lpp));
CloseFile(PrintText);
end;
The key thing about this approach is that it takes place sequentially, in a left to right and downward direction. Notice that I use tab characters to position some text, but the tabs apply in addition to any text already written to the line, as you would expect. This means that if I want to control where text other than the initial text in a line is printed I need to somehow measure the length of the initial text. This started to look difficult. Also, fiddling right justification would have been painful.
So this was not the right approach to my problem.
This is demonstrated in the following program:
procedure TPrintForm.TextOutButnClick(Sender: TObject);
var
ppix,ppiy,mmx,mmy,hmar,vmar,ls:integer;
begin
with printer do
begin
BeginDoc;
With Canvas do
begin
{Establish printer parameters}
ppix:=GetDeviceCaps(Handle,LOGPIXELSX); {Pixels per inch, x direction}
ppiy:=GetDeviceCaps(Handle,LOGPIXELSY); { " " y direction}
mmx:=round(ppix/25.4); {Pixels per mm, x direction}
mmy:=round(ppiy/25.4); { " " y direction}
hmar:=mmx*20; {Horizontal (left hand) margin}
vmar:=mmy*50; {Vertical (top) margin}
Font.PixelsPerInch:=ppix; {BUG!}
Font.Name:='Book Antiqua';
{Set font size and style, Print Heading}
Font.Size:=14;
Font.Style:=[fsBold];
textout(Hmar,Vmar,'This is the heading');
{Set font size and style, get line height for body}
SetFontSize(12);
Font.Style:=[]; {restore font style to regular (no style)}
ls:=textheight('X'); {line spacing = height of the current text.}
{Print body}
textout(Hmar,Vmar+10*ls,'This is 10 lines from the top of the page.');
{Print right justified column}
SetTextAlign(printer.canvas.handle,TA_RIGHT);
textout(Hmar+100*mmx,15*ls,FormatDateTime('d mmmm yyyy',Date));
textout(Hmar+100*mmx,16*ls,'$6,550,000.00');
setTextAlign(printer.canvas.handle,TA_LEFT);
{Print decorative heading}
with brush do
begin
style:=bsSolid;
color:=clGreen;
end;
Rectangle(HMar,ls*20,Printer.PageWidth,ls*23);
Font.Size:=18;
Font.Color:=clWhite;
Font.Style:=[fsBold];
TextOut(hmar+10*mmx,ls*21,'White heading in a green rectangle');
brush.color:=clwhite;
font.color:=clblack;
font.style:=[];
{Print large footer}
Font.Size:=24;
Font.Style:=[fsBold];
ls:=textheight('X');
TextOut(HMar,Printer.PageHeight-TextHeight('X'),'This is the end, my friend!');
end;
endDoc
end;
end;
Comments:
(1) The basic idea here is to generate some parameters which enable me to output text on a specified line, and/or at a specified number of millimetres from the lh side. So I needed to know, whatever font I was using, what was its height? After you've set your font and font size, you can do this satisfactorily by using
ls:=Printer.Canvas.Textheight('X');
ls is my linespacing variable, and X is an arbitrary character or string.
As always there's an API function which will get you the same info: GetTextMetrics: see later. I initially used that until I discovered the solution to my Delphi problems.
I needed also to know how many pixels there were in a horizontal millimetre, which I found as follows:
ppix:=GetDeviceCaps(Printer.Canvas.Handle,LOGPIXELSX);
GetDeviceCaps is an API function which provides access to a range of information about the specified device. Note that it cannot be used unless printing is happening, ie until after Printer.BeginDoc is called.
These parameters allowed me to use TextOut with mm values for y co-ordinates and line numbers for x.
(2) The bug. Note that there is a property called `PixelsPerInch' which belongs to the Printer.Canvas.Font. The bug is that it doesn't get reliably initilalized. Unless you set it somehow it starts off set to a value which probably relates to the screen. This program sets it (in effect) with
Printer.Canvas.Font.PixelsPerInch:= GetDeviceCaps(Printer.Canvas.Handle,LOGPIXELSX);
But just to confuse the issue, I found that the property will be set if you just call one of a range of Delphi Printer.Canvas methods, such as
ls:=Printer.Canvas.Textheight('X');
(3) BeginDoc starts a print job. Whether the printer, which is a variable in the Printers Unit, is printing or not is indicated by the value of Printer.Printing. Enddoc signals the end of the print job, and causes the printer to start printing.
There is another function called Printer.Abort which is supposed to kill the job, but my printer prints a page anyway. There was a Dtopics reference to a problem with Abort, which suggested that you use
AbortDoc(Printer.Canvas.Handle);
which is an API function from WinProcs. It didn't improve the situation for me.
(4) Right Justification
The Windows function SetTextAlign allows the direction of printing to be changed
SetTextAlign(printer.canvas.handle,TA_RIGHT);
Strangely enough, a TextOut after this statement will result in right justified text. Remember to return it to TA_LEFT when you've finished.
(5) Just for fun, I've printed some white text in a green rectangle.
(6) I've also printed something on the extreme bottom of the printable area. I know where this is because of the PageHeight function.
Miscellaneous Stuff.
The demo program contains a button which writes a number of statistics got from Delphi methods into a memo, and one which does the same with a few API functions - some of which we've already seen. This is the API button program:
procedure TPrintForm.WinStatsButnClick(Sender: TObject);
var
TM: TTextMetric;
begin
With Memo2.lines do
begin
Printer.Begindoc;
Add('PixelsPerInchX='+IntToStr(GetDeviceCaps(Printer.canvas.Handle,LOGPIXELSX)));
Add('PixelsPerInchY='+IntToStr(GetDeviceCaps(Printer.canvas.Handle,LOGPIXELSY)));
GetTextMetrics(Printer.Canvas.Handle,TM);
Add('TM.tmHeight='+IntToStr(TM.tmHeight));
Add('TM.tmAscent='+IntToStr(TM.tmAscent));
Add('TM.tmDescent='+IntToStr(TM.tmDescent));
Add('Horizontal device size, mm = `
+IntToStr(getDeviceCaps(Printer.Canvas.Handle,HORZSIZE)));
Add('Vertical Device Size, mm ='
+IntToStr(GetDeviceCaps(Printer.Canvas.Handle,VERTSIZE)));
Printer.Abort;
end;
end;
Notes:
(1) TextMetrics is a record containing a lot of information about the size of the current font. TmAscent is the distance from the baseline of the font to the top. TmDescent is the distance from the baseline to the bottom of the font, as in the bottom of j's and g's etc. tmHeight =tmAscent+tmDescent = Delphi's textHeight.
(2) HORZSIZE and VERTSIZE in this case are the printable dimensions of the page, in mm.