VFX - Tipps und Tricks

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...

 

Hinweise zum Einstieg

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.

Grundlagen von Hooks

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.

Einfache Hook-Beispiele

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

Korrektur von DisplayCount

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.

Defaultfarben für benötigte Felder

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().

Childformulare aus Childgrids aufrufen

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.

OnMore-Funktionen auf Shortcut-Menü

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.

Standardberichte nicht immer als Standard

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…

Kombinierter On(Post)Print-Hook

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 zurücksetzen

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.

Ort aus Postleitzahl vorbelegen

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.

VFX-PDF-Installation ohne Internet

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!

VFX-Berichte versenden

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.

Komprimieren der VFX-Messagetabelle

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.

Weglassen von VFX-Komponenten

Was man unter VFX vor einer Auslieferung der eigentlichen Anwendung tun kann, um Speicherplatz einzusparen, aber gelegentlich gerne vergisst:

Installation von VFX-Anwendungen

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.

Zusatzdateien für Hilfeaufruf

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).

Berichte exkludieren

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.

In Metadateien suchen

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!

VFX-INI-Datei und VFX-Splash

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.

 

Weitere Tipps und Tricks

Arbeiten mit Terminalserver

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...

Dateiabfrage abschalten

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.

Beschleunigter Maskenaufbau

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.

Kleiner Excel-Export-Button

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...

Sounds in Anwendungen

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...

SET-Einstellungen und SYS-Funktionen

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?

Noch mehr Tipps und Tricks zu VFX

Timeout einer Anwendung

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...

Sounds auch unter Terminalserver ausgeben

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.

Bitmaps an Farbtiefe anpassen

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.

ZIP-Funktion direkt aufrufen

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