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
Der Gang der Dinge
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:
device = Canon Bubble-Jet BJ-300,BJET,LPT2:
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:
m.LnGPS = RegFn("GetProfileString", "CCC@C","I") && FN registrieren
m.LcRS = REPLICATE(CHR(0),80) && Platz für Ergebnisstring vorbelegen
m.LnRL = 0 && Länge des tatsächlich zurückgegebenen Strings initialisieren
m.LnRL = CallFN(m.LnGPS, "Windows","Device","kein Default-Drucker",@m.LcReturnString,80)
STORE "" TO m.LcDevice, m.LcDriver, m.LcPort
DO CASE
CASE m.LnRL = 0
WAIT WINDOW TIMEOUT 5 "Kein Default-Druckername in ;
WIN.INI-Datei eingetragen"
CASE LEFT(m.LcRS,m.LnRL) = "kein Default-Drucker"
WAIT WINDOW TIMEOUT 5 "Kein Default-Druckereintrag ;
in WIN.INI-Datei vorhanden"
CASE OCCURS(",",m.LcRS) != 3
WAIT WINDOW TIMEOUT 5 "Fehlerhafter Default-Druckereintrag
in WIN.INI-Datei vorhanden"
OTHERWISE
m.LcDevice = LEFT(m.LnRS,AT(",",m.LnRS,1)-1) && Device-Name extrahieren
m.LcDriver = SUBSTR(m.LnRS,AT(",",m.LnRS,2)+1)
&& Treiber- u. Port-Name zusammen
m.LcDriver = LEFT(m.LnRS,AT(",",m.LcDriver,1)-1)
&& Treiber-Name jetzt extrahiert
m.LcPort = SUBSTR(m.LnRS,AT(",",m.LnRS,3)+1) && Rest ist Port-Name
ENDCASE
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:
[devices]
Canon Bubble-Jet BJ-300=BJET,LPT2:
Canon Bubble-Jet BJC-800=CANON800,LPT1:
Epson LQ-500=EPSON24,LPT2:
Epson LQ-860=EPSON24E,LPT1:
HP DeskJet 500=HPDSKJET,LPT1:
HP DeskJet 550C Printer=DESKJETC,LPT1:
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:
m.LnGPS = RegFn("GetProfileString", "CCC@C","I") && FN registrieren
m.LcAllDevS = REPLICATE(CHR(0),1024) && besser ein paar Bytes mehr...
m.LnRetLen1 = CallFn(GetProfileString,"Devices",0,"",@lcAllDevS,LEN(m.LcAllDevS))
m.LnCurPos = 0
m.LnCount = 1
DIMENSION LaAllPrnt[1,3]
STORE "" TO LaAllPrnt
DO WHILE m.LnCurPos < m.LnRetLen1
m.LnLastPos = lm.LnCurPos
m.LnCurPos = AT(CHR(0), m.LcAllDevS, m.LnCount)
DIMENSION LaAllPrnt[m.LnCount,3]
STORE "" TO LaAllPrnt[m.LnCount,1], LaAllPrnt[m.LnCount,2], ;
LaAllPrnt[m.LnCount,3]
LaAllPrnt[m.LnCount,1] = SUBSTR(m.LcAllDevS,m.LnLastPos + 1, ;
m.LnCurPos - (m.LnLastPos +1))
* Jetzt zum Device-Namen den Port und den Treibernamen holen
m.LcDevString = REPLICATE(CHR(0),32)
lnRetLen2 = CallFn(GetProfileString,"Devices",laAllPrnt[m.LnCount,1]+;
CHR(0),"",@m.LcDevString,LEN(m.LcDevString))
laAllPrnt[lnCount,2] = SUBSTR(m.LcDevString,1,;
AT(",",m.LcDevString,1)-1)
&& DruckerTreiber-Dosname (ohne ".DRV")!
laAllPrnt[lnCount,3] = SUBSTR(m.LcDevString,AT(",",;
m.LcDevString,1)+1, ;
m.LnRetLen2-AT(",",m.LcDevString,1)) && Ausgabe-Port
m.LnCount = m.LnCount +1
ENDDO
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:
[io.device]
7:TTY.DRV,"Universal/Nur Text","DEVICESPECIFIC"
9:BASE9.DRV,4:UNIDRV.DLL,"Generic Epson FX-85 9pin","DEVICESPECIFIC"
9:BASE9.DRV,4:UNIDRV.DLL,"Generic Epson FX-85 9pin wide","DEVICESPECIFIC"
9:LQBASE24.DRV,4:UNIDRV.DLL,"Generic ESC/P 24pin","DEVICESPECIFIC"
7:CANON330.DRV,4:UNIDRV.DLL,"Canon Bubble-Jet BJ-330","DEVICESPECIFIC"
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).
[io.dependent]
base9.drv= 5:unidrv.hlp, 6:dmcolor.dll
brhj770.drv= 5:unidrv.hlp, 7:finstall.dll, 7:finstall.hlp
canon330.drv= 5:unidrv.hlp
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:
m.LnGPPS=REGFN("GetPrivateProfileString","CCC@CIC","I") && FN anmelden
m.LcRS = REPLICATE(CHR(0),100) && Platz für Ergebnisstring vorbelegen
m.LnRL = CallFN(m.LnGPPS,"io.dependent","canon330.drv","",@LnRS,100,"Control.inf")
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"!
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:
HFILE LZOpenFile(lpszFile, lpof, style)
1.) LPCSTR lpszFile; /* Dateiname */
2.) OFSTRUCT FAR* lpof;
/* Zeiger auf Struktur-String mit "Open_File"-Info*/
3.) UINT style; /* Öffnen-Modus */
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:
Tabelle: LZOpenFile()-Flags
Mnemonic Hex Dezimal
#define OF_READ 0x0000 0
#define OF_WRITE 0x0001 1
#define OF_READWRITE 0x0002 2
#define OF_SHARE_COMPAT 0x0000 0
#define OF_SHARE_EXCLUSIVE 0x0010 16
#define OF_SHARE_DENY_WRITE 0x0020 32
#define OF_SHARE_DENY_READ 0x0030 48
#define OF_SHARE_DENY_NONE 0x0040 64
#define OF_PARSE 0x0100 256
#define OF_DELETE 0x0200 512
#define OF_VERIFY 0x0400 1024
#define OF_SEARCH 0x0400 1024
#define OF_CANCEL 0x0800 2048
#define OF_CREATE 0x1000 4096
#define OF_PROMPT 0x2000 8192
#define OF_EXIST 0x4000 16384
#define OF_REOPEN 0x8000 32768
Die folgenden Zeilen zeigen die "API"-Syntax der LZCopy()-Funktion:
LONG LZCopy(hfSource, hfDest)
1.) HFILE hfSource; /* Datei-Handle der Quell-Datei */
2.) HFILE hfDest; /* Datei-Handle der Ziel-Datei */
Die folgenden Zeilen zeigen die "API"-Syntax der LZClose()-Funktion:
void LZClose(hf)
1.) HFILE hf; /* Datei-Handle der zu schließenden Datei */
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:
m.LnLZOpenFile = RegFn("LZOpenFile","C@CI","I","lzexpand")
m.LnLZCopy = RegFn("LZCopy","II","L","lzexpand")
m.LnLZClose = RegFn("LZClose","I","","lzexpand")
m.OFSTRUCT1 = REPLICATE(CHR(0),136)
m.OFSTRUCT2 = REPLICATE(CHR(0),136)
m.hfSourceFile = CallFn(m.LnLZOpenFile,"A:\bjet.dr_",@m.OFSTRUCT1,OF_READ)
&& "Nur lesen" öffnen
m.hfDestFile = CallFn(m.LnLZOpenFile,"C:\WINDOWS\SYSTEM\bjet.drv",;
@m.OFSTRUCT2,OF_CREATE) && "Anlegen" öffnen
= CallFn(m.LnLZCopy,m.hfSrcFile,m.hfDestFile)
&& Kopieren mit Dekomprimirung
= CallFn(m.LnLZClose,m.hfSrcFile) && Dateien schließen
= CallFn(m.LnLZClose,m.hfDestFile) && Dateien schließen
RELEASE m.OFSTRUCT1,m.OFSTRUCT2
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:
BOOL WriteProfileString(lpszSection, lpszEntry, lpszString)
1.) LPCSTR lpszSection; /* Adresse des Sektionsnamens */
2.) LPCSTR lpszEntry; /* Adresse des Eintrags */
3.) LPCSTR lpszString; /* Adresse des Werts (rechts vom
Gleichheitszeichen) */
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:
m.LnWPS = RegFn("WriteProfileString", "CCC","I") && FN registrieren
m.LnOk = CallFN(m.LnWPS, "Devices","Canon Bubble-Jet BJ-300","BJET,LPT2:")
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:
m.LnOk = CallFN(m.LnWPS, 0, 0, 0)
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.
Setzen eines Default-Druckers
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:
BOOL PostMessage(hwnd, uMsg, wParam, lParam)
1.) HWND hwnd; /* Handle des Empfänger-Fensters */
2.) UINT uMsg; /* Nachrichtennummer die gesendet werden soll */
3.) WPARAM wParam; /* erster Nachrichten-Parameter - 16 Bit */
4.) LPARAM lParam; /* zweiter Nachrichten-Parameter - 32 Bit */
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:
PostMessage = RegFn('PostMessage',"IICC","I") && Message senden
#DEFINE HWND_BROADCAST 65535
#DEFINE WM_WININICHANGE 26
= CallFn(PostMessage,HWND_BROADCAST, WM_WININICHANGE,0,"Windows")
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):
[windows]
device=Canon Bubble-Jet BJ-300,CANON330,LPT2:
.
[devices]
Canon Bubble-Jet BJ-300=CANON330,LPT2:
.
[PrinterPorts]
Canon Bubble-Jet BJ-300=CANON330,LPT1:,15,45,LPT2:,15,100
.
[Canon Bubble-Jet BJ-300,LPT1]
Paper Size=264
Print Quality=180
Y Resolution=180
Brush=1
Intensity=92
Text Quality=4
.
[Canon Bubble-Jet BJ-300,LPT2]
Paper Size=9
Orientation=1
Brush=1
Intensity=92
Default Source=267
Print Quality=360
Y Resolution=360
Paper Length=2050
Paper Width=1500
Text Quality=1
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...
Funktionen und Strukturen des GDI
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:
Ausgabefunktion >> Device Context
>> Windows Gerätetreiber
>> physikalisches Ausgabegerät
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.
Der (Drucker) DeviceContext
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.
Anmelden eines Device Contexts
Die folgenden Zeilen zeigen die "API"-Syntax der CreateDC - Funktion:
HDC CreateDC(lpszDriver, lpszDevice, lpszOutput, lpvInitData)
/* Funktionsaufruf */
1.) LPCSTR lpszDriver; /* Adresse des Teiber-Namen (driver name)*/
2.) LPCSTR lpszDevice; /* Adresse des Device-Namen (device name)*/
3.) LPCSTR lpszOutput; /* Adresse des Datei- oder Port-Namen
(port name)*/
4.) const void FAR* lpvInitData; /* Adresse mit Initialisierungs-
Daten (init data)*/
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:
m.LnCreateDC = RegFn("CreateDC","@C@C@CC","I")
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:
m.LcDriver = "BJET"+CHR(0)
m.LcDevice = "Canon Bubble-Jet 300"+CHR(0)
m.LcPort = "LPT2"+CHR(0)
m.LnHDC = CallFn(LnCreateDC,@LcDriver,@LcDevice,@LcPort,0)
Da lt. SDK-Referenz alle String-Pointer als Parameter vom Typ LPCSTR vereinbart werden, wird auch folgende Anmeldung durchaus funktionieren:
m.LnCreateDC = RegFn("CreateDC","CCCC","I")
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:
m.LnHDC = CallFn(LnCreateDC,"BJET","Canon Bubble-Jet 300","LPT2",0)
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).
Die Kontrollstruktur DEVMODE
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:
typedef struct tagDEVMODE { /* dm */
char dmDeviceName[CCHDEVICENAME]; /* CCHDEVICENAME :=
derzeit 32 Zeichen (Bytes) */
UINT dmSpecVersion; /* 2 Bytes */
UINT dmDriverVersion; /* 2 Bytes */
UINT dmSize; /* 2 Bytes */
UINT dmDriverExtra; /* 2 Bytes */
DWORD dmFields; /* 4 Bytes */
int dmOrientation; /* 2 Bytes */
int dmPaperSize; /* 2 Bytes */
int dmPaperLength; /* 2 Bytes */
int dmPaperWidth; /* 2 Bytes */
int dmScale; /* 2 Bytes */
int dmCopies; /* 2 Bytes */
int dmDefaultSource; /* 2 Bytes */
int dmPrintQuality; /* 2 Bytes */
int dmColor; /* 2 Bytes */
int dmDuplex; /* 2 Bytes */
int dmYResolution; /* 2 Bytes */
int dmTTOption; /* 2 Bytes */
} DEVMODE; /* ergibt zusammen 68 Bytes für zu erstellenden String */
Dabei speichern die "Members" (Mitglieder) der Struktur folgende Informationen:
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"
#DEFINE DM_ORIENTATION 1 /* 0x0000001L */
#DEFINE DM_PAPERSIZE 2 /* 0x0000002L */
#DEFINE DM_PAPERLENGTH 4 /* 0x0000004L */
#DEFINE DM_PAPERWIDTH 8 /* 0x0000008L */
#DEFINE DM_SCALE 16 /* 0x0000010L */
#DEFINE DM_COPIES 256 /* 0x0000100L */
#DEFINE DM_DEFAULTSOURCE 512 /* 0x0000200L */
#DEFINE DM_PRINTQUALITY 1024 /* 0x0000400L */
#DEFINE DM_COLOR 2048 /* 0x0000800L */
#DEFINE DM_DUPLEX 4096 /* 0x0001000L */
#DEFINE DM_YRESOLUTION 8192 /* 0x0002000L */
#DEFINE DM_TTOPTION 16384 /* 0x0004000L */
Tabelle "Bin-Nummern"
#define DMBIN_FIRST 1
#DEFINE DMBIN_UPPER 1
#DEFINE DMBIN_ONLYONE 1
#DEFINE DMBIN_LOWER 2
#DEFINE DMBIN_MIDDLE 3
#DEFINE DMBIN_MANUAL 4
#DEFINE DMBIN_ENVELOPE 5
#DEFINE DMBIN_ENVMANUAL 6
#DEFINE DMBIN_AUTO 7
#DEFINE DMBIN_TRACTOR 8
#DEFINE DMBIN_SMALLFMT 9
#DEFINE DMBIN_LARGEFMT 10
#DEFINE DMBIN_LARGECAPACITY 11
#DEFINE DMBIN_CASSETTE 14
#DEFINE DMBIN_LAST 14
Tabelle "Die gebräuchlichsten Papiergrößen-Nummern" (nicht vollständig)
#DEFINE DMPAPER_FIRST 1
#DEFINE DMPAPER_LETTER 1 /* Letter 8 1/2 x 11 in */
#DEFINE DMPAPER_LETTERSMALL 2 /* Letter Small 8 1/2 x 11 in */
#DEFINE DMPAPER_TABLOID 3 /* Tabloid 11 x 17 in */
#DEFINE DMPAPER_LEDGER 4 /* Ledger 17 x 11 in */
#DEFINE DMPAPER_LEGAL 5 /* Legal 8 1/2 x 14 in */
#DEFINE DMPAPER_STATEMENT 6 /* Statement 5 1/2 x 8 1/2 in */
#DEFINE DMPAPER_EXECUTIVE 7 /* Executive 7 1/4 x 10 1/2 in */
#DEFINE DMPAPER_A3 8 /* A3 297 x 420 mm */
#DEFINE DMPAPER_A4 9 /* A4 210 x 297 mm */
#DEFINE DMPAPER_A4SMALL 10 /* A4 Small 210 x 297 mm */
#DEFINE DMPAPER_A5 11 /* A5 148 x 210 mm */
#DEFINE DMPAPER_B4 12 /* B4 250 x 354 */
#DEFINE DMPAPER_B5 13 /* B5 182 x 257 mm */
#DEFINE DMPAPER_FOLIO 14 /* Folio 8 1/2 x 13 in */
#DEFINE DMPAPER_QUARTO 15 /* Quarto 215 x 275 mm */
#DEFINE DMPAPER_10X14 16 /* 10x14 in */
#DEFINE DMPAPER_11X17 17 /* 11x17 in */
#DEFINE DMPAPER_NOTE 18 /* Note 8 1/2 x 11 in */
#DEFINE DMPAPER_ENV_9 19 /* Envelope #9 3 7/8 x 8 7/8 */
#DEFINE DMPAPER_ENV_10 20 /* Envelope #10 4 1/8 x 9 1/2 */
#DEFINE DMPAPER_ENV_11 21 /* Envelope #11 4 1/2 x 10 3/8 */
#DEFINE DMPAPER_ENV_12 22 /* Envelope #12 4 \276 x 11 */
#DEFINE DMPAPER_ENV_14 23 /* Envelope #14 5 x 11 1/2 */
#DEFINE DMPAPER_CSHEET 24 /* C size sheet */
#DEFINE DMPAPER_DSHEET 25 /* D size sheet */
#DEFINE DMPAPER_ESHEET 26 /* E size sheet */
#DEFINE DMPAPER_ENV_DL 27 /* Envelope DL 110 x 220mm */
#DEFINE DMPAPER_ENV_C5 28 /* Envelope C5 162 x 229 mm */
#DEFINE DMPAPER_ENV_C3 29 /* Envelope C3 324 x 458 mm */
#DEFINE DMPAPER_ENV_C4 30 /* Envelope C4 229 x 324 mm */
#DEFINE DMPAPER_ENV_C6 31 /* Envelope C6 114 x 162 mm */
#DEFINE DMPAPER_ENV_C65 32 /* Envelope C65 114 x 229 mm */
#DEFINE DMPAPER_ENV_B4 33 /* Envelope B4 250 x 353 mm */
#DEFINE DMPAPER_ENV_B5 34 /* Envelope B5 176 x 250 mm */
#DEFINE DMPAPER_ENV_B6 35 /* Envelope B6 176 x 125 mm */
#DEFINE DMPAPER_ENV_ITALY 36 /* Envelope 110 x 230 mm */
#DEFINE DMPAPER_ENV_MONARCH 37 /* Envelope Monarch 3.875 x 7.5 in */
#DEFINE DMPAPER_ENV_PERSONAL 38 /* 6 3/4 Envelope 3 5/8 x 6 1/2 in */
#DEFINE DMPAPER_FANFOLD_US 39 /* US Std Fanfold 14 7/8 x 11 in */
#DEFINE DMPAPER_FANFOLD_STD_GERMAN 40/* German Std Fanfold 8 1/2 x 12 in */
#DEFINE DMPAPER_FANFOLD_LGL_GERMAN 41/* German Legal Fanfold 8 1/2 x 13 */
#DEFINE DMPAPER_LAST 41
#DEFINE DMPAPER_USER 256
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
ORIENTATION 0x1 AND 0x6603 = 1 X
PAPERSIZE 0x2 AND 0x6603 = 1 X
PAPERLENGTH 0x4 AND 0x6603 = 0
PAPERWIDTH 0x8 AND 0x6603 = 0
SCALE 0x10 AND 0x6603 = 0
COPIES 0x100 AND 0x6603 = 0
DEFAULTSOURCE 0x200 AND 0x6603 = 512 X
PRINTQUALITY 0x400 AND 0x6603 = 1024 X
COLOR 0x800 AND 0x6603 = 0
DUPLEX 0x1000 AND 0x6603 = 0
YRESOLUTION 0x2000 AND 0x6603 = 8192 X
TTOPTION 0x4000 AND 0x6603 = 16384 X
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:
Auszug aus der DEVMODE-Struktur - > Einträge unter [Canon Bubble-
Jet BJ-300,LPT2] in WIN.INI
int dmOrientation - > Orientation=1
int dmPaperSize - > Paper Size=9
int dmPaperLength - > Paper Length=2050
int dmPaperWidth - > Paper Width=1500
int dmScale
int dmCopies
int dmDefaultSource - > Default Source=267
int dmPrintQuality - > Print Quality=360
int dmColor
int dmDuplex
int dmYResolution - > YResolution=360
int dmTTOption
Device- Brush=1
spezifischer => Intensity=92
Datenblock Text Quality=1
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
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:
m.LnLoadLibrary = RegFn("LoadLibrary","@C","I") && "DLL-Bibliothek laden" - Funktion anmelden
m.LcDrvFileName = "BJET.DRV"+CHR(0) && hier z.B. alter BubleJet-Treiber
m.LnInstHdl = CALLFN(m.LnLoadLibrary,@m.LcDrvFileName)
IF m.LnInstHdl < 33 && Ist Fehler aufgetreten ?
DO CASE && Auswerten! Es sind nicht alle Nummern belegt...
CASE m.LnInstHdl = 0
WAIT WINDOW "Nicht genug Hauptspeicher, Datei korrupt oder ungültige Zuweisung" TIMEOUT 5
CASE m.LnInstHdl = 2
WAIT WINDOW "Datei nicht gefunden" TIMEOUT 5
CASE m.LnInstHdl = 3
* u.s.w.
ENDCASE
ENDIF
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:
m.LnGetProcAddress = RegFn("GetProcAddress","I@C","I")
&& Funktions-Einsprungadresse in DLL ermitteln
m.LcDevModeName = "EXTDEVICEMODE"
m.LnDevModeAdd = CALLFN(GetProcAddress,m.LnInstHdl,@m.LcDevModeName)
&& <m.LnInstHdl> aus vorangeg. Beispiel
IF m.LnDevModeAdd = 0
WAIT WINDOW TIMEOUT 10 "Aktueller Druckertreiber nicht
EXTDEVICEMODE-fähig!"
RETURN
ELSE
WAIT WINDOW " Einsprungadresse gefunden an:
"+ALLTRIM(STR(m.LnDevModeAdd,10,0))
ENDIF
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:
int ExtDeviceMode(hwnd, hDriver, lpdmOutput, lpszDevice, lpszPort,
lpdmInput, lpszProfile, fwMode)
1.) HWND hwnd; /* Fenster-Handle des Elternfensters der
Druckerdialog-Box*/
2.) HANDLE hDriver; /* Treiber-Handle, das von LoadLibrary()
geliefert wurde */
3.) LPDEVMODE lpdmOutput; /* DEVMODE-Struktur für Rückgabe von
Einstellungen */
4.) LPSTR lpszDevice; /* Zeiger auf Device-Name */
5.) LPSTR lpszPort; /* Zeiger auf Port-Name */
6.) LPDEVMODE lpdmInput; /* DEVMODE-Struktur für Vorgabe von
Einstellungen */
7.) LPSTR lpszProfile; /* Zeiger auf Initialisierungs-Datei */
8.) WORD fwMode; /* Kennung für Funktions-Verhalten */
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:
Tabelle: Steuerwerte der ExtDeviceMode-Funktion
#DEFINE DM_IN_BUFFER 8 /* schreibt angeforderte Werte der momentanen
Treibereinstellungen in Vorgabe-DEVMODE */
#DEFINE DM_IN_PROMPT 4 /* Zeigt die Setup-Dialogbox des
Druckertreibers an */
#DEFINE DM_OUT_BUFFER 2 /* Schreibt die momentanen Treibereinstellungen
in Rückgabe-DEVMODE */
#DEFINE DM_OUT_DEFAULT 1 /* Schreibt die momentanen Treibereinstellungen
permanent in INI-Datei */
#DEFINE dm_get_size 0 /* ExtDeviceMode gibt die wirklich benötigte
DEVMODE-Gesamtgröße zurück */
So gehen Sie weiter im Programm vor:
m.lcDrvFileName = "BJET.DRV"+CHR(0) && als Beispiel hier
DOS-Treibername für Canon BJ300
* Jetzt EXTDEVICEMODE(hWnd, hDriver, Output, DeviceName, PortName,
Input, NULL, FlagWert) für Sonderfall "WIN.INI" registrieren:
m.LnExtDM1 = REGFN("EXTDEVICEMODE","II@C@C@C@CCI","I",m.lcDrvFileName)
* Jetzt EXTDEVICEMODE(hWnd, hDriver, Output, DeviceName, PortName, Input,
IniName, FlagWert) für alle INI-Dateien registrieren:
m.LnExtDM2 = REGFN("EXTDEVICEMODE","II@C@C@C@C@CI","I",m.lcDrvFileName)
m.LnHwndFPW = MAINHWND() && Foxtools.Fll - Funktion
* <m.LnInstHdl > ist Instanz-Handle des geladenen Treibers
m.LnOutSpace = 68 && Rückgabe der ExtDeviceMode-Funktion in Hauptaufgabe
m.outDEVMODE = REPLICATE(CHR(0),m.LnOutSpace) && Rückgabe - Struktur
m.inDEVMODE = REPLICATE(CHR(0),m.LnOutSpace) && Vorgabe - Struktur
m.Lcdevice = "Canon Bubble-Jet BJ-300"+CHR(0) && Das Anhängen der CHR(0)
an Device-Name kann entfallen
m.LcPort = "LPT1"+CHR(0) && Port-Name
* Abrufen der korrekten Größen für die DEVMODE Struktur-Strings
lnOutSpace = CALLFN(m.LnExtDM1,m.LnHwndFPW,LnInstHdl,@outDEVMODE,;
@LcDevice,@LcPort,@inDEVMODE,0,0)
m.outDEVMODE = REPLICATE(CHR(0),m.LnOutSpace)
&& Rückgabe - Struktur anpassen
m.inDEVMODE = REPLICATE(CHR(0),m.LnOutSpace) && Vorgabe - Struktur anpassen
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:
m.LnFlagwert = DM_IN_BUFFER + DM_IN_PROMPT + DM_OUTBUFFER
m.LnOk = CALLFN(m.LnExtDM1,m.LnHwndFPW,LnInstHdl,@outDEVMODE,;
@LcDevice,@LcPort,@inDEVMODE,0,m.LnFlagwert)
#DEFINE IDOK 1
#DEFINE IDCANCEL 2
IF m.LnOk = IDCANCEL && Anwender hat den Dialog mit <Abbruch> verlassen
outDEVMODE = inDEVMODE && bei Abbruch steht "unsinniges Zeug"
im Rückgabe-String
ENDIF
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:
m.LnFlagwert = DM_IN_BUFFER + DM_IN_PROMPT + DM_OUTDEFAULT
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().