Session D-PRN / D-FXTO

Druckersteuerung unter FoxPro

Burkhard Stiller
ECHTZEIT DV-Systeme und Engineering


Inhalt

Know How: Druckertreiber unter FoxPro für Windows

FoxPro für Windows besitzt einen "recht kompletten" Reportgenerator. Einige Funktionen werden jedoch vom professionellen Entwickler schmerzlich vermißt. Dieser Vortrag verschafft Ihnen einen Überblick über alle elementaren Abläufe der Druckjob-Abwicklung unter Windows. Es werden Verfahren unter FoxPro vorgestellt, mit deren Hilfe Sie zusätzliche Kontrolle über die Druckeransteuerung gewinnen können.


Thema dieses Vortrags

Ziel dieses Vortrags ist es, Ihnen detaillierten Einblick in die Funktionsweise von Windows-Druckertreibern zu geben und einen Weg aufzuzeigen, wie die von FoxPro ungenutzten Fähigkeiten von Ihnen programmtechnisch zu realisieren sind. Im Einzelnen werden Sie Antworten auf die folgenden Fragen finden:

All diese Fragen werden im Laufe dieses Vortrags behandelt werden - versprochen! Doch bis dahin müssen etliche technische Grundlagen und Funktionen der Programmier-Schnittstelle von Windows, kurz auch "WIN-API" genannt, erläutert werden. Sehen Sie sich dazu die Anlage Programmiertechniken an!

An diesen Vortrag möchte ich eine kurze Diskussion anschließen lassen, in der Möglichkeiten eines plattformübergreifenden Lösungswegs angesprochen werden, denn spätestens seit dem Erscheinen von WIN95 und dem Erstarken von WinNT, scheint mir der hier vorgestellte betriebssystemnahe Weg zu umständlich und unsicher zu werden.


Grundlagen

Ein Ausdruck unter FoxPro soll gestartet werden. Zuerst muß FP dazu ermitteln, auf welchem der verschiedenen im System installierten Drucker die Ausgabe erfolgen soll. Sie können Ihre Wahl z.B. mittels REPORT FORM....PROMPT an FoxPro übergeben. Sie haben auf der anderen Seite aber auch die Möglichkeit, FoxPro die Anwahl zu überlassen, indem Sie den im Report "hinterlegten" Druckertreiber als Ausgabegerät voreingestellt übernehmen (REPORT FORM... ohne Anwender-PROMPT). In beiden Fällen wird FoxPro, wie weiter unten beschrieben, schrittweise in den Besitz folgender Informationen gelangen und diese auswerten:

1.) Der Device-Name unter dem der Druckertreiber im System bekannt ist wird registriert (z.B. "Canon Bubble-Jet BJ-300"). Gleichzeitig ist damit auch der DOS-Name des Druckertreibers und dessen aktuelle "Verbindung" mit einem Ausgabeport bekannt.

2.) jetzt wird der Druckertreiber "aktiviert", d.h. das Treiber-Modul wird geladen, indem FPW einen DeviceContext (DC) für diesen Drucker vom GDI erstellen läßt. Nach dem Laden des Treibers hat FoxPro über das erhaltene Handle auf diesen DC Zugriff auf alle Informationen (und Funktionen) des Treibers.

3.) FoxPro "fragt" jetzt alle Fähigkeiten des Druckertreibers ab, um daraus die korrekten Größen von s.g. DEVMODE-Strukturen zu ermitteln, die zur Ansteuerung notwendig sind. Wenn der Default-Druckertreiber des Reports benutzt wird initialisiert FPW den Treiber gleich mit der im Report hinterlegten DEVMODE-Struktur.

4.) Anhand der Anwendervorgabe oder der reportinternen Werte wird nun via DeviceContext und entsprechend initialisierten DEVMODE-Strukturen der Druckertreiber für das gewünschte Druckresultat eingestellt. Dies geschieht indem (ab WIN 3.1) die Funktion StartDoc() an das WIN-GDI abgesetzt wird.

5.) Jetzt kann die eigentliche Ausgabe beginnen. Dazu werden mittels spezieller ESCAPE-Funktionen (bitte nicht mit den DOS ESC-Sequenzen für Drucker verwechseln) die Daten an den Druckertreiber abgeschickt. Die Daten werden jedoch meistens erst einmal auf der Festplatte (dort im Temp-Verzeichnis) in einem s.g. Meta-Dateiformat zwischengespeichert.

6.) Fast immer ist bei der Abwicklung von Druckjobs auch der Drucker-Spooler von Windows aktiviert. Dieser sorgt jetzt für eine störungsfreie Ausgabe der Metadaten im Hintergrund, während Sie bereits im Vordergrund mit anderen Aufgaben beschäftigt sein können.

Mit dieser Auflistung habe ich den einen oder anderen unter Ihnen sicherlich mit noch unbekannten Begriffen überfallen. Die Erläuterungen werden gleich folgen! Dieser "Kurzabriß" soll Ihnen nur als roter Faden für meine weiteren Ausführungen dienen.


Device-Informationen der Win.ini

 

Der Abschnitt [windows] der WIN.INI enthält einen device= Eintrag z.B. in folgender Form:

dies ist immer der aktuelle Windows (Default-) Drucker. Sie können den Default-Druckereintrag aus der WIN.INI-Datei bequem mit einer API-Funktionen auslesen.

Die folgenden Zeilen zeigen die "API"-Syntax der GetProfileString()-Funktion:

int GetProfileString(lpszSection, lpszEntry, lpszDefault, lpszReturnBuffer, cbReturnBuffer)

1.) LPCSTR lpszSection; /* Adresse des Sektionsnamens (hier "windows") */

2.) LPCSTR lpszEntry; /* Adresse des Eintrags (hier: "device") */

3.) LPCSTR lpszDefault, /* Adresse eines Default Rückgabe-Strings (wenn Eintrag ganz fehlt) */

4.) LPSTR lpszReturnBuffer; /* Adresse des Ergebnis-Strings (per Referenz) */

5.) int cbReturnBuffer; /* Länge in Bytes des übergebenen Ergebnisstings */

 

Die Funktion liefert als Integerwert die Anzahl der von ihr in den Ergebnis-String geschriebenen Zeichen zurück. Die GetProfileString()-Funktion ist die verkürzte Variante der GetPrivateProfileString()-Funktion. Die letztere kann alle (Text-) Dateien bearbeiten, GetProfileString() bearbeitet speziell nur die WIN.INI-Datei. Sie sehen anhand des kurzen Listings, wie die Funktion einzusetzen ist:

Damit wäre Punkt 1.) unseres "roten Fadens" abgehakt und die Antwort auf die eingangs gestellte Frage: "Wie erfahre ich wie der Default-Drucker heißt?" gegeben.

Hat der Anwender aber noch vor Druckbeginn die Möglichkeit der Treiberauswahl, müssen Sie in Erfahrung bringen können, welche Druckertreiber im System insgesamt installiert wurden und somit im Moment zur Verfügung stehen. Dazu existiert in der WIN.INI der Abschnitt [devices]. Die dort aufgeführten Einträge haben z.B. folgende Form:

Zum Einlesen dieser Einträge in FoxPro benutzen Sie wieder die im vorangegangenen Abschnitt bereits gebrauchte API-Funktion GetProfileString(). Diesmal machen wir uns eine Sondervereinbarung für die Funktion Zunutze. Wird als Eintragsname nämlich der NULL-Wert übergeben, gibt die Funktion alle Einträge (links vom Gleichheitszeichen) zu dem in Parameter #1 übergebenen Section-Namen zurück. Sie müssen über den "dicken Daumen" abschätzen, wieviel Bytes wohl zurückgegeben werden könnten. Ich kalkuliere da immer lieber hoch. Angenommen der Anwender hat 20! Druckertreiber installiert - Wahnsinn, aber denkbar! Ein Device-Name kann maximal 32 Zeichen lang sein inklusive des abschließenden CHR(0) je Device-Eintrag. Das Ende aller Einträge im Rückgabe-String wird durch zwei CHR(0) gekennzeichnet - 641 Bytes sind daher maximal notwendig. Nachfolgend sehen Sie das kurze Listing einer Routine, die Ihnen alle Druckertreiber aus der WIN.INI-Datei in ein Array einließt:

Damit wäre die Frage "Wo steht, welche Druckertreiber auf dem Zielsystem verfügbar sind?" beantwortet.

Was aber tun, wenn der Anwender einen Druckertreiber auf dem Zielsystem Ihrer Anwendung noch gar nicht installiert hat, Ihre mühsam erstellten Reports aber geradezu nach genau diesem Druckertreiber "schreien"? Richtig, Sie müßten in der Lage sein, den Anwender zur Installation des benötigten Druckertreibers zu bewegen. Stellen Sie sich vor, Ihre Anwendung setzt das Vorhandensein eines Canon Bubble Jet 300 mit zwei Schächten voraus. Dieser ist auch vorhanden und am PC angeschlossen, aber noch nicht unter Windows installiert. Sie wissen von Ihrem Entwicklungssystem her, daß der Device-Name "Canon Bubble-Jet BJ-300" heißt. Die Liste aller auf den Windows-Installationsdisketten verfügbaren Druckertreiber ist (ab Win 3.1) in der Datei CONTROL.INF enthalten. Nachfolgend sehen Sie einen (unvollständigen) Auszug aus der ersten Sektion dieser Datei:

Achtung - wie Sie sehen können, ist nur der Device-Name unter Windows eindeutig! Da sehr viele Druckertreiber (DOS .DRV-Dateien) gleich mehrere Druckertypen (Devices) unterstützen, können Sie nicht nur einfach nach dem ersten Eintrag mit dem benötigten DOS-Druckertreibernamen suchen!

Jede Zeile unterhalb der [io.device]-Zeile enthält einen Device-Eintrag. Viele Treiber benötigen noch weitere Dateien, die bei Ihrer Aktivierung nachgeladen werden müssen. Diese werden zum einen als zweiter Listenwert in derselben Zeile gleich mit angeführt, zum anderen in der 2. Sektion "[io.dependent]" der Control.inf hinterlegt (siehe folgende Zeilen).

Die [io.device]-Section benutzt einen Aufbau innerhalb der Zeilen, der nicht mit den GetProfileString()-Funktionen gelesen werden kann. Dies ist für den normalen Anwendungsfall (Anwender installiert Drucker aus der Systemsteuerung heraus) auch nicht sinnvoll, da standardmäßig alle zur Verfügung stehenden Druckertreiber aus einer Liste auswählbar sein sollen. Als Programmierer müssen Sie sich eine kleine Routine selbst erstellen (oder kopieren Sie meine aus der Demo), wenn Sie die Funktionalität des "Drucker-Installieren" - Dialogs simulieren wollen. Wichtig für Sie ist nur, zu ermitteln, von welchen Installationsdisketten welche Dateien auf die Festplatte gebracht werden müssen, um den neuen Druckertreiber unter Windows einzubinden. Die Diskettennummern stehen jeweils mit einem Doppelpunkt vor den DOS-Dateinamen! Die 2. Sektion "[io.dependant]" kann dann wiederum mit der API-Funktion GetPrivateProfilString() ausgelesen werden. Diese hat im Vergleich zur GetProfileString()-Variante einen zusätzlichen sechsten Parameter, in dem Sie den Namen der Datei mit übergeben müssen. Folgende Anmeldungs- und Aufruf-Syntax ist hierbei angebracht:

Nachdem Sie also die Zeile innerhalb der "[io.device]"-Sektion mit Ihrem Device-Namen "gefunden" haben, extrahieren Sie daraus die benötigten DOS-Dateinamen der Treiber nebst Diskettennummern und überprüfen, wie gerade beschrieben, abschließend noch die "[io.dependant]"-Sektion auf weitere, benötigte DOS-Dateien ab. Jetzt können Sie den Anwender auffordern, die entsprechenden Disketten einzulegen um die benötigten Dateien zu kopieren.

Doch halt, die Dateien auf den Windows-Installationsdisketten sind gepackt! Eine Datei z.B. mit Namen "canon330.drv" existiert gar nicht auf Diskette #7, sondern nur eine mit Namen "canon330.dr_" ! Gleiches gilt für alle weiteren Dateien! Nun gut, werden Sie vielleicht sagen, dafür gibt es ja das Microsoft Hilfsprogramm EXPAND.EXE, mit dessen Hilfe ich ja bequem die Dateien entpacken lassen kann. Nur leider ist dieses Tool ein DOS-Programm, das Sie unschöner Weise in einem DOS-Fenster als eigenständige Task aktivieren müßten; und so etwas bringt immer gewisse Risiken mit sich - wer weiß das nicht! Allerdings (und das wissen die wenigsten), enthält das WIN-API alle Funktionen, um die Komprimierung und Dekomprimierung von Dateien direkt in einer Windows-Anwendung ablaufen zu lassen, die sg. "Lempel-Ziv Encoding Functions"!

Um den Rahmen nicht zu sprengen, werde ich an dieser Stelle nur die API-Funktionen und Strukturen ausführlicher dokumentieren, die notwendig sind, um eine einzelne komprimierte Datei in eine neue, unkomprimierte Datei zu kopieren.

Die folgenden Zeilen zeigen die "API"-Syntax der LZOpenFile()-Funktion:

Neben dem Dateinamen 1.) wird dieser Funktion ein Zeiger auf eine Struktur mit Daten mitgegeben, die Informationen über das Öffnungsergebnis enthalten 2.). Diese Struktur ist genau auf 136 Bytes groß zu initialisieren und enthält nach der Ausführung der Funktion u.a. Informationen über das Kopierziel (wenn Byte #2 = 0, dann Ziel auf Diskette, sonst Ziel auf Platte entpackt) usw. (weitere Infos. in SDK-Referenz, Thema: "OFSTRUCT"-Struktur).

Der Parameter 3.) gibt an, wie die Datei geöffnet werden soll. Die verschiedenen Modi entnehmen Sie der folgenden Tabelle:

Die folgenden Zeilen zeigen die "API"-Syntax der LZCopy()-Funktion:

Die folgenden Zeilen zeigen die "API"-Syntax der LZClose()-Funktion:

Mit dem folgenden kleinen Listing wird z.B. die Datei "Bjet.dr_" von der Diskette ins Systemverzeichnis entpackt. Sie sollten in Ihren Modulen die hier fest eingetragenen Parameter natürlich flexibel einbinden:

Wenn Sie alle Dateien in dieser Form übertragen haben, bleibt abschließend nur noch der Schritt, den neuen Treiber an den entsprechenden Stellen in der WIN.INI-Datei einzutragen. Dies realisieren Sie am einfachsten mit Hilfe der API-Funktion WriteProfileString(), dem Gegenstück zur GetProfileString()-Funktion.

Die folgenden Zeilen zeigen die "API"-Syntax der WriteProfileString()-Funktion:

Besteht ein Eintrag unter der angegebenen Sektion schon, wird der Wert zu diesem Eintrag überschrieben. Ist die Aktion erfolgreich, gibt die Funktion einen Wert ungleich 0 zurück. Um in unserem Beispiel den neuen Druckertreiber in der Sektion [devices] anzufügen, könnte folgender Zweizeiler dienen:

Natürlich sollten Sie vorher den Ausgabeport programmatisch ermitteln und nicht, wie im Beispiel gezeigt, einfach als "LPT2:" eintragen! Welche Ports Ihnen dabei zur Auswahl stehen, ermitteln Sie wiederum mit der GetProfileString()-Funktion aus der Sektion "[Ports]" in der WIN.INI-Datei. Die Vorgehensweise sollte jetzt bekannt sein.

Unter der Sektion "[PrinterPorts]" haben Sie aber abschließend noch einen weiteren Eintrag für Ihren neuen Druckertreiber zu schreiben. Dort wird nämlich zu dem Device-Namen nochmals der zugeordnete Port mit zwei Zahlen abgespeichert. Dieses Zahlenpaar definiert die Fehlerwartezeit in Sekunden für "Drucker nicht bereit" und "Übertragung wiederholen", beides Einstellungen, die der Anwender in der Systemsteuerung unter Drucker <Verbinden...> verändern kann.

Tip: Sie sollten, nachdem Sie alle Einträge in die INI-Datei geschrieben haben, die registrierte Funktion WriteProfileString() abschließend mit drei NULL-Parametern aufrufen:

Windows speichert den Inhalt der WIN.INI aus Geschwindigkeitsgründen im RAM zwischen. Die genannte Sonderform des WriteProfileString-Aufrufs sorgt dafür, daß der WIN.INI-Buffer "geflushed" wird und Sie danach "auf der sicheren Seite" sind!

Damit wäre die Frage: "Wie kann ich einen neuen Druckertreiber aus FoxPro heraus installieren lassen?" erschöpfend geklärt!

Bevor wir aber unseren "roten Faden" wieder aufnehmen können, sollen noch zwei offene Fragen besprochen werden, damit das Thema "Druckereinstellungen in der WIN.INI manipulieren" abgeschlossen werden kann.

Mit dem bisher Gelernten ist die Frage "Wie kann ich einen neuen Default-Drucker setzen?" im Prinzip einfach zu beantworten. Richtig, Sie müssen lediglich in der WIN.INI-Datei unter der Sektion "[Windows]" dem Eintrag "Device" mit Hilfe der WriteProfileString()-Funktion den neuen Device-Namen für den Default-Drucker zuweisen. Doch halt, ganz so einfach ist das nicht! Das Ganze funktioniert bislang nur in Ihrer laufenden Anwendung. Alle anderen gleichzeitig aktiven Programme (Tasks) wissen leider noch nichts von diesem neuen Default-Drucker! Sie müssen, wenn Sie also eine systemweit gültige Resource / Einstellung verändern, allen anderen Tasks auf Ihrem System eine Nachricht zukommen lassen, daß sich etwas geändert hat! Wie das Wort Nachricht auch schon sagt, gibt es unter Windows eine Systemfunktion, die genau dies für uns erledigen wird, nämlich die PostMessage()-Funktion.

Die folgenden Zeilen zeigen die "API"-Syntax der PostMessage()-Funktion:

Windows verwaltet die Nachrichten, die zwischen den einzelnen Anwendungen (Fenstern) ausgetauscht werden in s.g. Warteschlangen. Jedes Fenster (Task) hat eigens dafür vorgesehene "message queues". Wenn Sie eine Nachricht im System absetzen, müssen Sie den Empfänger der Nachricht über die Kennummer seines Fensters (kurz auch Fenster-Handle genannt) genau spezifizieren (Parameter 1. der Funktion).

Es gibt eine Vielzahl von unter Windows vordefinierten Nachrichten. Jede dieser Nachrichten ist ebenfalls durch eine eindeutige Nummer gekennzeichnet, die "message number" (Parameter 2. der Funktion).

In den letzten Parametern werden normalerweise Zeiger auf Speicherblöcke mit weiterführenden Informationen an den Empfänger übergeben, um auf diese Weide Daten zwischen den Anwendungen direkt auszutauschen. In unserem Fall benutzen wir eine ganz spezielle Nachricht mit der Nummer #26, mit deren Hilfe Sie einer anderen Task mitteilen können, daß sich der Inhalt einer Sektion der WIN.INI-Datei geändert hat. Wenn Sie als Sonderfall als "Empfänger" kein gültiges Fensterhandle angeben (der Wert 65535 z.B. ist solch ein Sonder-Handle), wird Ihre Nachricht an alle Task-Fenster (sozusagen als Rundschreiben) abgeschick. Die Message #26 benutzt den 3. Parameter nicht (NULL-Wert). Im 4. Parameter wird ein Zeiger auf den Sektionsnamen der WIN.INI-Datei übergeben, dessen Veränderungen Sie bekannt geben möchten. Das Programm des Empfänger-Fensters liest dann beim Abarbeiten dieser Nachricht den jeweiligen Abschnitt der WINI.INI-Datei wieder neu ein.

Und so registrieren und aktivieren Sie die Funktion programmtechnisch in FoxPro:

Damit bleibt nur noch die eine Frage offen: "Wie kann ich Einstellungsänderungen zu einem speziellen Druckertreiber permanent speichern?"

Wenn Sie bestimmte, druckerspezifische Einstellungen innerhalb der Systemsteuerung in einem Treiber ändern (z.B. unter <Optionen...>) werden diese Informationen ebenfalls in der WIN.INI-Datei abgelegt. Der zu suchende Sektions-Name setzt sich dabei aus dem Device-Namen und dem dazugehörigen Port-Namen (durch Komma getrennt) zusammen. Dadurch sind Sie praktisch in der Lage, zwei gleiche Drucker an zwei verschiedenen Schnittstellen an Ihrem PC mit unterschiedlichen Einstellungen zu betreiben. Das folgende Beispiel soll dies verdeutlichen:

Angenommen Sie besitzen zwei gleiche Bubble-Jet Drucker, angeschlossen an den parallelen Schnittstellen #1 und #2 Ihres PC's. An LPT1 benutzen Sie einen automatischen Einzelblatteinzug und eine Druckauflösung von 360x360 Punkten. Das andere Gerät ist an LPT2 angeschlossen und bedruckt immer nur Endlospapier mit 180x180 dpi. Der erste Drucker ist zudem noch der Windows Default-Drucker. Damit würden in Ihrer WIN.INI-Datei folgende Einträge auftauchen (Abweichungen von den hier aufgeführten DOS-Namen durch verschiedenen Treiberversionen sind möglich):

Jeder Drucker hat auch seine eigene (Info-)Sektion innerhalb der WIN.INI. Die Werte zu den einzelnen Einträgen spiegeln die derzeitigen (Vorgabe-)Einstellungen wieder. Leider kocht derzeit jeder Druckerhersteller so sein eigenes Süppchen, was die Benennung der Einträge (links vom Gleichheitszeichen) angeht. So müssen Sie also als Programmierer schon genau wissen, wie der Name des Eintrags einer speziellen Device lautet, wenn Sie dessen Wert lesen oder schreiben wollen. Die Verfahrensweise ist immmer gleich: Sie lesen mit GetProfileString() den Wert ein, verändern diesen, schreiben ihn mit WriteProfileString wieder zurück, flushen sicherheitshalber danach den WINI.INI-Buffer und setzen abschließend mit der PostMessage()-Funktion eine entsprechende Nachricht an alle Task-Fenster ab. Natürlich gibt für diesen Zweck eine viel elegantere Lösung, die ich Ihnen aber erst am Ende des Kapitels über die s.g. ExtDeviceMode()-Funktion näherbringen werde...

Der individuellen Anpassung Ihrer Druckertreiber steht nun (außer Ihnen selbst) nichts mehr im Wege! Ich wünsche "gutes Gelingen" und nehme endlich unseren "roten Faden" wieder in die Hand...

Das GDI (Graphic Device Interface) bietet dem "C"-Programmierer eine Vielzahl von API-Funktionen, mit denen Ausgaben manipuliert werden können. Die grundlegende Verfahrensweise einer Ausgabe unter Windows läßt sich auf folgende einfache Darstellung reduzieren:

Egal ob nun eine Ausgabefunktion (z.B. die API-Funktion "TextOut") einen Text auf dem Drucker oder dem Bildschirm ausgeben soll, die Funktion "besorgt" sich bei Windows immer einen s.g. "Device Context", in dessen Rahmen (Speicherbereich) sie ihre Ausgabeanforderung "formuliert". Mit diesem Informationspaket steuert das GDI jetzt den entsprechenden Gerätetreiber an, der seinerseits wiederum die Ausgabe auf das vom physikalischen Endgerät verstandene Datenformat umsetzt.

Die Aufbereitung von Daten für die Druckerausgabe geschieht in Windows in einem eigens dafür reservierten Speicherbereich. Diesen könnte man auch als "Umgebung" eindeutschen, beläßt es aber (es klingt halt smarter) bei der englischen Orginalbezeichnung: Device Context. Ein solcher Speicherbereich kann später nur über eine spezielle Identifikationsnummer wieder angesprochen werden!

Das Anmelden einer Ausgabeabsicht wird durch eine spezielle WIN-API Funktion des GDI's erledigt. Sie heißt CreateDC, was soviel heißt, wie "erzeuge Device Context". Diese Funktion liefert als Resultat die gerade erwähnte ID-Nummer zurück, wenn der für die Ausgabe erforderliche Speicherbereich reserviert werden konnte. Diese Nummer werden wir im weiteren fachgerecht ein "Handle auf ein DeviceContext" nennen und auch dementsprechend mit "HDC" abkürzen.

Merken Sie sich für Ihre weitere Lektüre: Wollen Sie etwas unter Windows (irgendwohin) ausgeben, geht das nur über einen von Windows reservierten Speicherbereich, den DeviceContext. Dieser DC bekommt ein Handle zugewiesen, wodurch er, während seiner Laufzeit, eindeutig numerisch identifizierbar ist. - Besser, weil noch kürzer formuliert: Kein HDC, kein Output und erst recht keine Manipulation von Output!

Dies soll als Einführung für die Beantwortung der Frage: „Wie wickelt FoxPro eigentlich intern einen Druckjob ab?" reichen.

Die folgenden Zeilen zeigen die "API"-Syntax der CreateDC - Funktion:

Variablen-Beschreibung:

Als Rückgabewert erhalten Sie, wie schon erwähnt, einen HDC (Integerwert) für die angegebene Ausgabeeinheit. Anderenfalls, im Falle des Fehlschlagens, ist der Return-Wert NULL (in FoxPro: m.hdc = 0).

Lassen Sie uns also an dieser Stelle einmal die CreateDC()-Funktion registrieren. Laut SDK-Referenz sind alle vier Übergabeparameter Zeiger (drei davon auf Strings und der vierte auf eine Struktur). Eine Registrierung wie folgt ist somit richtig:

beachten Sie, daß der vierte Parameter als nicht modifizierbarer String vereinbart wird. Dieser String enthält später zur Laufzeit die feste (nicht modifizierbare) Struktur mit den Initialisierungsdaten. Um eine derart registrierte Funktion einsetzen zu können, müssen Sie vorab unter FPW drei Variablen anlegen und initialisieren, damit Sie diese dann referenziert an die Funktion übergeben können:

Da lt. SDK-Referenz alle String-Pointer als Parameter vom Typ LPCSTR vereinbart werden, wird auch folgende Anmeldung durchaus funktionieren:

diese Schreibweise hat den Vorteil, daß der Aufruf mit fixen Strings z.B. direkt aus dem Befehlsfenster abgesetzt werden kann, ohne vorher (im Direkteingabemodus umständlich) Variablen anlegen zu müssen:

Wie Sie als aufmerksamer Leser bemerkt haben, wurde im letzteren Beispiel die CHR(0)-Ergänzung der Strings unterlassen. Dieses "C"-typische String-Endekennzeichen kann entfallen, da FoxPro (weil in C programmiert) "von Haus aus" alle Strings mit dem 0-Byte beendet, als xBase-Programmierer sieht mann's halt "tagsüber" nur nicht.

Nachdem die Funktion erfolgreich Ihren angeforderten DeviceContext für den Drucker reserviert hat, sind Sie im Besitz des zugehörigen Handles (HDC). Dieses Handle ist elementarer Bestandteil für die meisten weiterführenden API-Calls im Windows-GDI. Der eigentliche Druckertreiber (in unserem Beispiel BJET.DRV) wird durch das Anlegen eines DCs automatisch in den Speicher geladen, falls er gerade ausgelagert (ungeladen) war. Bitte beachten Sie, daß die DOS-Namen der jeweiligen Treiber je nach Version auf Ihrem PC anders heißen können (z.B.: älter Treiber = BJET.DRV, neuere Version = CANON330.DRV).

Bislang haben Sie die Funktion CreateDC mit dem NULL-Wert als viertem Aktualparameter aufgerufen. Diese Sonderform des Aufrufs veranlaßt das Setzen von Default-Werten beim Initialisieren der Druckerumgebung, so wie diese in der Systemsteuerung zu dem Drucker hinterlegt worden sind. Die aktuellen (und die Initialisierungs-) Informationen der Druckerumgebung werden in einer Struktur mit Namen DEVMODE verwaltet. Für die weiteren Manipulationen der aktuellen Druckerumgebung werden wir mit verschiedenen Funktionen arbeiten, die wiederum auf die Inhalte von Strukturen dieses Typs zurückgreifen. Der feststehende Teil der DEVMODE-Struktur hat, wie im SDK beschrieben, folgenden Aufbau:

Dabei speichern die "Members" (Mitglieder) der Struktur folgende Informationen:

<dmDeviceName>
enthält den Device-Namen, den der Treiber unterstützt. Dieser kann derzeit maximal 32 Bytes (inklusive der abschließenden CHR(0)) lang sein.
<dmSpecVersion>
Versions-Nr. der DEVMODE-Struktur (für Windows 3.1 ist das hexadezimal 30A).
<dmDriverVersion>
Versions-Nr. des Druckertreibers, die vom Hersteller zugeordnet wurde.
<dmSize>
Länge der DEVMODE-Struktur (hier würde jetzt 68 eingetragen) ohne die möglicherweise anschließenden Extra-Bytes der druckertreiberspezifischen Daten.
<dmDriverExtra>
Länge des anschließenden Extra-Bytes Datenblocks, der treiberspezifisch unterschiedliche Längen aufweisen kann.
<dmFields>
Flagfeld, das bitweise kodiert Aufschluß darüber gibt, welche der ab hier noch folgenden Felder (Members) initialisiert (gesetzt) worden sind. Alle denkbaren Bitkombinationen sind erlaubt (siehe Tabelle "Bits im Flagfeld")
<dmOrientation>
Orientation: PORTRAIT (1) oder LANDSCAPE (2)
<dmPaperSize>
Wenn 0, wird die Papiergröße durch die beiden folgenden Members der Struktur bestimmt, sonst entspricht der Eintrag vom Wert her einer "festen", vordefinierten Papiergröße (siehe Tabelle "Papiergrößen-Nummern").
<dmPaperLength>
Papierlänge in 10tel Millimetern. Dieser Wert überschreibt in jedem Fall den Papierlängenwert eines u.U. aktiven Standard-Papierformats.
<dmPaperWidth>
dto. für Papierbreite
<dmScale>
Spezifiziert einen Skalierungsfaktor, um den der Ausdruck vergrößert/verkleinert wird. Die Originalpapiermaße (Länge und Breite) werden dabei mit <dmScale>/100 multipliziert. Ist z.B. das Papier 8,5" x 11" groß und der Wert in <dmScale> = 50, so würde auf dasselbe Blatt Papier ein Ausdruck der Größe 17" x 22" passen, bzw. das gedruckte Bild wird um 50% kleiner ausgegeben.
<dmCopies>
Anzahl der Kopien (falls der Drucker dieses Feature unterstützt).
<dmDefaultSource>
Standard-Schacht für Blattzufuhr. Die Werte für die verschiedenen Schächte entnehmen Sie der Tabelle "Bin-Nummern".
<dmPrintQuality>
Es gibt vier vordefinierte Druckqualitäten: HIGH (-4), MEDIUM (-3), LOW (-2) und DRAFT (-1). Wenn der Wert dieses Members positiv ist, spezifiziert er die Druckerauflösung in X-Richtung in Dots per Inch (DPI). Der korrespondierende Y-Wert muß dann im <dmYResolution>-Member abgelegt sein.
<dmColor>
Zeigt, ob es sich bei dem Drucker um einen Farb- (<dmColor>=1) oder Monochrome- (<dmColor>=2) Drucker handelt.
<dmDuplex>
Zeigt an, ob der Drucker Duplex-Druck (beidseitig) beherrscht. Die drei möglichen Werte stehen für SIMPLEX (1), Duplex HORIZONTAL (2) und Duplex VERTICAL (3).
<dmYResolution>
siehe <dmPrintQuality>-Member der Struktur.
<dmTTOption>
Zeigt an, wie TrueType-Fonts behandelt werden sollen. Die möglichen drei Werte sind: BITMAP (1) druckt TTF als Bitmap z.B. bei Matrix-Druckern, DOWNLOAD (2) z.B. bei HP-Laserdruckern die die Fonts als Softfonts einladen, SUBDEV (4) Substitution der Fonts (Ersetzen) wie dies bei Postscript-Druckern üblich ist.

Die folgenden Tabellen enthalten DEFINE-Statements, die Sie in Ihre FoxPro-Programme einfügen können. Die Zahlenwerte beziehen sich alle auf die verschiedenen Members einer DEVMODE-Struktur, soweit diese nicht schon dort erläutert wurden.

Tabelle "Bits im Flagfeld"

Tabelle "Bin-Nummern"

Tabelle "Die gebräuchlichsten Papiergrößen-Nummern" (nicht vollständig)

Wenn Sie also beim Anlegen des Drucker-DC's bereits eine DEVMODE-Struktur in Form eines Strings angelegt und die entsprechenden Members mit Werten versorgt haben, können Sie diese Struktur als vierten Parameter mit angeben (anstelle des NULL-Wertes), um den Druckertreiber gleich mit den von Ihnen gewünschten Werten zu initialisieren.

Es ist nun an der Zeit, daß Sie sich eine "gefüllte" DEVMODE-Struktur einmal ansehen. Das Demoprogramm zu diesem Vortrag: „DEMO1.APP" kann Ihnen eine solche Struktur anzeigen.

Sie sehen in der folgenden Abbildung eine DEVMODE-Struktur, die mit den momentanen Standardwerten des aktuellen Druckertreibers geladen wurde. Je nach geladenem Druckertreiber kann die Anzeige von der hier ausgedruckten Abbildung abweichen:

 

Bild001: <devmode1.bmp>

 

Die Anzeige enthält neben den bereits detailliert erklärten Elementen der DEVMODE-Struktur am unteren Rand ein zusätzliches Feld mit "Device-spezifischen Daten". Dieser, in der Länge variable Info-Block, differiert von Treiber zu Treiber und enthält diejenigen Informationen, die herstellerspezifisch sind und somit nicht in das allgemeingültige DEVMODE-Schema hineinpassen. Alle Werte werden in meinem Programm hexadezimal ausgegeben (ich entspreche damit mehr den "C"-typischen Konventionen). In der Abbildung sehen Sie, daß das Feld "DriverExtra" der DEVMODE-Struktur den Wert hex 4C enthält, was der Größe von 76 Bytes für die Treiber-spezifischen Daten entspricht. Die gesetzten Markierkästchen rechts neben den Feldern signalisieren, daß das jeweilige Feld (Member der Struktur) "aktiv" ist.

Schauen Sie zurück und vergleichen Sie einmal die Tabelle "Bits im Flagfeld". So wird ermittelt, welche Felder "gesetzt", d.h. aktiv und veränderbar / auswertbar sind:

Das DoubleWord <Fields> enthält in der Abbildung den Hex-Wert 6603. Jetzt wird eine AND-Verknüpfung zwischen den Flagwerten der Tabelle und dem <Fields>-Wert ausgeführt. Für alle Flagwerte, die ein Resultat größer 0 liefern (logisch WAHR), gilt der korrespondierende Eintrag der DEVMODE-Stuktrur als "gesetzt":

Mnemonic Flag-Wert Fields-Wert gesetzt

Fassen wir das bisher zur DEVMODE-Struktur Gesagte zusammen:

Beim Erstellen eines Drucker-DeviceContext wird der ermittelte Druckertreiber (DOSNAME.DRV) nebst weiteren benötigten Dateien geladen und mit den in der WIN.INI-Datei angelegten, ihm zugeordneten Einstellungen initialisiert - aber nur dann, wenn Sie keine DEVMODE-Struktur beim Aufruf der CreateDC-Funktion (vorbelegt) übergeben haben.

Die im WIN-API vereinbarten Namen der Members der DEVMODE-Struktur werden von fast allen Druckertreiber-Herstellern sinngemäß auch als Eintrags-Namen in der WIN.INI-Datei benutzt, wodurch es Ihnen in Zukunft leicht fallen sollte, die treiberspezifischen Einträge zu "entschlüsseln". Als Beispiel sehen Sie sich die folgende Gegenüberstellung einmal an:

Sie sollten wissen, daß nur diejenigen Einträge in der WINI.INI-Datei existieren, die bereits irgenwann einmal vom Anwender aus der Systemsteuerung heraus verändert wurden und dadurch von den treiberinternen Defaultwerten abweichen! So können Sie durch Löschen der gesamten Sektion eines Druckertreibers z.B. die Initialisierung mit Treiber-Defaults erzwingen.

Bislang haben wir uns immer nur um den ersten Teil der DEVMODE-Stuktur gekümmert. Jede DEVMODE-Stuktur besteht aber noch aus einem zweiten Block von treiberspezifischen Daten. Diese sind von Hersteller zu Hersteller verschieden und können dementsprechend unterschiedliche Längen aufweisen. Um auf den ersten Blick bestimmen zu können, wie viele Bytes für diesen zweiten Datenblock benötigt werden und wieviele für den ersten, besitzt die Struktur bekanntlich zwei Einträge, nämlich <dmSize> und <dmDriverExtra>, in denen diese Informationen abgelegt werden.

Nun müssen Sie aber als Programmierer immer für die gesamte DEVMODE-Struktur Speicherplatz reservieren, bevor Sie mit entsprechenden Funktionen diese Struktur dann füllen und manipulieren können. Würden Sie nur die eingangs beschriebenen 68 Bytes reservieren, wären Speicherbereichsüberschreibungen beim Beschreiben durch die anhängenden "ExtraBytes" vorprogrammiert. Zum Glück gibt es (fast immer - s.u.) eine Funktion, mit deren Hilfe Sie wichtige Informationen zur DEVMODE-Struktur aus einem Druckertreiber "herausholen" können, um diese dann weiterzubearbeiten: die ExtDeviceMode()-Funktion.

Die ExtDeviceMode()-Funktion ist recht komplex und vielfältig einsetzbar. Wichtigstes Merkmal ist, daß sie nicht direkt zum WIN-API gehört, sondern direkt vom Druckertreiber (.DRV-Datei) exportiert wird. Dies bedeutet, daß sie nicht in einer der Windows-DLL Dateien enthalten, sondern immer Bestandteil (wenn überhaupt) der DRV-Datei selbst ist. Das hat zur Folge, daß Sie (oder FoxPro) den Treiber erst einmal laden müssen, bevor Sie auf die Funktionen der ExtDeviceMode-Funktion zurückgreifen können. Ein Glück, daß ein Druckertreiber nichts anderes ist, als eine DLL-Datei mit einer abweichenden Namenserweiterung! Dadurch besteht wiederum für uns programmtechnisch die Möglichkeit, mit der LoadLibrary()-Funktion des WIN-API's die Treiber-DLL zu aktivieren. Nachfolgend ein kurzes Programm zur Verdeutlichung des Verfahrens:

Der Funktion LoadLibrary() wird der Dateiname der zu ladenen DLL-Bibliothek per Referenz übergeben. Sie können an einem Rückgabewert > 32 erkennen, daß das Modul erfolgreich geladen wurde. Kleinere Werte sind Fehlerwerte, deren genaue Bedeutung in der SDK-Referenz nachgeschlagen werden kann.

Nach dem erfolgreichen Laden gibt die Funktion ein s.g. Instanz-Handle des Bibliothek-Moduls zurück. Der Sinn solcher dynamischer Laufzeit-Module ist, nur einmal (für alle laufenden Tasks) geladen werden zu müssen. Wird also eine Druckertreiber-DLL von mehreren Applikationen kurz nacheinander "geladen", befindet sie sich effektiv nur einmal im Hauptspeicher und es wird bei jedem weiteren "Laden" nur ein Windows-interner Referenzzähler um eins erhöht. Beim Entladen des Moduls durch die einzelnen Anwendungen wird dieses auch folgerichtig erst dann endgültig aus dem Speicher entfernt, wenn der Referenzzähler wieder den Wert 0 erreicht hat. In dieser Technik des Resourcen-Sharings ist auch der Grund des teilweise unerklärlichen Verlußt von System-Resourcen beim exzessiven Drucken unter FoxPro zu sehen, dazu später mehr.

So, nachdem nun der Druckertreiber als Laufzeit-Bibliothek in den Speicher eingeladen wurde, muß als nächster Schritt überprüft werden, ob die ExtDeviceMode-Funktion überhaupt darin enthalten ist. Man sagt auch: es wird geprüft, ob der Treiber die Funktion exportiert. Einige Treiber beinhalten diese und weitere Funktionen dann nämlich nicht, wenn sie eine Device-Initialisierung per Software gar nicht unterstützen!

Diese Aufgabe erledigen Sie durch das Anmelden der API-Funktion GetProcAddress(). Nachfolgend wieder ein kurzes Programm zur Verdeutlichung:

Die Funktion wird mit einem Intergerwert (Instanz-Handle der Bibliothek im Speicher) und einem Zeiger auf den zu überprüfenden Namen der exportierten Funktion registriert. Der Funktionsname muß genauso angegeben werden, wie er bei der Erstellung der DLL vom Programmierer festgelegt wurde - normalerweise in Großbuchstaben. (Die Möglichkeit der Übergabe einer "ordinal value" im "double word"-Format anstelle der referenzierten String-Variable soll hier unterbleiben, da alle für unsere Versuchsreihen benutzen Funktionen namentlich exportiert werden.)

Wird die gesuchte Funktion exportiert, erhalten Sie die Einsprungadresse in der Bibliothek, ansonsten den Wert 0. Ist die Prüfung positiv verlaufen, kann (endlich!) die ExtDeviceMode-Funktion angemeldet und benutzt werden.

Hier die "API"-Syntax der ExtDeviceMode() - Funktion:

Die Funktion liefert einen Integerwert zurück, dessen Bedeutung abhängig vom Einsatzzweck variiert (s.u.)

Die ExtDeviceMode führt drei Hauptaufgaben aus:

I.) Sie kann benutzt werden, um die wirklich benötigte Anzahl Bytes für die einzusetzenden DEVMODE-Strukturen zu ermitteln. Dies sollte auch immer Ihr erster Einsatzschritt sein.

II.) Sie kann benutzt werden, um die Initialisierungs-Einstellungen eines Druckertreibers einzulesen.

III.) Sie kann benutzt werden, um eine Dialog-Box anzuzeigen, in der der Anwender Einstellungsänderungen vornehmen kann. Diese können dann benutzt werden um (a.) eine temporäre Druckereinstellung zu realisieren, oder (b.) permanente Grundeinstellungen zurück in die entsprechende INI-Datei zu schreiben.

Hinweis: im Einsatzfall III.) entspricht die ExtDeviceMode-Funktion der älteren DevMode()-Funktion, die dieselbe Dialog-Box anzeigen kann. Die DeviceMode-Funktion sollte (lt. SDK-Referenz) aber von neueren Applikationen nicht mehr benutzt werden und wird daher in diesem Beitrag von mir nicht weiter erläutert, wenngleich im Demo-Programm noch enthalten.

Die einzelnen Parameter (1. - 8.) werden für folgende Aufgaben (I. - III.) benutzt:

Der 1. Parameter <hwnd> ist ein "HWND" Windows Fenster-Handle. Dieses Handle wird benutzt, wenn die ExtDeviceMode-Funktion in Aufgabe III.) eine Dialogbox anzeigt. Das übergebene Fensterhandle verweist damit auf das Elternfenster der Druckerdialog-Box. In unserem Fall "besorgen" wir uns das Windows-Handle des FoxPro-Hauptfensters mit der Funktion MAINHWND() aus der Foxtools.fll.

Der 2. Parameter <hDriver> ist ein "Instanz-HANDLE" auf den zuvor mit LoadLibrary() geladenen Druckertreiber. In allen Anwendungfällen wird dieser 2. Parameter benötigt!

Der 3. Parameter <lpdmOutput> ist eine Zeiger auf eine von der ExtDeviceMode-Funktion mit Rückgabewerten gefüllte DEVMODE-Struktur. Diese Struktur wird in Fall II.) und III.) mit Werten belegt. In unseren Quelltexten ist dies ein per Referenz übergebener "DEVMODE-String".

Der 4. und 5. Parameter <lpszDevice> und <lpszPort> sollte Ihnen aus den früheren Kapiteln bekannt sein. Diese Werte werden benutzt, wenn die ExtDeviceMode-Funktion in den Fällen II.) und IIIb.) auf INI-Dateien zugreifen muß, in denen Drucker-Defaulteinstellungen in bekannter Form (z.B.: "[Canon Bubble-Jet BJ-300,LPT1]") abgelegt sind.

Der 6. Parameter <lpdmInput> ist eine Zeiger auf eine DEVMODE-Struktur, die von Ihnen vorbelegt werden kann. Diese Struktur wird in Fall IIIa.) benutzt, um die erscheinende Dialog-Box mit den entsprechenden Werten zu initialisieren.

Der 7. Parameter <lpszProfile> ist vom Typ her ein "LPSTR"-Zeiger und muß unter FoxPro also als "@C"-Parametertyp vereinbart werden. In diesem String wird die INI-Datei angegeben, in der sich die vom Treiber benötigten Default-Werte befinden, bzw. wieder zurückgeschrieben werden sollen. Wird dieser Zeiger in der C-Programmierung als NULL-Wert übergeben, heißt das für die ExtDeviceMode-Funktion, daß speziell die WIN.INI-Datei gemeint ist. Wollen Sie diese verkürzte (weil eingeschränkte) Syntax auch in FoxPro benutzen, können Sie den 7. Parameter (ausnahmsweise!) als einfachen "C"-Type vereinbaren. Dann dürfen Sie aber auf jeden Fall später NUR den nummerischen Wert 0 übergeben, um die bekannte "NULL" zu simulieren.

Der 8. Parameter <fwMode> hat die Bytelänge 2 (WORD) und wird von uns bei der Funktions-Registrierung als "I"-Typ vereinbart. Mit dem Wert dieses Parameters steuern Sie später beim Aufruf der Funktion ihr Verhalten (welche Aufgabe soll erledigt werden). Die Werte und die daraus resultierende Arbeitsweise der ExtDeviceMode-Funktion entnehmen Sie der nachfolgenden Tabelle:

So gehen Sie weiter im Programm vor:

Jetzt haben Sie die benötigten DEVMODE-Strings an die wirkliche Gesamtgröße der Struktur angepaßt. In den weiteren Aufrufen müssen die Flag-Werte im 8. Parameter <fwMode> auf Bitebene kombiniert werden. Dabei ist es wichtig zu wissen, daß immer entweder der Wert "DM_OUT_BUFFER" oder der Wert "DM_OUT_DEFAULT" - also Bit #0 oder #1 gesetzt sein müssen. Diese können dann mit den restlichen Werten kombiniert werden. Da die Flag-Werte allesamt 2er-Potenzen darstellen, ist dies ganz einfach unter FoxPro durch Addition der Einzelwerte zu erreichen. So ist es z.B. möglich mit folgendem Aufruf "auf einen Rutsch" zuerst die momentanen Einstellungen des Treibers in die Vorgabe-Struktur einzulesen, dann die Dialog-Box für Änderungen durch den Anwender zu präsentieren, um diese dann abschließend in der Rückgabe-Struktur abzulegen:

Wie Sie sehen, gibt die ExtDeviceMode-Funktion nach der Präsentation der Dialog-Box den "OK/Abbrechen"-Status in Form der Werte 1 oder 2 zurück. Bricht der Anwender den Dialog ab, ist die Rückgabe DEVMODE-Struktur nicht initialisiert (es stehen keine brauchbaren Werte darin!). In diesem Fall müssen Sie auf die Werte der Vorgabe DEVMODE-Struktur zurückgreifen.

So, wenn Sie nun im vorangegangenen Beispiel den Flagwert wie folgt vorbelegen:

wird die vom Anwender vielleicht veränderte Einstellung bei Bestätigung des Dialogs (IF m.LnOk = IDOK ) vollautomatisch in die WIN.INI-Datei als neue Default-Werte zurückgeschrieben. Dabei brauchen Sie nicht zwangsläufig das "DM_IN_PROMPT "-Bit im Flag-Wert setzen! Natürlich können Sie aus Ihrem Programm heraus direkt den outDEVMODE-String manipulieren und Ihre Einstellungen für den Anwender unsichtbar nur durch Angabe von "DM_OUTDEFAULT" als Flag-Wert in die INI-Datei schreiben lassen. Bedenken Sie zusätzlich noch, daß Sie durch Angabe einer INI-Datei Ihrer Wahl (im vorletzten Parameter) die Initialisierungswerte des Druckers nicht zwangsläufig in der WIN.INI speichern müssen - ungeahnte Möglichkeiten tun sich da offen, wie ich meine...

Dies sollte jetzt aber genügen um die Frage: "Wie kann ich Einstellungsänderungen zu einem speziellen Druckertreiber permanent speichern?" endgültig vom Tisch zu bekommen.

Mit der ExtDeviceMode-Funktion und den DEVMODE-Strukturen können Sie jetzt bequem die Druckereinstellungen manipulieren - oder etwa nicht??

Bequem nur dann, wenn Sie die Veränderungen lediglich per Dialog-Box vom Anwender ausführen lassen wollen! Sie bekommen programmtechnisch bereits etablierte Einstellungswerte des Druckerteibers jetzt schon "angeliefert", aber die Gesamtheit ALLER Einstellmöglichkeiten, die möglichen Schrittweiten innerhalb einer einzelnen Einstellung und deren Grenzwerte bleiben bislang nur per Dialog erreichbar!

Damit Sie sich in Zukunft über die Fähigkeiten (engl.: capabilities) Ihres Druckertreibers auch programmatisch Überblick verschaffen können, existiert eine weitere Funktion genannt: DeviceCapabilities().

 

Next Page