How to
Print in Windows
Printing in
Windows is achieved with a device control associated
with the
printing device described in the WIN.INI.
Before you
start
printing in your program, you will need to create a device
control
associated with your printer.
To do this,
you will call the function GetProfileString as
follows.
GetMem(Info, 80);
GetProfileString('windows','device',',,',
Info, 80);
Where Info
is defined to be of type PChar. The
resulting ASCIIZ
string will
be placed in Info. Unfortunately,
CreateDC requires
three
parameters, one for the driver, the printer type, and the
port. If GetProfileString is successful, Info
contains all the
information
required by CreateDC but contained in one string
separated
by semi-colons. These items must be
parsed out of the
string Info
before CreateDC can be called. The
following
function is
helpful in this task.
function GetItem(var S:PChar): PChar;
var
P: PChar;
I: Integer;
begin
i:=0;
while (S[I]<>',') and
(S[I]<>#0) do
inc(I);
S[I]:=#0;
GetItem:=S;
if S[0]<>#0 then S:= ;
end;
The
function GetItem does not copy the strings but rather turns
the larger
string into a structure containing smaller component
strings. Now separating the driver, printer type, and
port
information
is fairly straight forward. Assuming
that the PChar
variables
Driver, PrinterType, Port and TInfo have been
predeclared,
you would use the following assignments.
TInfo:=Info;
PrinterType:=GetItem(Info);
Driver:=GetItem(Info);
Port:=GetItem(Info);
The
variable TInfo is necessary for a subsequent FreeMem call to
free up the
memory originally allocated for Info.
At last,
you may now call CreateDC with the appropriate
parameters
(assuming DC was previously declared to be of type
HDC).
DC:=CreateDC(Driver, PrinterType, Port,
Nil);
If for some
reason CreateDC is not successful, it will return a
value of
zero.
Now you are
ready to start using the Escape function.
Escape is
a very
special function as it allows access to device
capabilities
that are not supported by the Windows GDI.
The
second
parameter to Escape represents the particular escape
function to
be used. Two fundamental escape
functions always
used when
printing in Windows are STARTDOC and ENDDOC. These
functions
do any setup and shutdown required by the printer.
Another
important escape function is NEWFRAME. NEWFRAME
forces a
buffer
flush and causes either a form feed operation or a page
eject
operation depending upon the type of printer used.
To print
some text, the following code illustrates the process
involved.
procedure Print;
var
TInfo, Info: PChar;
Driver, PrinterType, Port: PChar;
DC: HDC;
begin
GetMem(Info, 80);
GetProfileString('windows', 'device',
',,', Info, 80);
TInfo:=Info;
Driver:=GetItem(Info);
PrinterType:=GetItem(Info);
Port:=GetItem(Info);
DC:=CreateDC(PrinterType, Driver,
Port, Nil);
FreeMem(TInfo, 80);
if Escape(DC, _STARTDOC, 8,
StrNew('Doc Name'),
nil)<0then
begin
MessageBox(Window, 'Printing can
not be started', nil,
mb_Ok or mb_IconStop);
exit;
end;
if Escape(DC, NEWFRAME, 0, nil,
nil)<0 then
begin
MessageBox(Window, 'Paper can not
be advanced', nil,
mb_Ok or mb_IconStop);
exit;
end;
TextOut(DC, 10, 10, 'This text is
being printed', 26);
if Escape(DC, NEWFRAME, 0, nil,
nil)<0 then
begin
MessageBox(Window, 'Paper can not
be advanced', nil,
mb_Ok or mb_IconStop);
exit;
end;
if Escape(DC, ENDDOC, 0, nil,
nil)<0 then
begin
MessageBox(Window, 'Error
Printing', nil,
mb_Ok or mb_IconStop);
exit;
end;
end;
Notice that
if Escape fails that it returns a value less than
zero. The actual errors are governed by the
spooler constants
sp_Error,
sp_OutOfDisk, sp_OutOfMemory, and sp_UserAbort. It is
recommended
that a NEWFRAME call precede and follow the intended
output to
ensure that the printer is working with a fresh piece
of paper and flushes its buffer when printing ends.
The coordinates used in TextOut are arbitrary. Exactly lines can
be calculated from the values returned by GetTextMetrics as
you
would for normal output to the screen. See Chapter 4 of the
Windows Reference Guide of TPW for a complete description of
the
TTextMetric type.
The tmHeight, tmAveCharWidth, and
tmMaxCharWidth fields will prove to be the most useful when
determining line lengths and the number of characters per
page.
Use GetDeviceCaps to determine the maximum horizontal and
vertical resolutions of your printer when calculating these
values.
Since device controls are used, printing graphics is a
simple
extension of what is done to display graphics on
screen. Here's
a procedure that copies a bitmap to the printer using the
techniques that have been discussed so far.
procedure HardCopy(HWindow: HWnd; BitMap: HBitMap);
var
DC, ScreenDC,
MemDC: HDC;
BM: TBitMap;
OldBitMap: HBitMap;
Info, TInfo: PChar;
Driver,
PrinterType, Port: PChar;
begin
GetMem(Info, 80);
GetProfileString('windows', 'device', ',,', Info, 80);
TInfo:=Info;
Driver:=GetInfo(Info);
PrinterType:=GetItem(Info);
Port:=GetItem(Info);
DC:=CreateDC(PrinterType, Driver, Port, Nil);
Escape(DC,
STARTDOC, 8, StrNew('HardCopy'), nil);
Escape(DC,
NEWFRAME, 0, nil, nil);
ScreenDC:=GetDC(HWindow);
MemDC:=CreateCompatibleDC(DC);
ReleaseDC(HWindow,
ScreenDC);
OldBitMap:=SelectObject(MemDC, BitMap);
GetObject(BitMap,
sizeof(BM), @ BM);
BitBlt(DC, 0,0,
BM.bmWidth, BM.bmHeight,
MemDC, 0, 0,
SRCCOPY);
Escape(DC,
NEWFRAME, 0, nil, nil);
Escape(DC, ENDDOC,
0, nil, nil);
SelectObject(MemDC,
OldBitMap);
DeleteDC(MemDC);
DeleteDC(DC);
end;
For clarity, this routine does not perform error checking in
the
manner of the previous example so use it with appropriate
caution.
As with BitBlt, you may use any of the other GDI functions
with a
DC associated with the printer. The only difficulty that you may
run into in this regard is the capabilities of the printer
itself. To determine if a printer can handle bitmaps, use
the
GetDeviceCaps function as follows.
Value:=GetDeviceCaps(DC, RASTERCAPS);
Value:=Value and rc_BitBlt;
Assuming, DC to be associate with a printer device and Value
to
be declared as an integer, if the resulting contents of
Value is
not zero then the device has the ability to display
bitmaps. See
Device Capabilities under Chapter 1 of TPW's Windows
Reference
Guide for more on what GetDeviceCaps can do.
Frequently, programs give user's the ability to change the
setting of the printer.
However, printers often have greatly
different capabilities and it is impossible to determine
what
printers will be capable of in the future. Therefore, no one
dialog is appropriate to change printer setting. Because of
these facts, Windows requires the manufacturers of printers
to
supply drivers and incorporate into these drivers a dialog
used
to alter printer settings.
There are two functions supported by the Windows API to call
up
this dialog DeviceMode and ExtDeviceMode. These functions do not
exist in the API but rather are loaded from the driver
directly.
Before you can call on these functions you must obtain a
driver
handle. Here's how
this would be done knowing what we know so
far.
GetMem(FullDriver,13);
GetProfileString('windows', 'device', ',,', Info, 80);
TInfo:=Info;
PrinterType:=GetItem(Info);
Driver:=GetItem(Info);
Port:=GetItem(Info);
StrLCat(StrCopy(FullDriver,Driver),'.DRV',12);
DriverHandle:=LoadLibrary(FullDriver);
PrinterType, Driver, Port, Info, and TInfo you have seen
above.
FullDriver is declared to be PChar and DriverHandle is a
THandle.
FullDriver is necessary to provide the file extension to
Driver
as the function LoadLibrary requires a complete file
name. All
printer drivers have the file extension .DRV.
ExtDeviceMode is a more advanced form of DeviceMode. It not only
provides the dialog to allow user's change settings but also
gives the means for the program to moniter and change the
device
settings. DeviceMode
only provides the user dialog capability.
However, ExtDeviceMode was introduced in version 3.0 of
Windows
and not all printer drivers provide a ExtDeviceMode
function.
But, all printer driver provide a DeviceMode function. So if
ExtDeviceMode is not present, DeviceMode will be. The following
code is appropriate for displaying the printer's dialog.
P:=GetProcAddress(DriverHandle, 'ExtDeviceMode');
if P<>nil
then
begin
ExtDeviceMode:=TExtDeviceMode(P);
Size:=ExtDeviceMode(Window, DriverHandle,
OutModeRec^, PrinterType, Port,
InModeRec^, nil, 0);
GetMem(OutModeRec, Size);
GetMem(InModeRec, Size);
ExtDeviceMode(Window, DriverHandle,
OutModeRec^, PrinterType, Port,
InModeRec^, nil, dm_Prompt);
FreeMem(OutModeRec, Size);
FreeMem(InModeRec, Size);
end
else begin
P:=GetProcAddress(DriverHandle, 'DeviceMode');
DeviceMode:=TDeviceMode(P);
DeviceMode(Window, DriverHandle, PrinterType, Port);
end;
FreeLibrary(DriverHandle);
P is declared to be of type TFarProc; ExtDeviceMode is of
type
TExtDeviceMode which is supplied by the WinTypes unit (the
same
applies for DeviceMode and TDeviceMode). Both OutModeRec and
InModeRec are declared to be of type PDevMode. These two
pointers
control the input and output to ExtDeviceMode where
required.
Notice that if the results of GetProcAddress(DriverHandle,
'ExtDeviceHandle'); are nil then ExtDeviceHandle cannot be
provided by the printer.
In this case, the code proceeds to the
simple DeviceMode call.
Before ExtDeviceMode can be called to prompt the user, it is
a
good idea to call ExtDeviceMode with its mode parameter (the
8th
parameter) set to zero.
The result will be the amount of space
required for it input and output, TDevMode parameters. This is
important as these records may not be the same size as the
TDevMode type. After
memory is allocated for these records, the
data fields of the input record may then be initialized (see
Chapter 4 of TPW's Windows Reference guide for more on
TDevMode).
Now ExtDeviceMode is called with the mode paramter set to
dm_Prompt which displays and activates the printer's dialog.
This constant can be combined with the other constants
dm_Copy,
dm_Modify, and dm_Update with Turbo's bitwise OR operator.
(See
Chapter 1 of TPW's Windows Reference Guide for information
on
these dm_ Device mode selections).
Another important feature for printing is the Abort
Dialog. An
Abort Dialog is a dialog displayed when printing is taking
place
and gives the user of the program an opprotunity to halt
printing
before it is finished.
The first thing to do, is create a dialog suitable for the
task.
Something like this .DLG file would serve (in a true DLG
file all
data for statement would appear on a single line). Though you
may prefer to use a resource toolkit of some sort to build
your
dialog into a resource used by your program.
ABORTBOX DIALOG DISCARDABLE LOADONCALL PURE MOVEABLE 10, 37,
187,
67
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | 0x80L
CAPTION "Printing in Progress"
BEGIN
CONTROL
"Escape" 100, "BUTTON", WS_CHILD | WS_VISIBLE |
WS_TABSTOP, 73,
37, 41, 16
CONTROL "Press
Escape to Stop Printing" 102, "STATIC", WS_CHILD
| WS_VISIBLE,
44, 12, 99, 11
END
Next, using the Object Windows Library, you would define an
object type to govern your Abort Dialog. Its actuall definition
needs to be no more than something like the following.
var
Abort: Boolean;
AbortWindow:
HWindow;
type
PAbortDialog =
^TAbortDialog;
TAbortDialog =
object(TDlgWindow)
procedure
SetUpWindow; virtual;
procedure
WMCommand(var Msg: TMessage);
virtual
wm_First + wm_Command;
end;
procedure TAbortDialog.SetUpWindow;
begin
Abort:=false;
SetFocus(HWindow);
AbortWindow:=HWindow;
end;
procedure TAbortDialog.WMCommand(var Msg: TMessage);
{Will be entered whenever a button, return key or escape key
is
pressed.}
begin
Abort:=true;
end;
As any button press, return key press, or escape key press
will
abort printing only a WMCommand method is needed. InitDialog is
necessary to set the focus to this dialog and to set the
field
Abort.
Abort is used by the dialog to determine if printing has
been
aborted. To prevent
interrupting printing while the dialog is
displayed, a Call Back function is required to be installed.
Most such functions call PeekMessage which checks the
application
queue for a message.
If there are no messages, it returns and
gives control to Windows.
Here's a simple example of such a
function.
function AbortCallBack(DC: HDC; Code: Integer): Bool;
export;
var
Msg: TMsg;
begin
While (not Abort)
and PeekMessage(Msg, 0, 0, 0, pm_Remove) do
if not
IsDialogMessage(AbortWindow, Msg) then
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
if Abort then
AbortCallBack:=false else AbortCallBack:=true;
end;
PeekMessage yields control to Windows when a message is not
available. If a
message is received and it doesn't belong to the
dialog, it is sent along using the customary
TranslateMessage and
DispatchMessage functions.
After it checks to see if the user
has aborted and if so, AbortCallBack returns false. Otherwise,
it simply returns true which indicates printing may
continue.
Now you have all that you need to install you Abort Dialog.
Before, your call to Escape for the STARTDOC, the following code
should be executed (assuming that you have previously
declared a
variable AbortDialog of type PAbortDialog and a variable
called
AbortCallBackProc of type TFarProc to hold the address of
the
callback function).
AbortDialog:=New(PAbortDialog,
Init(Application^.MainWindow,
'ABORTBOX'));
AbortDialog:=PAbortDialog(Application^.
MakeWindow(AbortDialog));
AbortCallBackProc:=MakeProcInstance(@ AbortCallBack,
HInstance);
Escape(DC,
SETABORTPROC, 0, AbortCallBackProc, nil);
Now you would call Escape with the appropriate STARTDOC,
NEWFRAME, and ENDDOC parameters as illustrated before.
However,
it is vital to check for errors after a NEWFRAME escape
call.
For printing actually occurs during the NEWFRAME. If an error or
user abort occurs Escape will return a value less than
zero. If
this happens do not allow the ENDDOC escape call take place;
for
the GDI automatically terminates the operation before returning
the error value.
Also, if an error other than a user abort
occurs, use AbortDialog^.CloseWindow to remove the dialog
from
view. If the user
cancles the print operation, Windows removes
the dialog
automatically.