| Printer Demo #2 | Lab Report |
Purpose
The purposes of this project are to
Explore printing in Delphi, especially using various brush styles and pen styles with the 20 Windows System Colors.
Demonstrate the ability of a printer to display shades of gray.
Demonstrate an approach for device-independent graphics suitable for display on the screen or a printout.
Demonstrate how to define a clipping region for a canvas.
Materials and Equipment
Software Requirements
Windows 95/98/2000
Delphi 3/4/5 (to recompile)
PrinterDemo2.EXEHardware Requirements
VGA display
Windows printer
Procedure


Data
| Printer | Type | Comments/Features |
| Canon BJC-620 | Color ink jet | Good color.OK with regular paper. Line.Styles only work for Pen.Width = 1. Approximately 3.5 "dashes" per inch for psDash Pen.Style. |
| Epson Stylus Color 850 | Color ink jet | Excellent color, especially with photo-quality paper. "Shades of gray" have slight blue-green hue on photo-quality paper, but OK with regular paper. Pen.Styles only work for Pen.Width = 1. Approximately 6 "dashes" per inch for psDash Pen.Style. |
| HP DeskJet 540 | Color ink jet | Has terrible color and does not support Pen.Style with width greater then 1. The pie at 9 o'clock is does not print with color enabled, but does print a light dot pattern when the driver is forced to black mode. [Thanks to Jeff Wormsley for this information] |
| HP DeskJet 870C | Color ink jet | Pen.Styles are not very distinctive in black (they all appear to be the same) and are all psSolid for red. Background colors are wrong for most Brush.Styles, except for the first two rows in the output. |
| HP DesignJet 750C | Color ink jet "plotter" |
Excellent color. Only known printing device to support Pen.Styles for Pen.Widths > 1. |
| HP LaserJet II | Black/ White laser | Does not print the bottom line, but otherwise is similar to the HP III. [Thanks to Jeff Wormsley for this information] |
| HP LaserJet IIP | Black/ White laser | Approximately 6 "dashes" per inch for psDash Pen.Style (which is much more readable than the HP LaserJet 5). Approximately 38 "dots" per inch for psDot Pen.Style (which is not the solid line seen with a HP LaserJet 5). |
| HP LaserJet III | Black/ White laser | Approximately 10 "dashes" per inch for psDash Pen.Style (which is much more readable than the HP LaserJet 5). Approximately 38 "dots" per inch for psDot Pen.Style (which is not the solid line seen with a HP LaserJet 5). |
| HP LaserJet 5 | Black/ White laser | "Shades of gray" are in groups of 4. For example, RGB(i,i,i) for i = 252..255 are the same, 248..251 are the same, etc. Pen.Styles are not very distinctive. Approximately 26 "dashes" per inch for psDash Pen.Style! Line for psDot Pen.Style appears as a solid line. The first two rows of Brush.Style appear the same. Most of the colors (the columns) cannot be distinguished with the various Brush.Styles. |
| IBM 4039 | Black/ White laser | |
| Lexmark 4039 Plus | Black/ White laser |
Discussion
The System colors were defined in a CONST array:
CONST // GGBBRR clMoneyGreen = TColor($C0DCC0); // additional colors that don't dither clSkyBlue = TColor($F0CAA6); clCream = TColor($F0FBFF); clMediumGray = TColor($A4A0A0); ColorCount = 20; // These colors won't dither on 256-color display in Windows
colors : ARRAY[0..ColorCount-1] OF TColor =
(clBlack, clMaroon, clGreen, clOlive, clNavy, clPurple,
clTeal, clSilver,
clMoneyGreen, clSkyBlue, clCream, clMediumGray,
clGray, clRed, clLime, clYellow, clBlue, clFuchsia,
clAqua, clWhite);
|
The approach in this project was to define a single routine, DisplayGraphics, that draws on any canvas. In this routine, the Clipping parameter determines when a clipping region is imposed on subsequent drawings.
When requested, a complex Windows region is defined to establish this clipping area. CreateRectRgn and CreateEllipticRgn define two Windows "regions" that are combined into a single region using CombineRgn. SelectClipRgn then establishes the combined, complex region as the clipping region. Note: since Delphi has no "wrappers" around Windows hRGN objects, you must take care to free these resources using DeleteObject.
PROCEDURE DisplayGraphics(CONST Title: STRING;
CONST Canvas: TCanvas;
CONST Width, Height: INTEGER;
CONST Clipping: BOOLEAN);
VAR
brush : WORD;
color : WORD;
GrayShade : WORD;
PenWidth : WORD;
Region1 : hRGN;
Region2 : hRGN;
s : STRING;
style : WORD;
xBase : WORD;
yBase : WORD;
xDelta : WORD;
yDelta : WORD;
x : WORD;
x1,x2 : WORD;
y : WORD;
BEGIN
Region1 := 0; // avoid compiler initialization warning
// Demonstrate Clipping by the union of two rectangular regions
IF Clipping
THEN BEGIN
// Create rectangular region of the upper-half of the vertical area,
// from 20% to 80% of the horizontal area.
Region1 := CreateRectRgn(Width DIV 5,
0,
4*Width DIV 5,
Height DIV 2);
// Create elliptical region of the lower-half of the vertical area
Region2 := CreateEllipticRgn(Width DIV 4,
Height DIV 2,
3*Width DIV 4,
Height);
TRY
// Add Region 2 to Region 1
CombineRgn(Region1, Region1, Region2, RGN_OR);
SelectClipRgn(Canvas.Handle, Region1);
// Show Outline of Clipping Area
Canvas.Brush.Color := clRed;
FrameRgn(Canvas.Handle, Region1, Canvas.Brush.Handle, 2,2);
FINALLY
DeleteObject(Region2)
END
// See final DeleteObject statement at end of routine END; |
A big red X formed by diagonals is drawn, as is a box around the edges:
// Big X Canvas.Pen.Color := clRed; Canvas.MoveTo ( 0, 0); Canvas.LineTo (Width-1, Height-1); Canvas.MoveTo ( 0, Height-1); Canvas.LineTo (Width-1, 0); // A Box -- the hard way Canvas.Pen.Color := clBlack; Canvas.MoveTo ( 0, 0); Canvas.LineTo ( 0, Height-1); Canvas.LineTo (Width-1, Height-1); Canvas.LineTo (Width-1, 0); Canvas.LineTo ( 0, 0); |
Use something like MulDiv(Width, percent, 100) to position to a particular horizontal location in a device-independent way. Likewise, use something like MulDiv(Height, percent, 100) to position to a particular vertical location in a device-independent way.
Set Font.Height (instead of Font.Size) for better device independence in the display of text. In fact, it's a good idea to never use Font.Size because of various bugs in printer drivers. Set the Height in a device-independent way and never use the font.size:
Printer.Canvas.Font.Height :=
MulDiv(GetDeviceCaps(Printer.Canvas.Handle, LOGPIXELSY), FontSize, 72);
// Line Styles yDelta := MulDiv(Height, 125, 10000); // 1.25% Canvas.Font.Height := MulDiv(Height, 2, 100);
Canvas.Font.Name := 'Arial';
Canvas.Font.Color := clBLack;
Canvas.Font.Style := [fsItalic, fsbold];
Canvas.Brush.Style := bsClear;
s := 'efg''s Computer Lab: Printer Demo #2';
Canvas.TextOut((Width - Canvas.TextWidth(s)) DIV 2,
MulDiv(Height, 1, 100),
s);
Canvas.Font.Name := 'Times New Roman'; Canvas.Font.Style := []; Canvas.Font.Height := MulDiv(Height, 15, 1000);
s := 'www.efg2.com/Lab';
Canvas.TextOut((Width - Canvas.TextWidth(s)) DIV 2,
MulDiv(Height, 3, 100),
s);
Canvas.Font.Height := MulDiv(Height, 2, 100); Canvas.Font.Color := clBlue; Canvas.Font.Height := MulDiv(Height, 1, 100);
s := 'Line Styles (by pen width)';
Canvas.TextOut(MulDiv(Width, 76, 100),
MulDiv(Height, 7, 100) - 3*Canvas.TextHeight(s) DIV 2,
s);
|
The various Pen.Styles are set by typecasting an integer, namely, TPenStyle(style MOD 7) returned the various Pen.Styles: psSolid (0), psDash (1), psDot (2), psDashDot (3), psDashDotDot (4), psClear (5), and psInsideFrame (6).
Canvas.Pen.Color := clBlack;
FOR PenWidth := 1 TO 4 DO
BEGIN
FOR style := 0 TO 6 DO
BEGIN
// Style is
// psSolid, psDash, psDot, psDashDot, psDashDotDot, psClear, psInsideFrame
Canvas.Pen.Style := TPenStyle(style MOD 7);
Canvas.Pen.Width := PenWidth;
y := MulDiv(Height, 7, 100) + (10 * (PenWidth-1) + style)*yDelta;
Canvas.MoveTo ( MulDiv(Width, 76, 100), y);
Canvas.LineTo ( MulDiv(Width, 87, 100), y)
END
END;
Canvas.Pen.Color := clRed;
FOR PenWidth := 1 TO 4 DO
BEGIN
FOR style := 0 TO 6 DO
BEGIN
// Style is
// psSolid, psDash, psDot, psDashDot, psDashDotDot, psClear, psInsideFrame
Canvas.Pen.Style := TPenStyle(style MOD 7);
Canvas.Pen.Width := PenWidth;
y := MulDiv(Height, 7, 100) + (10 * (PenWidth-1) + style)*yDelta;
Canvas.MoveTo ( MulDiv(Width, 88, 100), y);
Canvas.LineTo ( MulDiv(Width, 99, 100), y)
END
END;
|
Draw the color boxes with the various brush styles:
// Rectangles: Vary Colors and Brush Styles Canvas.Pen.Style := psInsideFrame; Canvas.Pen.Color := clBlack; Canvas.Pen.Width := 1; xDelta := MulDiv(Width, 35, 1000); // 3.5% yDelta := MulDiv(Height, 50, 1000); // 5% xBase := MulDiv(Width, 2, 100); // 2% yBase := MulDiv(Height,7, 100); // 7% s := 'Brush Styles';
Canvas.TextOut(XBase, YBase - 3*Canvas.TextHeight(s) DIV 2,
s);
FOR color := Low(Colors) TO High(Colors) DO
BEGIN
x := xBase + color*xDelta;
FOR brush := 0 TO 7 DO {See p. 764, VCL Ref, Delphi 1}
BEGIN
// Per E-mail from Thérèse Hanquet, always assign Brush Color // before assigning Brush Style. March 2001. Canvas.Brush.Color := colors[color]; Canvas.Brush.Style := TBrushStyle(brush); y := yBase + brush*yDelta; Canvas.Rectangle (x, y, x+xDelta, y+yDelta); END; END; |
The various Brush.Styles are set by typecasting an integer, namely TBrushStyle(brush) returned the various brush styles: bsSolid (0), bsClear (1), bsHorizontal (2), bsVertical (3), bsFDiagonal (4), bsBDiagonal (5), bsCross (6), bsDiagCross (7).
Here's what these rectangles with various colors and brush styles look like:

If the order of the assignments to Brush.Color and Brush.Style were reversed in the code above, the bsClear row above (the second row), would also be a solid color (like the first row). Perhaps this is a bug. [Thank you, Thérèse Hanquet, for bringing this to my attention. This has affected this example since D1! efg, March 2001.]
The two rows of shades-of-gray boxes are created with this code. The function call, RGB(i, i, i), for i = 0..255 gives the possible 256 shades of "gray" colors:
// Rectangles: Shades of Gray yBase := MulDiv(Height, 87, 100); yDelta := MulDiv(Height, 4, 100); s := 'Shades of Gray';
Canvas.TextOut(MulDiv(Width, 1, 63),
YBase - 3*Canvas.TextHeight(s) DIV 2,
s);
Canvas.Brush.Style := bsSolid;
FOR GrayShade := 0 TO 60 DO
BEGIN
x1 := MulDiv(Width, GrayShade+1, 63);
x2 := MulDiv(Width, GrayShade+2, 63);
Canvas.Brush.Color := RGB(4*GrayShade, 4*GrayShade, 4*GrayShade);
Canvas.Rectangle (x1, yBase, x2, yBase+yDelta);
END;
yBase := yBase + yDelta;
FOR GrayShade := 0 TO 60 DO
BEGIN
x1 := MulDiv(Width, GrayShade+1, 63);
x2 := MulDiv(Width, GrayShade+2, 63);
Canvas.Brush.Color := RGB(195+GrayShade, 195+GrayShade, 195+GrayShade);
Canvas.Rectangle (x1, yBase, x2, yBase+yDelta);
END;
|
Ellipses, chords and pies are drawn next:
// Ellipses
Canvas.Pen.Color := clWhite;
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clRed;
Canvas.Ellipse (MulDiv(Width, 55, 100),
MulDiv(Height, 55, 100),
MulDiv(Width, 85, 100),
MulDiv(Height, 85, 100));
Canvas.Brush.Color := clLime;
Canvas.Ellipse (MulDiv(Width, 57, 100),
MulDiv(Height, 57, 100),
MulDiv(Width, 83, 100),
MulDiv(Height, 83, 100));
Canvas.Brush.Color := clBlue;
Canvas.Ellipse (MulDiv(Width, 59, 100),
MulDiv(Height, 59, 100),
MulDiv(Width, 81, 100),
MulDiv(Height, 81, 100));
Canvas.Brush.Color := clYellow;
Canvas.Ellipse (MulDiv(Width, 61, 100),
MulDiv(Height, 61, 100),
MulDiv(Width, 79, 100),
MulDiv(Height, 79, 100));
Canvas.Brush.Color := clWHite;
Canvas.Ellipse (MulDiv(Width, 63, 100),
MulDiv(Height, 63, 100),
MulDiv(Width, 77, 100),
MulDiv(Height, 77, 100));
// Chords
Canvas.Pen.Color := clRed;
Canvas.Chord (MulDiv(Width, 63, 100),
MulDiv(Height, 63, 100),
MulDiv(Width, 77, 100),
MulDiv(Height, 77, 100),
MulDiv(Width, 72, 100),
MulDiv(Height, 63, 100),
MulDiv(Width, 72, 100),
MulDiv(Height, 77, 100));
Canvas.Pen.Color := clBlue;
Canvas.Chord (MulDiv(Width, 63, 100),
MulDiv(Height, 63, 100),
MulDiv(Width, 77, 100),
MulDiv(Height, 77, 100),
MulDiv(Width, 75, 100),
MulDiv(Height, 77, 100),
MulDiv(Width, 75, 100),
MulDiv(Height, 63, 100));
// Pie
Canvas.Pen.Color := clWhite;
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clLime;
Canvas.Pie (MulDiv(Width, 15, 100),
MulDiv(Height, 55, 100),
MulDiv(Width, 45, 100),
MulDiv(Height, 85, 100),
MulDiv(Width, 45, 100),
MulDiv(Height, 55, 100),
MulDiv(Width, 15, 100),
MulDiv(Height, 55, 100));
Canvas.Brush.Color := clCream;
Canvas.Pie (MulDiv(Width, 15, 100),
MulDiv(Height, 55, 100),
MulDiv(Width, 45, 100),
MulDiv(Height, 85, 100),
MulDiv(Width, 15, 100),
MulDiv(Height, 55, 100),
MulDiv(Width, 15, 100),
MulDiv(Height, 85, 100));
Canvas.Brush.Color := clAqua;
Canvas.Pie (MulDiv(Width, 15, 100),
MulDiv(Height, 55, 100),
MulDiv(Width, 45, 100),
MulDiv(Height, 85, 100),
MulDiv(Width, 45, 100),
MulDiv(Height, 85, 100),
MulDiv(Width, 45, 100),
MulDiv(Height, 55, 100));
Canvas.Brush.Color := clMoneyGreen;
Canvas.Pie (MulDiv(Width, 15, 100),
MulDiv(Height, 55, 100),
MulDiv(Width, 45, 100),
MulDiv(Height, 85, 100),
MulDiv(Width, 15, 100),
MulDiv(Height, 85, 100),
MulDiv(Width, 45, 100),
MulDiv(Height, 85, 100));
|
The last part of DisplayGraphics prints a footer with the date and printer name, and does some housekeeping to recycle the clipping region resource.
// Footer
Canvas.Font.Height := MulDiv(Height, 1, 100);
Canvas.Font.Color := clBlack;
Canvas.Brush.Style := bsClear;
s := Title + ', ' + IntToStr(Width) + ' pixels wide by ' +
IntToStr(Height) + ' pixels high, ' +
FormatDateTime('m/d/yy hh:nn', Now);
Canvas.TextOut ((Width - Canvas.TextWidth(s)) DIV 2,
Height - 3*Canvas.TextHeight(s) DIV 2, s);
IF Clipping
THEN BEGIN
SelectClipRgn(Canvas.Handle, 0); // remove clipping restriction
DeleteObject(Region1)
END
END {DisplayGraphics};
|
DisplayGraphics is called with various parameters for each kind of target canvas. Simply drawing these figures on a form's canvas is NOT a good idea since there is no persistence. If you minimize the form, or display a window over the objects drawn on the form, they original drawings will be lost.
TForm
procedure TFormPrinterDemo2.ButtonTFormClick(Sender: TObject);
begin
DisplayGraphics ('TForm',
Self.Canvas,
Self.ClientWidth,
Self.ClientHeight,
CheckBoxClipping.Checked)
end;
|
Only draw on a form's canvas inside an OnPaint handler. Click the check box to "hook up" the FormPaint:
procedure TFormPrinterDemo2.CheckBoxOnPaintClick(Sender: TObject); begin IF CheckBoxOnPaint.Checked THEN Self.OnPaint := Self.FormPaint ELSE Self.OnPaint := NIL; Self.Invalidate end; |
With the FormPaint defined, you can resize the form however you'd like. The form is redrawn by just calling the Invalidate method. (See below for changes to Image1 here.)
procedure TFormPrinterDemo2.FormResize(Sender: TObject); begin IF CheckBoxOnPaint.Checked THEN Self.Invalidate; Image1.Width := ClientWidth - 2*Image1.Left; Image1.Height := ClientHeight - Image1.Top - Image1.Left; IF ImageShow THEN ButtonTImageClick(Sender); end; |
TImage
Keeping persistent graphics is much easier by displaying them in a TImage. As shown next, it's better to draw on an in-memory bitmap the same size of the TImage to avoid flicker while drawing:
procedure TFormPrinterDemo2.ButtonTImageClick(Sender: TObject);
VAR
Bitmap: TBitmap;
begin
ImageShow := TRUE;
Bitmap := TBitmap.Create;
TRY
Bitmap.Width := Image1.Width;
Bitmap.Height := Image1.Height;
DisplayGraphics ('TBitmap/TImage',
Bitmap.Canvas,
Bitmap.Width,
Bitmap.Height,
CheckBoxClipping.Checked);
Image1.Picture.Graphic := Bitmap;
FINALLY
Bitmap.Free
END
end;
|
The FormResize (see above) dynamically changes the size of the TImage and forces an update. This approach automatically redraws the whole background (otherwise you'd be drawing on top of the previous drawing).
TPrinter
Drawing on the printer isn't much more complicated than a TForm or a TImage:
procedure TFormPrinterDemo2.ButtonPrintPortraitClick(Sender: TObject);
VAR
Title: STRING;
begin
Printer.Orientation := poPortrait;
Printer.BeginDoc;
IF Printer.PrinterIndex < 0
THEN Title := 'Printer (Portrait)'
ELSE Title := 'Printer (Portrait, ' +
Printer.Printers.Strings[Printer.PrinterIndex] + ')';
DisplayGraphics (Title,
Printer.Canvas, Printer.PageWidth, Printer.PageHeight,
CheckBoxClipping.Checked);
Printer.EndDoc;
ShowMessage('Printer Demo #2 Printed (Portrait)');
end;
|
A similar routine is used to display the drawings in the landscape orientation.
TBitmap
Drawing only on an in-memory bitmap and saving it to a disk file is fairly simple:
procedure TFormPrinterDemo2.ButtonWriteFileClick(Sender: TObject);
VAR
Bitmap: TBitmap;
begin
Bitmap := TBitmap.Create;
TRY
Bitmap.Width := 800;
Bitmap.Height := 600;
Bitmap.PixelFormat := pf24bit;
DisplayGraphics ('TBitmap',
Bitmap.Canvas,
Bitmap.Width,
Bitmap.Height,
CheckBoxClipping.Checked);
Bitmap.SaveToFile('ScreenDemo2.BMP');
FINALLY
Bitmap.Free
END
end; |
Miscellaneous
Steve Schafer's (Team B) UseNet Post: "GDI drivers are not required to support line styles other than psSolid and psClear if the line width is anything more than 1 pixel. Some drivers do support wide styled lines, but most do not. The only sure-fire fix that I know of is to draw all of the dashes and dots yourself."
Conclusions
Using device independence drawing allows one to draw on any
canvas, whether the canvas is for a TImage, a TPrinter, or even an
in-memory TBitmap.
Drawing a line with various values for Pen.Style is possible for Pen.Widths > 1, but most printers do not support this.
Getting the "same" printout on a variety of Windows printers is almost impossible! (This is a Windows issue, not a Delphi problem. See Steve Schafer's UseNet Post about why WYSIWIG is so difficult.)
Keywords
Windows non-dither colors, Pen.Style, Pen.Width, Brush.Style, Canvas.MoveTo,
Canvas.LineTo, Canvas.Rectangle, Canvas.Ellipse, Canvas.Chord, Canvas.Pie, Canvas.TextOut,
MulDiv, Device-independent graphics, CreateRectRgn, CreateEllipticRgn, CombineRgn,
SelectClipRgn, DeleteObject
Download
Delphi 3/4/5 Source and EXE (127 KB): PrinterDemo2.ZIP
Delphi Conversion Notes
No changes are needed to the Delphi 3 code to compile in Delphi 4 or 5.
Size of Delphi 3 EXE: 229 KB
Size of Delphi 4 EXE: 317 KB
Size of Delphi 5 EXE: 330 KB
If you use short filenames (PDemo2.dpr, SPDemo2.pas, etc.) and change the // comments to
(* *) or { } comments, then the project will compile and run correctly in all versions of
Delphi (D1 - D5). Of course, it only takes a few minutes to change it over
anyway. [Thanks to Jeff Wormsley for this comment.]
Updated 18 Feb 2002
since 1 Nov 1998