Drucken mit der Win-API


Oftmals wird gefragt, wie man denn etwas ausdrucken könnte. Es scheint so, als ob damit eine gewisse Schwierigkeit verbunden wäre. Dem ist aber in Wirklichkeit gar nicht so. Denn ist man in der Lage Text und Grafik auf den Canvas eine Fensters auszugeben, ist man auch in der Lage zu drucken. Denn auch der Drucker besitzt einen Gerätekontext (DC) auf dem man ganz einfach Text ausgeben und Grafiken zeichnen kann. Im Prinzip braucht man also nur den Gerätekontext des Druckers zu ermitteln und kann dann wie gewohnt seine Ausgabe machen. Dabei muss man eigentlich nur eins noch zusätzlich berücksichtigen: Eine Papierseite ist irgendwann mal zu ende. Aber auch das stellt eigentlich kein größeres Problem dar.

Will man also drucken, muss man folgende Aufgaben bewältigen:

  1. Ermitteln der Drucker und Auswahl des Druckers
  2. Ermitteln des Gerätekontextes des Druckers
  3. Aufbereiten des auszugebenden Textes, wenn man dann Text ausdrucken will
  4. Ermitteln wann eine Seite zu ende ist und eine neue Seite anfangen

Wir wollen diese Liste Schritt für Schritt durchgehen und am Ende sollte man in der Lage sein einen mehrseitigen Text und einfache Grafikelemente auf dem Drucker auszugeben. Arbeitet man mit Delphi, dann steht einem die VCL-Unit Printers.pas zur Verfügung, die einem etwas Arbeit abnimmt. Ich habe an dieser Stelle bewußt auf sie verzichtet, weil ich zeigen will, was eigentlich im Hintergrund wirklich passiert. Doch zu vor noch ein paar einführende Worte zur grafischen Geräteschnittstelle (GDI):

Struktur der GDI

Für die Zeichenroutinen unter Windows sind in erster Linie die Funktionen aus der GDI32.dll zuständig. (Unter Windows 98 noch die 16-Bit Bibliothek GDI.exe.) Die GDI32 übernimmt die übergeordnete Logik, was das Zeichen angeht und überlassen den Treiber dann die eigentliche Ausführung der Operationen.

Zwischen GDI und Treibern findet dabei eine rege Kommunikation statt. Windows kann über den Treiber ermitteln, welche Operation das Gerät selber ausführen kann und bei welchen Fällen es Unterstützung in Form einer übergeordneten Logik braucht. Unterstützt eine Grafikkarte zum Beispiel nicht das Zeichnen einer Linie von Punkt A nach Punkt B, berechnet die GDI jeden einzelnen Pixel, wird hingegen diese Funktion unterstützt, bekommt die Grafikkarte hingegen nur die benötigten Befehlscodes und Koordinaten.

Hauptziel der GDI ist es möglichst geräteunabhängig zu sein. Das heißt, wenn die nötigen Treiber vorliegen, dann kann mit ein und der selben GDI-Funktion die Ausgabe sowohl auf den Bildschirm als auch auf einen Drucker oder Plotter erfolgen ohne, dass man etwas am Code ändern müsste - außer das Ziel der Zeichenoperation natürlich. Die Ergebnisse sollten dann ebenso (fast) identisch aussehen. Und deswegen habe ich oben gesagt, wenn man in der Lage ist, Text und Grafik auf dem Canvas eines Fensters auszugeben, dann kann man auch drucken.

Mit diesem Vorwissen können wir uns nun unserem eigentlichen Problem widmen, dem Drucken.

Ermitteln der verfügbaren Drucker und des Standarddruckers

Damit wir überhaupt was drucken können, müssen wir erstmal raus finden, was für Drucker uns eigentlich zur Verfügung stehen. Dazu stellt uns die Win-API die Funktion EnumPrinters bereit:

The EnumPrinters function enumerates available printers, print servers, domains, or print providers.

Da wir uns hier erstmal nur für die lokal verfügbaren Drucker interessieren setzen wir nur den Flag PRINTER_ENUM_LOCAL. Die restlichen Parameter betreffen dann noch den Informationslevel, die Struktur, die die Daten aufnimmt und deren Größe. Im Demo Programm erfolgt der Aufruf von EnumPrinters zweimal: Einmal um zu bestimmen wie groß die zu Datenmenge ist und dann ein zweites mal mit dem dafür erforderlichen reservierten Speicher.

  EnumPrinters(PRINTER_ENUM_LOCAL, nil, 4, nil, 0, dwNeeded, dwReturn);
  GetMem(pinfo4, dwNeeded);
  try
    if EnumPrinters(PRINTER_ENUM_LOCAL, nil, 4, pinfo4, dwNeeded, dwNeeded, dwReturn) then
    begin
      pWork := pinfo4;

Der Wrapper für EnumPrinters, GetPrinters, ist so implementiert, dass die Struktur in einer Schleife für jeden gefundenen Drucker durchlaufen wird und im Schleifenrumpf jedesmal eine Callback-Funktion mit dem Druckernamen aufgerufen wird:

for i := 0 to dwReturn - 1 do
begin
  s := string(pWork.pPrinterName);
  if not Callback(s) then
    break;
  Inc(pWork);
end;

Anzumerken sei noch folgendes: Unter Windows 98/ME gibt es noch den Flag PRINTER_ENUM_DEFAULT, welcher bewirkt, dass der Standarddrucker ermittelt wird. Unter Windows NT und höher gibt es den Flag nicht mehr. Unter Windows NT und höher muss man eine zusätzliche API-Funktion GetDefaultPrinter bemühen, um den Standarddrucker zu ermitteln. Zusätzlich gibt es zu beachten, dass unter Windows 98/ME der Informationslevel vier nicht zur Verfügung steht. Wie man oben an meinem Code sehen kann, benutze ich die Informationsebene fünf, die es unter Windows 98/ME nicht gibt. Desweiteren wird die Funktion GetDefaultPrinter, die es wiederum nur unter Windows NT und höher gibt, statisch gelinkt. Deswegen ist mein Demo auch unter Windows 98/ME nicht lauffähig.

Wie man den Standarddrucker ermittelt hab eich im vorherigen Absatz ja schon mal kurz angesprochen. Viel mehr gibt es dazu auch nicht zu sagen, außer dass man die API-Funktion unter Delphi eben selber importieren muss:

function GetDefaultPrinterA(prnName: LPTSTR; var bufSize: DWORD): BOOL; stdcall;
  external 'winspool.drv' name'GetDefaultPrinterA';

Aufruf:

len := sizeof(Buffer) + 1;
if GetDefaultPrinterA(Buffer, len) then
begin
  SetDlgItemText(hWnd, IDC_CB_PRINTERS, Buffer);
end;

Der Gerätekontext des Druckers

Als nächstes brauchen wir Zugriff auf die Zeichenfläche (Canvas) des Druckers. Die Zeichenfläche wird durch einen so genannten Gerätekontext (DeviceContext) eindeutig identifiziert. Dazu erzeugen wir uns mit CreateDC einen Gerätekontext für unseren Drucker:

HDC CreateDC(
  LPCTSTR lpszDriver,        // driver name
  LPCTSTR lpszDevice,        // device name
  LPCTSTR lpszOutput,        // not used; should be NULL
  CONST DEVMODE* lpInitData  // optional printer data
);

Bis auf den zweiten Parameter können alle anderen Parameter nil sein. Im zweiten Parameter geben wir den Druckernamen an, wie er in der Druckerverwaltung steht bzw. wie wir ihn von EnumPrinters zurückbekommen. Wichtig ist, dass wir, wenn wir den Gerätekontext nicht mehr brauchen, ihn auch wieder mit der Funktion DeleteDC löschen:

BOOL DeleteDC(
  HDC hdc   // handle to DC
);

Drucken

So bald wir den Gerätekontext des Druckers haben, können wir mit den bekannten GDI-Funktionen Grafiken und Text auf der Zeichenfläche ausgeben. Da es sich, um die Zeichenfläche des Druckers handelt, wird das ganze eben ausgedruckt. Allerdings sind doch noch ein paar zusätzliche API-Funktionsaufrufe nötig, weil es sich eben um einen Drucker handelt und dieser eben vom Druckerspooler gesteuert wird. Das heißt, wir müssen einen so genannten "Print Job" starten, dem Druckerspooler sagen, wann er eine neue Seite anfangen soll und wir müssen den Print Job "beenden". Und erst, wenn der Druckerspooler das vollständige Dokument bekommen hat und der Print Job beendet wurde, wird das Dokument an den Drucker geschickt und letztendlich ausgedruckt.

Wer schon mal einen Blick in den Quellcode des Demo-Programms geworfen hat, wird auch schon die API-Funktionen gefunden haben, die wir noch benötigen


API-FunktionBeschreibung
StartDocBeginnt einen Print Job
EndDocSchliesst einen Print Job ab
EndPage</td>Schliesst eine Seite ab und beginnt eine neue

Somit hätten wir jetzt das nötige Handwerkszeug zusammen, um zu drucken. Bei unserem konkreten Beispiel bleibt nur noch ein Problem zu lösen: Umbrechen des Fließtextes in Zeilen, die nicht breiter sind als unser Papier. Das hat aber weniger mit dem eigentlichen Drucken zu tun. Helfen tut uns dabei die Funktion GetTextExtentExPoint. Sie ermittelt uns wie viele Zeichen in ein definiertes Rechteck passen. damit der Text aber nicht mitten in einem Wort umgebrochen wird, gehen wir so lange zurück bis wir das erste Leerzeichen von hinten gefunden und schneiden dort unseren Text ab.

GetTextExtentExPoint(dc, PChar(s), length(s), (PageW * 10) - (BORDERLEFT * 10) -
  (BORDERRIGHT * 10), @cntChars, nil, size);
while (s[cntChars] <> ' ') do
  Dec(cntChars);

Und das Ergebnis unserer Bemühungen sieht dann so aus:

Tipps

Noch zwei Tipps von mir: Da jeder Drucker eine andere Auflösung besitzt, ist es hilfreich den MapMode mit der API-Funktion SetMapMode auf MM_LOMETRIC umzustellen. Denn dann entspricht jede logische Einheit einen zehntel Millimeter und wir können unseren Text und unsere Grafiken millimetergenau ausgeben.

Desweiteren hat es sich als praktisch erwiesen einen PDF-Drucktreiber zu installieren, um einfach Papier und Tinte zu sparen. Solch ein Drucktreiber erscheint im System einfach als weiterer Drucker und kann auch so genutzt werden. Nur eben mit dem Unterschied, dass die Ausgabe in eine PDF-Datei erfolgt anstatt auf dem Drucker.

Das Demo-Programm "Print"

Zu diesem kleinen Tutorial gibt es natürlich auch ein kleines Demo, was das hier besprochene entsprechend demonstriert. Zu bedienen ist es ganz einfach. In der Combobox werden alle verfügbaren lokalen Drucker aufgelistet, wobei der Standarddrucker immer vorausgewählt ist. Klickt man dann auf "Drucken", wird ein eineinhalb seitiger Platzhaltertext ("Lore Ipsum") ausgedruckt. Das Programm ist im Stile der Win32-API Tutorials für Delphi ohne VCL geschrieben. Der zusätzliche Code sollte aber nicht weiter stören, da der eigentliche Code sich auf zwei Routinen beschränkt: GetPrinter und PrintDoc.

MSDN Links:

Downloads


Drucken.zip Wednesday, 29-Dec-2010 23:45:23 CET 34K
2010-12-29T23:44:44 +0100, mail+homepage[at]michael-puff.de