von Rainer Becker
Seit der Übernahme des Frameworks Visual Extend durch die dFPUG c/o ISYS GmbH erstelle ich mehr und mehr Anwendungen mit VFX. Mittlerweile sogar fast ausschließlich, da sich diese Vorgehensweise in vielerlei Hinsicht bewährt hat. Unter anderem durch schnellere Abwicklung und bessere Kalkulierbarkeit von Projekten sowie regelmässigere Lieferung von Updates im Rahmen von Wartungsverträgen mit niedrigerem Aufwand.
Im Rahmen der praktischen Arbeit sammeln sich eine Vielzahl von kleinen Hilfsroutinen und nützlichen Funktionen. Im gedruckten Handbuch zu VFX 8.0 hatten wir dazu eine Tipps & Tricks-Sammlung von Stefan Zehner veröffentlicht. Das gedruckte Handbuch zu VFX 9.0 wurde aber wesentlich umfangreicher, so daß für Anhänge leider kein Platz mehr war. In diesem Beitrag deshalb eine separate Tipps & Tricks-Sammlung zu VFX 9.0 und früher.
Und damit Besucher unserer Website anlässlich der neuen Version VFX 9.5 auch auf dieser Seite etwas neues vorfinden, wurden ein paar Absätze an weiteren kleinen Tipps & Tricks am Ende hinzugefügt. Weitere Tipps und Tricks zu VFX 10.0 folgen in einiger Zeit...
Bestimmt ist es Ihnen auch schon einmal passiert. In der Headerdatei USERDEF.H stehen die benutzerdefinierten Konstanten, die von VFX nicht überschrieben werden wie zum Beispiel:
#DEFINE INTROFORM_LOC && Startbild für Splash-Formular #DEFINE DESKTOP_LOC && Hintergrundbild für den Desktop
Aber nach Änderung der Bildnamen erscheinen weiterhin die alten Bilder. Da hilft auch das mehrfache komplette neu Erstellen des Projektes nichts. Abhilfe schafft hier nur das Löschen aller .FXP-Dateien im PROGRAM-Verzeichnis!
Das war jetzt wirklich kein neuer Tipp, denn dieser Hinweis und einige weitere kurze Hinweise (wie zum Beispiel „Projektpfad immer richtig setzen“ oder, sehr wichtig!, „OneToMany in 1:n-Relationen nicht auf .T. setzen“) finden sich im Kapitel 16.25 im gedruckten VFX9-Handbuch (Seite 236ff.)!
Das nur vorab, aber nun lassen Sie uns mit ein paar Hooks beginnen, die jeder ohne Anpassung von VFX-Basisklassen oder Programmen in seine Anwendung integrieren kann. Hooks sind nämlich ein ganz einfacher Weg, um generisches Verhalten in VFX-Projekten zu integrieren, ohne die Basisklassen von VFX zu verändern und damit mögliche Probleme beim automatischen Update auf das nächste Build zu vermeiden.
Hooks sind eine einfache Möglichkeit, um das Verhalten von Klassen und Formularen von Visual Extend auf den eigenen Bedarf anzupassen, ohne direkt in den Quellcode eingreifen zu müssen. Dadurch entfallen mögliche Probleme bei der Installation neuer Builds oder neuer Versionen des Frameworks, da man keine Codeänderungen nachziehen muß.
An diversen Stellen in den Klassen von Visual Extend werden Hooks aufgerufen und in VFXHook.Prg findet sich der Eventhookhandler zum Platzieren der eigenen Funktionen:
FUNCTION eventhookhandler(tcevent, toobject, toform) LOCAL lcontinue lcontinue = .T. *-- your code goes here RETURN lcontinue ENDFUNC
Diesen rudimentären Handler kann man sich beim Erstellen des ersten Hooks auch gleich noch ein wenig anpassen, um die weitere Programmierung von Hooks zu vereinfachen.
LOCAL lcBaseClass tcEvent = UPPER(ALLTRIM( tcEvent )) WITH toObject lcBaseClass = UPPER( .baseclass )
Damit entfällt bei allen weiteren Hooks das Beachten der exakten Schreibweise des auslösenden Events (z.B. „OnPrint“) für den Hook sowie ggf. der exakten Schreibweise der Basisklasse (z.B. „Textbox“) der aufrufenden Klasse. Außerdem kann man das aktuelle Objekt durch die Änderung einfach mit einem „.“ am Zeilenanfang referenzieren. Noch weiter vereinfachen kann man sich die weitere Hookprogrammierung durch ein geschachteltes CASE-Statement folgender Art:
DO CASE CASE tcEvent=="INIT" DO CASE CASE INLIST( lcBaseclass, ; "TEXTBOX", "COMMANDBUTTON", ; "LABEL", "COMBOBOX" )
Auf der ersten Ebene wird das Ereignis abgefragt und auf der zweiten Ebene die entsprechende Basisklasse. Natürlich könnte man es auch umgekehrt staffeln, aber oft reagiert eine Gruppe von Klassen auf gleiche Art und Weise auf ein Ereignis.
Gut verwenden kann man Hooks für das generelle Einstellen von Eigenschaften von Steuerelementen im Init. Dadurch muss man die Basisklassen nicht anfassen. Zum Beispiel Abschalten des Tabstops für Textboxen und Commandbuttons, Einschalten von Hyperlinks in Editboxen, Abschalten der Größenänderbarkeit von Zeilen und Headern in Grids sowie schönere Darstellung von Containern:
IF lcbaseclass =="TEXTBOX" AND .readonly .tabstop = .F. ENDIF IF lcbaseclass =="COMMANDBUTTON" .tabstop = .F. .caption = STRTRAN( .caption, "\<", "" ) *-- Achtung: Das schaltet Hotkeys ab... ENDIF CASE lcbaseclass =="EDITBOX" .enablehyperlinks = .T. CASE lcbaseclass =="GRID" .allowrowsizing=.F. .allowheadersizing=.F. .recordmark = .F. CASE lcbaseclass =="CONTAINER" .style = 3 && -Themed IF UPPER( .name ) != "RECORD" .specialeffect = 1 && -sunken ENDIF
Gerne vergisst man auch mal das Eintragen von wahlweise einem Tooltip-Text oder einem Statusbar-Text, was mit folgenden zwei Zeilen erledigt ist (zumindest sofern nur ein Text von beiden fehlt):
.tooltiptext = EVL( .tooltiptext , .statusbartext ) .statusbartext = EVL( .statusbartext, .tooltiptext )
Und gerne schalte ich auch mal Elemente temporär ab, ohne alle notwendigen Eigenschaften dafür alle einzeln umzusetzen – es wird einfach die Schriftart auf durchgestrichen geändert und die folgenden Zeilen erledigen den Rest:
IF .FontStrikethru .visible = .F. .enabled = .F. ENDIF
Besonders praktisch finde ich außerdem einen Hook für Comboboxen zur Festlegung der Anzahl der angezeigten Zeilen. Dies ist z.B. besonders praktisch im Filterformular für die Anzeige der auswählbaren Felder, denn der Standard von 7 Elementen ist meist nicht ausreichend. Mit der neuen Funktion SYS(2910) kann man die Anzahl der angezeigten Elemente in Auswahllisten einstellen, aber das bezieht sich nicht auf Comboboxen. Gerade im Filterformular ist das störend, da dort in der aktuellen Version von VFX nicht mit der Tastatur gescrollt werden kann, da direkt in das Wertfeld gesprungen wird. Dies kann man im Formular hardcodiert einstellen:
thisform.grdExpressions.colFields.cmbFields.DisplayCount = lnitem+1
Oder man setzt global über einen Hook die Anzahl der angezeigten Einträge z.B. von 7 auf 15 hoch. Wesentlich höher sollte man den Wert nicht setzen, da sonst die Combobox nach oben oder unten aus dem Bildschirm wandert.
CASE lcbaseclass =="COMBOBOX" IF .DisplayCount = 0 .DisplayCount = 15 ENDIF
Wenn der Wert höher als die Anzahl der anzuzeigenden Elemente sein sollte, stört dies nicht weiter. Man kann DisplayCount bei einer Combobox mit 10 Elementen auch auf 99 setzen, ohne das Fehler auftreten oder mehr als die 10 vorhandenen Elemente gezeigt würden.
Komplizierter werden die Hookdefinitionen natürlich, sobald es sich um die Einstellung von Eigenschaften handelt, die nicht schon in der Basisklasse vorhanden sind, sondern erst ab einer bestimmten Subklasse in der Vererbungshierarchie. Da muß man natürlich erstmal prüfen, ob die Eigenschaft überhaupt vorhanden ist, um Fehlermeldungen zu vermeiden. Ein Beispiel dafür wäre die zentrale Einstellung der Farben von Steuerelementen für benötigte Felder (.crequiredfields) in Visual Extend ab der Version 9.0 statt individuell Einstellung in jeder Maske wie folgt:
IF tcevent="INIT" AND lcbaseclass="FORM" AND ; pemstatus( toobject, "crequiredfieldfailureprops", 5) .crequiredfieldfailureprops = ; EVL(.crequiredfieldfailureprops , ; "backcolor=rgb(255,255,0)") .crequiredfieldinitprops = ; EVL(.crequiredfieldinitprops ,; "backcolor=rgb(255,255,255)") ENDIF
Man beachte insbesondere die Verwendung der Funktion EVL, um zu vermeiden, das im Formular bereits vorhandene abweichende Definitionen überschrieben werden. EVL eignet sich auch besonders für die Festlegung von Defaultwerten für übergebene Parameter in Methoden und Funktionen. Aufwändiger zu schreiben aber im Ablauf evtl. performanter wäre eine IF-ENDIF-Prüfung auf EMPTY().
Aber lösen wir doch einmal ein kompliziertes Problem! Ich möchte z.B. gerne aus
Childgrids in einem VFX-cOneToMany-Formular auf Doppelklick oder Returntaste
korrespondierende Childformulare aufrufen. Sofern ich über den Parent/Child-Builder
die entsprechenden Child-Formulare verknüpft habe, müsste ich nur die
Eintragsnummer übergeben und mehr wäre nicht zu tun. Wenn, ja wenn, die
VFX-OnKeyEnter-Methode des VFX-Childgrids denn genauso aufgerufen werden würde,
wie dies in den normalen VFX-Grids passiert. Dies ist nur leider nicht der Fall!
Wollte ich es auf die abwärtskompatible Art von VFX tun, müsste ich nunmehr in
sämtliche Textboxen entsprechenden Aufrufcode platzieren. Das ist für die
Builder einfach, da die einfach die Vorlage aus der VFXCODE.DBF kopieren, aber
manuell ist das doch etwas zu aufwändig – insbesondere bei der Pflege, wenn
weitere Spalten hinzukommen. Man könnte das leicht bei einer neuen Spalte
vergessen…
Deshalb nehmen wir lieber die neue BINDEVENT-Funktion von Visual FoxPro und
platzieren diese in einem Hook. Allerdings möchten wir die Funktionalität nicht
unbedingt bei jedem Childgrid, weshalb wir die Eigenschaft TAG mit irgendeinem
Wert belegen (in diesem Fall die Klausel „ONMORE“). Das sieht dann wie folgt
aus:
IF tcevent="INIT" AND lcbaseclass="GRID" ; AND UPPER(.class) = "CCHILDGRID" ; AND "ONMORE" $ UPPER(.tag ) LOCAL lnCounter FOR lnCounter = 1 TO.ColumnCount =bindevent(.columns(lnCounter).text1, ; "KeyPress", toObject, "onkeyenter") =bindevent(.columns(lnCounter).text1, ; "DblClick", toObject, "onkeyenter") NEXT ENDIF
Und schon leiten sämtliche Textboxen der markierten Childgrids Ihre Keypress und
DblClick-Events an die OnKeyEnter-Methode des Childgrids weiter. Statt an
OnKeyEnter kann man es natürlich auch an die Methode DblClick binden, das ist
Geschmackssache außer das die Grid.DblClick-Methode natürlich auch direkt durch
den Anwender aufrufbar wäre.
In der Methode DblClick or OnKeyEnter müssen wir unbedingt eine
LPARAMETER-Anweisung mit einbauen, damit das gebundene Ereignis auch empfangen
werden kann! Desweiteren wollen wir für den Fall der Returntaste den Tastendruck
mit CLEAR TYPEAHEAD verwenden und unsere eigene Funktion aufrufen wie folgt:
LPARAMETERS nKeyCode, nShiftAltCtrl CLEAR TYPEAHEAD THISFORM.OnMore( <n> )
Das ist jetzt allerdings eine etwas sehr minimalistische Variante. Leider bietet
das VFX-Childgrid noch keine leere Methode mitsamt Hook für diesen Fall an (was
aber verbessert werden wird), so dass wir an dieser Stelle ohnehin keinen Hook
einsetzen können. Also müssen wir in jedem Childgrid die entsprechende Methode
ausprogrammieren wie folgt:
LPARAMETERS nKeyCode, nShiftAltCtrl IF THISFORM.nformstatus=0 or THIS.ReadOnly CLEAR TYPEAHEAD ENDIF =DODEFAULT() IF ( THISFORM.nformstatus=0 OR THIS.ReadOnly ) AND NOT EOF( THIS.RecordSource ) THISFORM.OnMore( 1 ) ENDIF
Das Childformular wird nur aufgerufen, wenn die Maske nicht im Bearbeitenmodus ist bzw. wenn das Childgrid ohnehin Readonly ist und wenn ausserdem überhaupt Daten im Childgrid vorhanden sind. Im Beispiel wird die OnMore-Methode mit dem Parameter 1 aufgerufen. In einer separaten Methode am Childgrid könnte man das natürlich auch generisch ausformulieren und den Parameter ebenfalls aus der Eigenschaft .Tag oder einer zusätzlich definierten Eigenschaft auslesen. Aber zumindest bis hierhin hat uns ein einfacher Hook gebracht.
Wo wir gerade beim Wiederverwenden von OnMore-Funktionen sind, noch ein anderes kleines Beispiel. Die Auswahlmaske für Zusatzfunktionen auf der Funktionstaste F6 ist zwar sehr hilfreich, aber manche mögen es gerne moderner und hätten Zusatzfunktionen gerne auf der rechten Maustaste, auch wenn man den OnMore-Dialog etwas modernisieren kann (z.B. Borderstyle=Fixed Dialog, Closable=.T., Maske größer). Nachfolgend deshalb eine Funktion die auf Formularebene (Objektreferenz ToForm) die alle OnMore-Funktionen als Rechtsklick-Menü zur Verfügung stellt:
FUNCTION onmore2shortcut LPARAMETERS ToForm LOCAL lnCount, lcCmd, lnMax LOCAL ARRAY laArray(1) lnCounter = 0 lcCmd = "" lnMax = TOFORM.onmore( -1, @laArray) IF lnMax > 0 DEFINE POPUP onmore SHORTCUT RELATIVE ; FROM MROW(),MCOL() FOR lnCounter = 1 to lnMax DEFINE BAR lnCounter OF onmore ; PROMPT laArray [lnCounter,1] ; MESSAGE laArray [lnCounter,2] lcCmd = "ON SELECTION BAR lnCounter" + ; "OF onmore =_screen.activeform.onmore(" + ; LTRIM(STR(lnCounter)) +")" &lcCmd NEXT ACTIVATE POPUP onmore ENDIF ENDFUNC
Dieses Beispiel basiert allerdings darauf, dass die OnMore-Methode bei Übergabe
von -1 als Aufrufparameter als Rückgabewert die Anzahl der verfügbaren
Funktionen liefert und im zweiten Parameter per Referenz das Array zur Verfügung
stellt. Dies macht es derzeit leider noch etwas problematisch, dieses Beispiel
zusammen mit dem Parent/Child-Builder einzusetzen.
Hinweis: Bestimmte Einträge in der OnMore-Methode kann man mit einer Abfrage auf
den Übergabeparameterwert -1 klammern und somit nicht zur Verfügung stellen,
wenn die Rechtsklickfunktion das Array abholt. Dann muß man allerdings mit einer
Zählvariablen a la lnCounter=lnCounter+1 arbeiten und diese als Offset im Array
benutzen, statt feste Arraydimensionen. Dies sollte man aber sowieso tun, weil
man dann problemlos die Reihenfolge von Einträgen ändern kann (steht übrigens
auch auf unserer Wunschliste…).
Zusätzlich ist es sinnvoll, das Rightclick-Event von Labels und Container an den
Parent weiterzuleiten sowie von Pages an den Parent.Parent.Rightclick bzw.
direkt an Thisform.Rightclick, damit bei diesen Elementen ebenfalls das
Rechtsklickmenü des Formulars zur Verfügung steht.
Kommen wir zu einem weiteren ganz anders gelagerten Beispiel: Wenn man in der
Eigenschaft .cReportName eines beliebigen VFX-Datenformulars einen
Standardbericht einträgt, wird dieser IMMER verwendet und als Liste ausgedruckt.
Damit steht die Selbsterstellungsmöglichkeit von Listen leider nicht mehr zur
Verfügung. Manchmal möchte man aber gerne beides haben, einen Standardbericht
und den VFX-Listendesigner.
Jetzt könnte man in der OnPrint-Methode einfach folgendes schreiben:
LPARAMETERS tlpreview IF this.pgfpageframe.activepage#this.npagelist this.creportname="meinsuperbericht" ELSE this.creportname="" ENDIF RETURN DODEFAULT(m.tlpreview)
Wenn die Suchseite aktiv ist, erscheint daraufhin der
Standard-VFX-Listendesigner. Auf allen anderen Seiten hingegen wird direkt der
Standardbericht für das Formular gedruckt. Das verstehen sogar einfache
Anwender. Damit hätten wir eine erste schnelle Lösung.
Nur für Entwickler ist das eigentlich nicht ganz so toll. Denn leider müsste man
diesen Code in sämtliche Formulare in die OnPrint-Methode kopieren, welche einen
Standardbericht zur Verfügung stellen sollen. Dies wäre für die Wartung sehr
ärgerlich, insbesondere wenn man auf die zusätzliche Idee kommt, dass man auf
den normalen Bearbeitungsseiten mit dem Standardbericht nur den aktuellen
Datensatz ausdrucken möchte (z.B. Bestellung, Rechnung usw.).
Deshalb lösen wir das Problem über einen selbstdefinierten Hook, der auf das
Ereignis OnPrint bzw. OnPostPrint lauert. Sie müssen allerdings am Ende der
OnPostPrint-Methode der cDataForm-Klasse noch folgenden Hookaufruf einfügen:
*-- Ergänzung cDataForm.OnPrint: OnPostPrint-Hook IF THISFORM.lusehook LOCAL luhookvalue luhookvalue = THISFORM.oneventhook(; "OnPostPrint",THIS,THISFORM) DO CASE CASE VARTYPE(luhookvalue)="L" IF !luhookvalue RETURN .T. ENDIF CASE VARTYPE(luhookvalue)="N" RETURN luhookvalue=0 ENDCASE ENDIF
In einem zukünftigen Build von Visual Extend wird dieser weitere Hook bereitstehen. Schauen Sie also erst nach, ob die obigen Zeilen in Ihrer Version nicht ohnehin enthalten sind. Falls nicht, ist das Hinzufügen kein Problem, da es in einem weiteren Build ja sowieso dazukommen wird…
Und hier der Quellcode für den eigentlichen Hook, der die gesamte Arbeit für uns übernimmt. Sofern in der VFX-Standardeigenschaft .cReportName ein Vorlagebericht definiert ist, wird dieser von Einzelseiten aus für den aktuellen Satz gedruckt; in der Übersicht hingegen steht weiterhin der VFX-Listendesigner zur Verfügung. Benötige Eigenschaften werden notfalls schnell zum laufenden Formular addiert:
CASE lcBaseclass == "FORM" AND ; ( tcevent == "ONPRINT" or ; tcevent == "ONPOSTPRINT" ) *-- Ausführung wenn man drucken darf und kann IF NOT .lCanPrint OR .lEmpty lcontinue = .F. ELSE IF tcevent == "ONPRINT" *-- Erster Eventaufruf *-- Fehlende Eigenschaften anlegen IF NOT Pemstatus( toObject, "cPrintReportName", 5) .addproperty( "cPrintReportName", .creportname ) ENDIF IF NOT Pemstatus( toObject, "cPrintFilterSave", 5) .addproperty( "cPrintFilterSave", FILTER() ) ENDIF *-- Druck aus Übersicht *-- Defaultreport abschalten IF .pgfpageframe.activepage = .npagelist IF NOT EMPTY( .creportname ) .cPrintReportName = .creportname .creportname = "" ENDIF ELSE *-- Druck aus Default *-- Filter und ggf. Defaultreport setzen IF NOT EMPTY( .cPrintReportName ) .creportname = .cPrintReportName ENDIF .cPrintFilterSave = FILTER() LOCAL lnRecno lnRecno = STR(RECNO()) SET FILTER TO RECNO() = &lnRecno ENDIF ELSE && tcevent == "ONPOSTPRINT" *-- Nach Druck aus Übersicht *-- Defaultreport wiederherstellen IF .pgfpageframe.activepage = .npagelist IF NOT EMPTY( .cPrintReportName ) .creportname = .cPrintReportName ENDIF ELSE *-- Nach Druck aus Detail *-- Filter wiederherstellen IF EMPTY( .cPrintFilterSave ) SET FILTER TO ELSE LOCAL lcFilter lcFilter = .cPrintFilterSave .cPrintFilterSave = "" SET FILTER TO &lcFilter ENDIF ENDIF ENDIF ENDIF
Wie Sie sehen, kann man mit einem etwas komplexeren aber immer noch nicht wirklich komplizierten Hook das Standardverhalten einer mit Visual Extend generierten Anwendung in ganz erheblichem Maße anpassen! Kopieren Sie einfach den obigen Code in Ihren Eventhandler und schon können auch Sie diese Funktionalität verwenden.
Comboboxen sind in Visual FoxPro stellenweise etwas trickreich. Als Vorgabe ist
der Style=1 (Dropdown Combo) voreingestellt, der sowohl die Auswahl eines Wertes
aus einer aufklappenden Liste oder die Neueingabe eines Wertes ermöglicht. Neu
eingegebene Werte aber müssen dann in die Auswahlliste transferiert werden,
damit sie bei der nächsten Anzeige des Elements auch noch sichtbar sind. Und
wenn man den Style=2 (Dropdown List) einstellt, kann der Anwender nur noch
Auswählen, was die Combobox unfreiwillig in ein Pflichtfeld wandelt, welches nie
wieder ohne Wert sein kann, sofern man nicht in sämtlichen Listen einen Leerwert
zusätzlich definiert, was sehr unpraktisch sein kann.
Deshalb wollen wir im Rechtsklickmenü der Combobox für diesen Fall eine
zusätzliche Möglichkeit anbieten, den Wert zurückzusetzen. Dafür müssen wir in
die Methode Rightclick der Basisklasse. Unser Fall macht natürlich nur bei
Style=2 einen Sinn. Zusätzlich sollte aber eine selbstdefinierte Eigenschaft
(hier lResetValue) abgefragt werden, da die Funktionalität bei Pflichtfeldern
z.B. unerwünscht wäre.
Nach DEFINE BAR 5 / ON SELECTION BAR 5 fügen wir in der Rightclick-Methode
folgenden Code ein:
WITH THIS IF .style = 2 AND ; .lresetvalue = .T. AND ; NOT EMPTY(.Value ) AND ; NOT thisform.lempty AND ; thisform.lcanedit DEFINE BAR 6 OF shortcut PROMPT "\-" DEFINE BAR 7 OF shortcut PROMPT "Reset Value" ON SELECTION BAR 5 OF shortcut ; lothis.REQUERY() ON SELECTION BAR 7 OF shortcut ; lothis.resetvalue() ENDIF
Und in die neue Methode Resetvalue an der Comboboxbasisklasse kommt dann folgender Codeabschnitt:
WITH THIS .Value = "" .DisplayValue = "" .InteractiveChange() ENDWITH
Wenn man sich im Gotfocus-Event in einer Eigenschaft den DisplayValue beim
Betreten des Feldes in einer Eigenschaft merkt, kann man alternativ oder
zusätzlich das zurücksetzen auf den ursprünglichen Wert anbieten.
Mit ein bißchen leider noch fehlender Verfeinerungsarbeit kann man den ersten
Aufruf natürlich wiederum in einen Hook einbauen, der auf Combobx.Rightclick
reagiert und statt einer Methode eine Funktion aufruft. Den zusätzlichen
Schalter würde man aber vermutlich trotzdem benötigen.
Soviel erstmal zu Hooks. Kommen wir zu einem weiteren typischen Beispiel, welches in praxisnahen Anwendungen immer wieder vorkommt. Wenn der Anwender eine Postleitzahl eintippt, kann man in vielen Fällen daraus problemlos den entsprechenden Ort ableiten ohne dass der Anwender diesen nochmals tippfehlerbehaftet eintippen müsste oder wirklich separate Steuertabellen benötigt werden. IntelliSense oder eine Combobox helfen dabei aber nicht wirklich weiter. Deshalb platziere ich im Valid-Ereignis der PLZ-Textbox folgenden Code:
WITH THIS IF NOT EMPTY(.value ) WITH .Parent.<txtort> IF EMPTY( .Value ) LOCAL lcValue lcValue = getcityforzip( ; <adressen.land>, THIS.Value ) IF NOT EMPTY( lcValue ) .value = lcValue .interactivechange() ENDIF ENDIF ENDWITH ENDIF ENDWITH
Bei Eingabe einer Postleitzahl wird damit mit nachfolgender Funktion nach anderen Datensätzen gesucht, die als PLZ oder als Postfach-PLZ bereits die gleiche PLZ verwenden und einen Ortseintrag haben. Alle anzupassenden Werte stehen in eckigen Klammern.
FUNCTION getcityforzip LPARAMETERS tcCountry, tcZip LOCAL lnSelect, lcReturn, lcAlias lnSelect = SELECT() lcReturn = "" tcCountry = ALLTRIM( tcCountry ) if EMPTY( tcCountry ) tcCountry = <"Deutschland"> ENDIF tcZip = ALLTRIM( tcZip ) IF NOT EMPTY( tcCountry ) AND NOT EMPTY( tcZip ) lcAlias = "findcity4zip" IF NOT USED( lcAlias ) USE <adressen> ALIAS ( lcAlias ) ; IN 0 AGAIN SHARED ENDIF SELECT (lcAlias) LOCATE FOR <land> = tcCountry AND ; <plz> = tcZip AND ; NOT EMPTY( <ort> ) IF FOUND() lcReturn = <ort> ELSE LOCATE FOR <land> = tcCountry AND : <plz_pf> = tcZip AND ; NOT EMPTY( <postfach> ) IF FOUND() lcReturn = <postfach> ENDIF ENDIF SELECT (lnSelect) lcReturn = ALLTRIM( lcReturn ) ENDIF RETURN lcReturn ENDFUNC
Daraus könnte man natürlich auch einen vollständig parametrisierten Funktionsaufruf machen, der im Valid der PLZ-Textbox (oder PLZ-Postfach-Textbox) leicht aufgerufen werden kann. Das wäre allerdings für das Verständnis des Ablaufs wenig hilfreich. Ansonsten benötigt die obige Funktion das Framework VFX nicht sondern kann generell eingesetzt werden.
Zurück zu VFX und zum Thema Berichte: Um die PDF-Ausgabe von Visual Extend auch bei Anwendern installieren zu können, die keinen Internetzugang haben, so dass die automatische Installation nicht funktioniert, gibt es einfache Möglichkeit. Man kann in das Verzeichnis von SYS(2023), normalerweise C:\Dokumente und Einstellungen\<Username>\Lokale Einstellungen\Temp, einfach die entsprechende Installationsdatei von Ghostscript unter ftp://mirror.cs.wisc.edu/pub/mirrors/ghost/AFPL/gs814/gs814w32.exe kopieren. Sofern die Datei dort vorhanden ist, wird seitens VFX der automatische Internetdownload gar nicht erst versucht. Dies beschleunigt natürlich die Installation auch wenn ein Internetzugang tatsächlich verfügbar wäre.
PS: Sofern Sie Ihre VFX-Version lange nicht aktualisiert haben, sollten Sie diesen Pfad in der VFXSYS.DBF im Feld install_gs überprüfen und ggf. mit obiger Angabe aktualisieren!
Sofern ein unterstützer Faxtreiber vorhanden ist, kann man mit folgendem Pseudocode aus VFX heraus einen Bericht an eine Faxnummer versenden:
loFax = NEWOBJECT("cFax") loFax.SendFax(<alias>,<reportname>,<faxnummer>)
Dabei wird für FritzFax ein temporärer Registry-Eintrag mit der Telefonnummer erzeugt und WinFax wird per OLE angesteuert, damit die jeweilige Faxsoftware nicht erneut in einem Dialog nach der Empfängernummer fragt.
Sofern die PDF-Unterstützung konfiguriert ist, können Sie auch einen Bericht per eMail versenden. Leider muss man da etwas mehr wissen als beim reinen Faxversand. Hier ebenfalls nur Pseudocode für das Prinzip und die Ausführung innerhalb von VFX:
*-- Deklarationen lnSelect=select() lcDefault=GetDefaultFolder() lcreportname = <berichtsdatei>+".frx" lcAlias = Alias() loEmail = CreateObject("CEmail") *-- Maildetailsabfrage DO Form vfxEmailDetails WITH ,,loEmail to oEmailDetails lcEmail = oEmailDetails.cEmail lcSubject = oEmailDetails.cSubject lcText = oEmailDetails.cText lcCCRecipient = oEmailDetails.cCCRecipient lcBCCRecipient = oEmailDetails.cBCCRecipient lcFor = [] && ggf. Bericht filtern.. *-- Berichtserstellung und Versand lcFileName = ForceExt(lcreportname, "pdf") loEmail.AddAttachment(lcAlias, lcFileName, lcreportname, lcFor) loEmail.send_email_report(lcEmail, lcSubject, lcText, lcCCRecipient, lcBCCRecipient) *-- Aufräumen SELECT(lnSelect) SET DEFAULT TO (lcDefault)
Weitere Details zur Umsetzung finden Sie in der Methode OnPrint in der Klasse cDataForm.
Sofern man in einer Anwendung nur einsprachig oder mit wenigen Sprachen arbeitet, und seine Projekt-Entwicklungsumgebung sichern oder versenden möchte, empfiehlt sich das Komprimieren der VFXMSG.DBF, da diese eine nicht unbeträchtliche Größe hat. Am einfachsten löscht man den Inhalt der nicht benötigten Sprachspalten mit einem einfachen Replace-Befehl a la
REPLACE ALL esp with "", fre with "", ; ita with "", bul with "", ; gre with "", cze with "", ; nl with "" , por with "", ; ru with "" , fin with "", pl with ""
und führt danach ein PACK MEMO aus. Bei der Installation eines neuen Builds von VFX werden neue Messagetexte natürlich wieder für alle Sprachspalten übernommen und man muss die entsprechenden Befehle erneut absetzen.
Was man unter VFX vor einer Auslieferung der eigentlichen Anwendung tun kann, um Speicherplatz einzusparen, aber gelegentlich gerne vergisst:
Mit dem RuntimeInstaller der dFPUG kann man die Laufzeitumgebung für Visual FoxPro problemlos installieren. Dazu kommen dann noch die lokalisierten Applikationen für Berichtsausgabe, Vorschau und Bearbeitung. Damit wäre alles bis auf die eigentliche Anwendung und die Mitlieferung der VFX.FLL soweit vorbereitet, aber sofern man alle Möglichkeiten von Visual Extend in seiner Applikation verwendet, kommen dann doch noch ein paar weitere Dateien hinzu:
Diese Dateien sind zwar meist auf den Kundenrechnern vorhanden, aber dessen kann man sich nicht sicher sein und kommt deshalb um eine entsprechende Installationsroutine nicht herum.
Sofern man mit dem VFX Help Wizard seine Helpcontext-Ids in allen Formularen gesetzt und aus der generierten und gefüllten VFXHELP.DBF eine Hilfedatei über den HelpWorkshop erstellt hat, liegt zwar eine schön .CHM-Datei vor, aber leider benötigt die generierte Anwendung noch die Dateien Foxhhelp9.exe und foxhhelpps9.dll (gleiches gilt für die Version 8), um die Hilfe aufrufen zu können. Da unter Windows XP der Suchen-Dialog nicht automatisch alle Verzeichnisse durchsucht, schauen Sie am besten direkt unter „C:\Programme\ Gemeinsame Dateien\ Microsoft Shared\ VFP“ nach zwecks Mitlieferung. Dort finden sich übrigens auch die verschiedenen Runtimebibliotheken und die gdiplus.dll. Bitte beachten Sie, dass die beiden Dateien entsprechend registriert werden müssen (regsvr32 foxhhelpps9.dll bzw. foxhhelp9.exe /regserver).
Sofern man Anwendern Berichte für die Bearbeitung freischalten oder aktualisierte Berichte ohne Gesamtauslieferung der kompilierten Anwendung versenden möchte, muß man die Berichtsdateien in der Projektdatei via rechte Maustaste exkludieren. Zumindest aus meiner Sicht „leider“ werden von Visual FoxPro Berichte defaultmäßig mit in die EXE einbezogen und blähen diese wahnsinnig auf. Ausserdem ist keine Änderung durch den Anwender mehr möglich. Am Stück ändern kann man das mit folgenden zwei Zeilen:
use <project>.pjx replace all exlude with .T. for type = "R"
Das Verfahren kann man natürlich mit entsprechend anderer FOR-Klausel auch auf alle anderen Arten von Einträgen in Projektdateien anwenden und es ist nicht spezifisch für Visual Extend.
Und das gilt natürlich für alle weiteren FoxPro-Metadateien wie Formulare (.scx),
Berichte (.frx), Menüs (.mnx) und so weiter, da es ja auch alles nur
DBF-Tabellen sind. Um zum Beispiel einen vom Projekt als fehlerhaft gemeldeten
Eintrag zu suchen, braucht man nur mit USE die Projektdatei öffnen und mit GOTO
auf den entsprechenden Datensatz gehen.
oder bestimmte Wertbelegungen zu ändern. Elemente mit bestimmten Werten in
Eigenschaften hingegen sucht man am besten wie folgt:
use <formular>.scx browse for '<Eigenschaft>="<Wertanfang>' $ properties
Sofern man Klassen umbenennt und über das Codereferenztool nach weiterem Vorkommen des alten Namens zwecks Auffinden von abgeleiteten Klassen sucht, wird man um das Hacken der Metadateien von VFP nicht herumkommen. Im Codereferenztool findet man die Klasse nämlich, aber im Eigenschaftsfenster wird witzigerweise der neue Name angezeigt, obwohl im Namensfeld der Metatabelle immer noch der ursprüngliche Klassenname drinsteht!
Zwecks Erhalt des Microsoft-Logos „Verified for Windows XP“ war es notwendig
verschiedene Speicherpfade von Dateien zu ändern, so auch der INI-Datei von
Visual Extend. Diese findet man nunmehr unter „C:\Dokumente und Einstellungen\
All Users\ Anwendungsdaten\ dFPUG\ Visual Extend\ 9.0“ (oder natürlich 9.5).
Dort kann man übrigens unter [VFX] mit dem Eintrag SPLASHSCREEN=NO den
Startbildschirm von VFX für die Entwicklungsumgebung einfach abschalten – nur falls es jemanden stören
sollte.
In der Visual FoxPro Hilfe findet man eine kurze Erläuterung der SYS-Funktion SYS(602), die den Bildschirmaufbau unter Terminalservern beschleunigen kann. Dort wird darauf hingewiesen, dass man das Vorhandensein einer Terminalserver-Instanz auf dem aktuellen Rechner über OS(10) feststellen kann. Leider sagt einem diese Funktion nicht, ob die aktuelle Anwendung auch tatsächlich in einer Terminalserver-Session läuft. Deshalb empfehlen wir die Verwendung der Windows-Funktion GetSystemMetrics im Hauptprogramm (vfxmain.prg) wie folgt:
DECLARE INTEGER GetSystemMetrics IN WIN32API INTEGER nIndex IF GetSystemMetrics(4096) != 0 =SYS(602,1) *-- ggf. hier: SYS(3050,1 bzw 2, <reduzierter Speicherbedarf) *-- für weniger Speicherbedarf der Anwendung unter Terminalserver ENDIF
Auf dem Terminalserver gibt es darüber hinaus noch eine weitere Problematik. Anwender können sich von einer Session abmelden, ohne die laufende Anwendung zu beenden. Diese läuft dann sprichwörtlich ewig weiter und verhindert exklusiven Zugriff auf Tabellen auch zu nachtschlafender Zeit. Da es natürlich unhöflich ist, so einen Task dann einfach zu beenden, sollte die Anwendung selbst dafür Sorge tragen - und zwar mit Hilfe eines Timers in der Anwendung, der diese z.B. nach einer Stunde Leerlauf zwangsbeendet.
Ein Timer ist leicht an das globale Programmobjekt addiert. Aber so einen Timer muss man natürlich immer wieder zurücksetzen, wenn der Anwender aktiv ist. Dafür bietet sich die VFX-Funktion oneventhookhandler an, die ja fortwährend durchlaufen wird. Damit mir aber nicht meine Entwicklungsumgebung beendet wird, wollte ich dabei den _VFP.StartMode abfragen. Das führt bei einem Aufruf am Anfang des Eventhookhandlers aber plötzlich zu einem OLE-Fehler bei diversen Anwendern! Deshalb die Empfehlung, das in ein seltener aufgerufenes Event zu verschieben und die Version-Funktion zu verwenden wie folgt:
CASE tcEvent=="ONRECORDMOVE" IF TYPE("goprogram")="O" AND NOT ISNULL(goprogram) AND VERSION(2)=0 *_vfp.StartMode != 0 =goprogram.timeout() ENDIF
Die entsprechende Methode muss man sich natürlich noch anlegen, die den Timer jeweils zurücksetzt...
Wenn Visual FoxPro in einem SQL-Statement eine benötigte Tabelle nicht findet, erscheint ein Dateiauswahldialog, in welchem der Anwender wahllos irgendeine Tabelle auswählen oder über einen Button sogar versehentlich löschen kann. Von diesem Dialog fühlen sich nicht nur die meisten Anwender schlicht überfordert, sondern auch Administratoren halten so einen Dialog z.B. auf einem Terminalserver für eher unglücklich.
Zum Glück gibt es dafür die neue SET-Funktion SET TABLEPROMPT OFF. Statt dem Dialog erhält man eine handfeste Fehlermeldung und kann die Anwendung leidlich sauber beenden, statt in eine völlig unberechenbare Situation einer vom Anwender willkürlich ausgewählten Tabelle mit einer Vielzahl von möglichen Folgefehlern zu stolpern.
Im englischen Hilfstext wird es zwar nicht klar gesagt, aber SET TABLEPROMPT ist scoped, d.h. dieser Befehl muss in jeder Datasession neu gesetzt werden. Dies kann man in VFX vor 9.5 in der Funktion formsetup in dem Programm applfunc.prg tun. Ab VFX 9.5 gibt es dafür ganz einfach die Eigenschaft lTableprompt am Applikationsobjekt.
Manchmal dauert der Aufbau eines Formulars im Netzwerk einfach zu lange, weil man vielleicht sehr viele Tabellen öffnen und für diverse Grids positionieren muß. Dabei kommt einem dann der Screen-Refresh heftig in den Weg, da dieser beim Aufbau der Maske möglicherweise mehrfach aufgerufen wird. Marcia Akins und Andy Kramek haben mir dafür nachfolgende Funktion zur Verfügung gestellt, die ich gerne kurz vorstellen möchte.
Statt dem möglicherweise vielfach verschachtelten Aufruf der Lockscreen-Funktion von Visual FoxPro kann man die Windows-Funktion LockWindowUpdate verwenden, um ein Screen-Refresh ganz abzuschalten. Die Funktion muß man in seinem Hauptprogramm (vfxmain.prg) einmal wie folgt deklarieren:
DECLARE INTEGER LockWindowUpdate IN Win32API INTEGER nHandle
In das Init des jeweiligen Formulars kommt der folgende Aufruf:
Thisform.ReallyLockScreen( .T. )
Und in das Activate des jeweiligen Formulars kommen folgende Zeilen:
IF NOT EMPTY( DODEFAULT()) Thisform.ReallyLockScreen( .F. ) ENDIF
Und jetzt müssen wir nur noch die Methode ReallyLockScreen in unserer Formularbasisklasse (cform in vfxobj.vcx oder cdataform in vfxform.vcx) anlegen und mit folgendem Inhalt füllen:
LPARAMETERS tlLock LOCAL lnHWnd *** Now set the Handle to lock according to the parameter lnHWnd = IIF( tlLock, ThisForm.HWnd, 0 ) *** And call the function =LockWindowUpdate(lnHWnd) RETURN
Das lässt sich natürlich in jedes Formular oder jede Formularbasisklasse einbauen und ist nicht VFX-spezifisch. Aber es funktioniert sehr gut, auch wenn es sich nur für wirklich aufwändige Formulare lohnt.
Immer wieder kommt es vor, dass ein Anwender einem einmal eine Tabelle schicken soll und dabei mit den Endungen durcheinander kommt, oder dass ein Anwender Daten unbedingt in Excel haben möchte, aber mit der Bedienung der Anwendung nicht hinreichend klar kommt. Für diese Anwender bzw. diese Fälle platziere ich gerne auf die Schnelle einen kleinen Excel-Button auf dem Formular mit folgendem Code im Click-Event:
LPARAMETERS tcAlias LOCAL lnRecno, lnSelect, lcAlias lnSelect = SELECT() lcAlias = EVL( tcAlias, thisform.cworkalias ) SELECT (lcAlias) lnRecno = RECNO() _vfp.datatoclip( lcAlias, , 3) =ShellExecute(0,"Open","EXCEL.EXE","","",1) WAIT WINDOW "Records copied to clipboard and Excel opened, insert data with CTRL+V." NOWAIT IF lnRecno > 0 AND lnRecno <= RECCOUNT() GOTO (lnRecno) ELSE GO TOP ENDIF SELECT (lnSelect)
Einmal draufklicken und im bereits gestarteten Excel die Daten einfügen, fertig. Den Text für das WAIT WINDOWS muss man noch anpassen oder dafür die weiter unten erwähnte Funktion GETSOUND verwenden. Die Funktion ShellExecute muss natürlich im Hauptprogramm definiert worden sein. Falls dies nicht der Fall sein sollte, folgende Zeilen einkopieren für die sehr praktische Funktion:
DECLARE INTEGER ShellExecute IN SHELL32.DLL ; INTEGER nWinHandle, STRING cOperation, STRING cFileName, ; STRING cParameters, STRING cDirectory, INTEGER nShowWindow
Damit der Button auch schön aussieht und sich im Resizer richtig verhält, stellen wir schnell noch ein paar Eigenschaften wie folgt ein:
Caption = Excel Comment = <NORESIZE> Height = 23 Picture = bitmap\xls.bmp PictureMargin = 2 PicturePosition = 1 PictureSpacing = 0 Width = 60
In der Refresh-Methode müssen wir nur noch dafür sorgen, dass der Button auch nur in sinnvollen Situationen enabled ist (also nicht beim Bearbeiten oder wenn gar keine Daten vorhanden sind):
=DODEFAULT() WITH THISFORM THIS.Enabled = .nFormStatus=0 AND NOT .lEmpty AND NOT EMPTY( .cworkalias ) ENDWITH
Fertig ist der kleine Excel-Export-Button. Einfach auf das Formular kleben, am besten oben rechts neben die integrierte Toolbar, falls vorhanden. Und sofern man nicht die Haupttabelle exportieren möchte, sondern eine andere Tabelle kann man in das Click-Event des Buttons auf der Form einfach DODEFAULT( "<Aliasname>" ) schreiben...
Windows macht immer so hübsche Töne bei bestimmten Ereignissen. Die wollte ich in meiner Anwendung auch unbedingt haben. Also ein Unterverzeichnis Sounds angelegt und die entsprechenden WAV-Dateien von Windows flugs kopiert. Und dann die nachfolgende Funktion GETSOUND in applfunc.prg eingebaut:
LPARAMETERS tcCase, tcMessage, tcTitle LOCAL lcReturn, lcSetBell tcCase = EVL( tcCase, "" ) && gewünschter Sound tcMessage = EVL( tcMessage, "") && optional: anzuzeigender Text tcTitle = EVL( tcTitle, "") && optional: Titel für Messagebo lcSetBell = SET("BELL") DO CASE CASE ".wav" $ tcCase * wav-filename already provided... lcReturn = tcCase CASE tcCase = "login" lcReturn = "Windows XP-Anmeldesound.wav" CASE tcCase = "logout" lcReturn = "Windows XP-Abmeldesound.wav" CASE tcCase = "warning" lcReturn = "Windows XP-Ping.wav" CASE tcCase = "error" lcReturn = "Windows XP-Fehler.wav" CASE tcCase = "critical" lcReturn = "Windows XP-kritischer Fehler.wav" CASE tcCase = "message" lcReturn = "Windows XP-Hinweis.wav" OTHERWISE lcReturn = "Windows XP-Hinweis.wav" ENDCASE
Die Funktion EVL stellt einem netterweise leere Strings bereit, wenn der Parameter gar nicht übergeben wurde. Die Sounds sind in diesem Beispiel natürlich hardcodiert, was man auch generisch über eine Steuertabelle lösen könnte. Alternativ könnte man natürlich die entsprechende Sound-Belegung des aktuellen Windows-Systems abfragen. Aber für eine erste einfach Implementation ist der obige Code völlig ausreichend. Und wahlweise kann man ein anderes .wav-File übergeben, das wir natürlich noch auf Vorhandensein prüfen müssen:
IF NOT EMPTY( lcReturn ) AND NOT FILE( lcReturn ) lcReturn = "sounds\" + lcReturn IF NOT FILE( lcReturn ) lcReturn = "" ENDIF ENDIF
Und dann brauchen wir es nur noch abspielen. Dabei kann man über eine globale Varialbe gs_usesound den Sound auch abschalten. Einfach ein Feld usesound in der vfxsys.dbf anlegen...
IF NOT EMPTY( lcReturn ) SET BELL ON SET BELL TO (lcReturn) IF TYPE("m.gs_usesound")="U" OR m.gs_usesound ?? CHR(7) ENDIF SET BELL TO IF lcSetBell = "OFF" SET BELL OFF ENDIF ENDIF
Sofern ein Messagetext übergeben wurde, wird der noch als WAIT WINDOW angezeigt. Gibt es zusätzlich einen Titel für eine Messagebox, wird natürlich stattdessen eine Messagebox angezeigt wie folgt:
IF NOT EMPTY( tcMessage ) tcMessage = LEFT( tcMessage, 250) IF EMPTY( tcTitle ) WAIT WIND tcMessage NOWAIT ELSE =MESSAGEBOX( tcMessage, 16, tcTitle ) ENDIF ENDIF
Der eigentliche Aufwand ist nunmehr natürlich, die diversen Nachrichten-Anzeigen in seinem Programm herauszusuchen und um den entsprechenden Sound-Parameter zu erweitern...
Es gibt eine Vielzahl von SET-Einstellungen und SYS-Funktionen in Visual FoxPro und was wäre eine neue Version ohne weitere SETs und SYSs! Beim Update übersieht man dabei gerne die eine oder andere neue Möglichkeit, insbesondere wenn man beim Update eine oder gar mehrere Versionen von Visual FoxPro überspringt. Deshalb hier ein paar Hinweise auf von VFX nicht explizit ohnehin gesetzte Einstellungen und SYS-Aufrufe, die Sie in Ihrem Hauptprogramm (vfxmain.prg) nachtragen können:
In Visual FoxPro 8.0 funktionierte SET TABLEVALIDATE sehr gut. Aber mit Visual FoxPro haben einige Entwickler von fehlerhaften Fehlermeldungen berichtet. Setzen Sie ggf. SET TABLEVALIDATE TO 0, sofern Sie diese Funktionalität nicht explizit benötigen...
Manche Comboboxen enthalten arg viele Einträge und werden Bildschirmfüllen geöffnet. Mit Visual FoxPro 9.0 ist eine einfache Limitierung der Anzahl der angezeigten Einträge in einer Combobox mit der Funktion SYS(2910,<Anzahl>) möglich. Ich setze das in meinen Anwendungen z.B. auf 15.
Nicht neu ist SYS(2450,1 ), damit erst die Anwendung und dann der Pfad nach Komponenten durchsucht wird. Ebenfalls nicht neu ist SET OLEOBJECT OFF, damit bei der Suche nach Objekten nicht erst die ganze Registry durchsucht wird.
Statt mühselig die verschiedene Themes-Eigenschaften umzuschalten, kann man auch einfach =SYS(2700,0 ) aufrufen, um den XP Themes-Support in der Anwendung pauschal abzuschalten...
Eine Checksumme für einen Datensatz kann man ganz einfach mit SYS(2017) für vor Änderungen zu schützende Tabellen (z.B. Tabellen mit Passwörtern wie VFXUSR) ermitteln.
Und noch ein letzter Tipp: Wussten Sie schon, dass man in AutoComplete-Auswahllisten durch das Drücken der Entfernen-Taste bei geöffneter Auswahlliste unerwünschte Einträge einfach wieder entfernen kann?
Manchmal ist es störend, wenn Anwender in einem Netzwerk oder auf einem Terminalserver vergessen, die aktuelle Anwendung zu beenden und dann möglicherweise stundenlang oder gar über Nacht oder über das Wochenende mit Ihrer Anwendung auf Tabellen stehenbleiben. Da hilft nur ein selbstgebautes Timeout innerhalb der Anwendung!
Um einen Timeout einer Anwendung zu erreichen, benötigen wir natürlich einen Timer basierend auf der Klasse ctimer in vfxobj.vcx. In das Time-Event kommt folgender Codeabschnitt:
this.interval=0 goprogram.nasktosave=2 WITH _SCREEN FOR lnform = .FORMCOUNT TO 1 STEP - 1 IF TYPE("_screen.Forms[lnForm]")="O" AND !ISNULL(.FORMS[lnForm]) IF pemstatus(.FORMS[lnForm],'lasktosave',5) .FORMS[lnForm].lasktosave=.F. ENDIF IF pemstatus(.FORMS[lnForm],'nformstatus',5) IF .FORMS[lnForm].nformstatus>0 .FORMS[lnForm].onundo() ENDIF ENDIF ENDIF NEXT ENDWITH goprogram.onquit()
Damit wird für sämtliche offenen Masken die Sicherheitsabfrage beim Speichern abgeschaltet und bei Vorhandensein von Änderungen, werden diese verworfen. Jetzt müssen wir nur noch dafür sorgen, dass der Timer auch bei Benutzeraktivitäten zurückgesetzt wird. Das einfachste Verfahren ist dabei ein Bindevent auf Tastendrücke wie folgt:
=THIS.addobject("otimerref","capptimer") BINDEVENT(_SCREEN, "keypress", goprogram, "timeout")
Und eine Methode Timeout hilft einem dann beim Zurücksetzen ebendiesen Timers:
PROCEDURE timeout LPARAMETERS tnKeyCode, tnShiftaltctrl IF TYPE("m.gs_timeout")="N" AND m.gs_timeout > 0 WITH THIS .nTimeoutSeconds = 0 .otimerref.reset() ENDWITH ENDIF RETURN
Natürlich gibt es dabei noch einige weitere Tricks, aber die heben wir uns für ein weiteres Build von VFX auf. Grundsätzlich funktioniert das obige Verfahren - man muss nur noch die Timeoutzeit einstellen...
Wie weiter oben beschrieben, kann man relativ leicht seine eigene Anwendung um Sound-Ausgabe erweitern. Leider funktioniert das einfache Beispiel unter Terminalserver nicht. Dafür ist stattdessen eine spezielle Windowsfunktion notwendig, die den Mediamanager für die Ausgabe ansteuert und den Sound dann auch wirklich auf dem Client abspielt wie folgt:
DECLARE INTEGER PLAYSOUND IN WINMM.DLL STRING, INTEGER, INTEGER =Playsound( <name der .wav-datei>,0,1)
Das Declare wird nur einmalig in der Hauptanwendung benötigt und in der erwähnten Soundfunktion kann man stattdessen die Playsound-Funktion aufrufen statt SET BELL umzusetzen und CHR(7) auszugeben.
Auf dem Terminalserver unter Windows 2000 ist im Gegensatz zu Windows 2003 die Farbtiefe auf maximal 256 Farben beschränkt, was einem aber möglicherweise auch auf niedrig eingestellten PCs passieren kann. Dies führt dann leider zu einer sehr unschönen Darstellung des normalen VFX XPOpen-Dialogs. Der nachfolgende Code schafft dabei Abhilfe:
DECLARE INTEGER GetDC IN WIN32API INTEGER DECLARE INTEGER GetDeviceCaps IN WIN32API INTEGER, INTEGER IF GetDeviceCaps(GetDC(_WhToHwnd(_WMainWind())),12) <=8 this.Picture="bitmap\blue16.bmp" this.closepicture="bitmap\tabclose16.bmp" this.openpicture="bitmap\tabopen16.bmp" ENDIF
Die Auflösung der Darstellung wird über Windows-API-Funktionen ermittelt. Die Bilder des Dialogs werden dann ggf. auf eine 16-Bit-Variante umgestellt und alles sieht wieder schön aus.
Natürlich kann man die integrierte ZIP-Funktion von VFX ganz schnell direkt aufrufen:
SET LIBRARY TO VFX.FLL ADDITIVE CreateZipArchive(lcPath , lcFileMask, tcArchiveFullPathName, "This.ZIPProgress", ; lnCompressionLevel, llRecurseSubfolders, lcPassword)
Aber dann hat man kein Thermometer und keine schöne Anzeige der aus irgendwelchen Gründen nicht eingepackten Dateien. Die bekommt man, in dem man stattdessen die carchive-Klasse von VFX instanziiert, die einen Wrapper für die FLL-Funktion darstellt. Das geht ganz einfach wie folgt:
loCreateArchive = CREATEOBJECT( "carchive" ) llOK = loCreateArchive.CreateArchive(; lcDirectory, lcFileMask, lcArchiveName,; lnCompressionLevel, llRecurseSubfolders, lcPassword) RELEASE loCreateArchive IF llOK RETURN lcArchiveName ENDIF RETURN ""
Und schon hat man ein Thermometer und ggf. eine Anzeige problematischer Dateien. Wenn man die ZIP-Datei verschlüsseln möchte, kann man das z.B. mit der Zuweisung lcPassword = xcrypt( vfxusr.password ) tun, wenn man denn gerade auf dem richtigen Datensatz des Users steht. Oder einfach lcPassword = xcryxpt( gouser.password ) schreiben...
To be continued