Donnerstag, 29. November 2007

Arbeiten mit Umgebungsvariablen / Working with Environment Variables

Als Umgebungsvariablen werden solche Werte angesehen, die vom BS bereitgestellt werden. Die meisten dieser Variablen können über START/Systemsteuerung/System/Erweitert/Umgebungsvariablen eingesehen und auch den eigenen Bedürfnissen entsprechend angepasst werden.

Die Funktion GetEnv() kommt in VFP schon lange zum Einsatz. Sie stellt nicht nur die zur Verfügung stehenden Variablen des BS dar, sondern zeigt gleichzeitig als Tooltip den aktuellen Inhalt dieser Variablen an. Benötigen wir nun bspw. eine Liste der auf unserem Rechner freigegebenen Verzeichnisse, dann ist dies über eine Kombination von GetEnv() und ANetResources() möglich.

ANETRESOURCES(myResources,[\\] + GETENV([COMPUTERNAME]),1)
DISPLAY MEMORY LIKE myResources

Oben stehender Code liefert ein 1-Dimensionales Array mit dem Namen 'myResources' in dem sämtliche Netzwerkfreigaben des eigenen Rechners aufgelistet sind.

Wie erhalte ich nun die aktuell gesetzten Umgebungsvariablen in einer Liste?

In 2002 wurde von Michael Reynolds im fox wiki eine ausgesprochen einfache Lösung gepostet. Wissend, dass in der DOS-Shell über den Befehl SET sämtliche Umgebungsvariablen abrufbar sind:

RUN set > setlist.txt
lnFile = FOPEN('setlist.txt')
DO WHILE !FEOF(lnFile)
    lcString = FGETS(lnFile)
    lcVar = LEFT(lcString, AT('=', lcString) - 1)
    ? lcVar + ' = ' + GETENV(lcVar)
ENDDO
FCLOSE(lnFile)

Als Fan der Funktion FileToStr() (Wer diesen Blog verfolgt wird dies sicherlich schon bemerkt haben), habe ich Michael's Code auf diese Funktion umgestellt. Die Routine liefert ein 2-Dimensionales Array das in Spalte 1 den Namen und in Spalte 2 den Wert der Umgebungsvariablen enthält.

RUN set > c:\temp\setlist.txt

LOCAL lcSetList as String, i as Integer

* Einlesen der Ergebnisdatei
lcSetlist = FILETOSTR([c:\temp\setlist.txt])
* Ergebnisdatei loeschen
DELETE FILE ([c:\temp\setlist.txt])

* Abarbeitungsschleife für jede Zeile innerhalb der Ergebnisdatei
FOR i = 1 TO GETWORDCOUNT(lcSetList,CHR(13)+CHR(10))

    * Array redimensionieren
    DIMENSION laSetArray(i,2) as String
    * Variablenname auslesen
    laSetArray(i,1) = GETWORDNUM(GETWORDNUM(lcSetList,i,CHR(13)+CHR(10)),1,[=])
    * Variablenwert auslesen
    laSetArray(i,2) = GETWORDNUM(GETWORDNUM(lcSetList,i,CHR(13)+CHR(10)),2,[=])

ENDFOR

* Schleifenergebnis anzeigen
DISPLAY MEMORY LIKE laSetArray

RELEASE lcSetList, i, laSetArray

Im selben wiki-Beitrag (s.o.) zeigte Ed Rauh ein Codemuster auf Basis von Pointern und API-Aufrufen zur Arbeit mit statischen Blöcken wie sie im Zusammenspiel mit Umgebungsvariablen zum Tragen kommen.

Der API-Aufruf GetEnvironmentStrings() liefert einen vom OS verwalteten Datenblock.

Die VFP-Funktion GetEnv() ruft den Wert einer spezifischen Variablen gezielt ab. Hierfür muss jedoch deren genauer Name bekannt sein. GetEnv() unterstützt zwar Intellisense, je nach Einsatzgebiet steht Intellisense jedoch nicht zur Verfügung.

Der u.a. Code liefert ebenfalls einen 2-Dimensionalen Array der in Spalte 1 den Namen und in Spalte 2 den Wert der Umgebungsvariablen enthält.

DECLARE aEnv[1,2]
? GetAllEnvStrings(@aEnv)
DISPLAY MEMORY LIKE aENV

FUNCTION GetAllEnvStrings
LPARAMETER aEnvArray
DECLARE INTEGER GetEnvironmentStrings IN WIN32API
DECLARE SHORT FreeEnvironmentStrings IN WIN32API INTEGER lpszEnvironmentBlock
DECLARE INTEGER lstrcpyn IN WIN32API AS StrCpyN ;
    STRING @ lpDestString, ;
    INTEGER lpSource, ;
    INTEGER nMaxLength
LOCAL nOffset, nEnvironmentBlock, cEnvString, nNumEntries, cEqualPos
DECLARE aEnvArray(1,2)
nNumEntries = 0
cEnvString = ' '
nOffset = 0
nEnvironmentBlock = GetEnvironmentStrings()
DO WHILE LEN(cEnvString) > 0
   cEnvString = REPL(CHR(0), 512)
   IF StrCpyN(@cEnvString, nEnvironmentBlock + nOffset, 512) # 0
      cEnvString = LEFT(cEnvString, MAX(0,AT(CHR(0),cEnvString) - 1))
      nEqualPos = AT('=',cEnvString)
      IF nEqualPos > 0
         nNumEntries = nNumEntries + 1
         DECLARE aEnvArray(nNumEntries,2)
         aEnvArray[nNumEntries,1] = LEFT(cEnvString,nEqualPos - 1)
         aEnvArray[nNumEntries,2] = SUBST(cEnvString, nEqualPos + 1)
         nOffset = nOffset + LEN(cEnvString) + 1
      ENDIF
   ENDIF
ENDDO
=FreeEnvironmentStrings(nEnvironmentBlock)

Mittwoch, 28. November 2007

ID3v1 Tags in MP3-Dateien auslesen und verändern / Read and change ID3v1 Tags in MP3 files

Die Position des ID3v1 Tags in einer MP3 Datei ist recht einfach zu ermitteln. Der Offset in der Datei Beträgt -128 Bytes. Wenn also die MP3 Datei eine Grösse von 2500 Bytes besitzt, dann beginnt der Tag bei Byte 2372.

Die ersten drei Bytes stellen den Magic Code, der anzeigt, dass hier auch wirklich ein ID3v1 Tag vorhanden ist. Entsprechen diese 3 Zeichen nicht der Zeichenkette "TAG", dann ist der ID3v1 Tag ungültig und kann nicht verarbeitet werden.

Dem Magic Byte schliessen sich weitere Felder an:

Titel                  30 Bytes
Interpret              30 Bytes
Name des Albums        30 Bytes
Erscheinungsjahr        4 Bytes
Kommentar              30 Bytes
Genre (Rock, POP, ...)  1 Byte

Insgesamt ist die Struktur 3 Byte Magic Code + 125 Byte Informationen = 128 Byte groß.

Unsere erste Aufgabe besteht nun darin, die besagten 128 Bytes zur Bearbeitung in eine Variable zu kopieren. Hierfür bietet sich die Funktion FileToStr() an.

lcMP3File = GETFILE([mp3],[MP3 Audio],[Lesen],0,[Lesen des MP3 ID3 Tags])
lcMP3String = FILETOSTR(lcMP3File)

Den Tag-Block erhalten wir, indem wir die rechten 128 Byte einer eigenen Variablen zuweisen.

lcMP3Tag = RIGHT(lcMP3String,128)

Mit Substr() können wir nun Feld für Feld zur Anzeige bringen. Hierbei ist jedoch zu beachten, dass die unbenutzten Bytes eines Feldes nicht mit Blank sondern mit Chr(0) aufgefüllt sind. Hierbei handelt es sich um ein nicht darstellbares Zeichen und der Fux zeigt diese Positionen somit als Rechteck an.

Da dies optisch nicht so viel hermacht, sollten wir die Chr(0) Positionen herausfiltern.

CHRTRAN(SUBSTR(lcMP3Tag,4,30),CHR(0),[])

Möchten wir nun neue Werte hinterlegen, so können wir dies mit der Funktion Stuff() durchführen. Wichtig hierbei ist das Auffüllen des Feldes mit Chr(0).

lcMP3Tag = STUFF(lcMP3Tag,4,30,PADR([Neuer Titel],30,CHR(0)))

Als letzte Aufgabe bleibt uns nun das Zusammenführen des MP3-Strings mit unserem Tag-Block. Hierzu nehmen wir uns den zu Anfang eingelesenen String und reduzieren ihn um 128 Bytes. Im Anschluss ergänzen wir ihn um unseren Tag-String und schreiben die Daten mit StrToFile() wieder zurück auf die Festplatte. Fertig ist der Editor.

lcMP3String = SUBSTR(lcMP3String,1,LEN(lcMP3String) - 128) + lcMP3Tag
STRTOFILE(lcMP3String,lcMP3File,0)

Natürlich fehlt an dieser Stelle noch eine Bearbeitungsmaske, aber das sollte nun wirklich kein Problem mehr darstellen. Hier nun der komplette Codeblock:

LOCAL lcMP3File as String, lcMP3String as String, lcMP3Tag as String
lcMP3File = GETFILE([mp3],[MP3 Audio],[Lesen],0,[Lesen des MP3 ID3 Tags])

IF FILE(lcMP3File)
    lcMP3String = FILETOSTR(lcMP3File)
    lcMP3Tag    = RIGHT(lcMP3String,128)
 
    IF LEFT(lcMP3Tag,3) == [TAG]
        * Auslesen von ID3v1Tag Informationen
        CLEAR
        ?    CHRTRAN(SUBSTR(lcMP3Tag,  4,30),CHR(0),[])
        ?    CHRTRAN(SUBSTR(lcMP3Tag, 34,30),CHR(0),[])
        ?    CHRTRAN(SUBSTR(lcMP3Tag, 64,30),CHR(0),[])
        ?    CHRTRAN(SUBSTR(lcMP3Tag, 94, 4),CHR(0),[])
        ?    CHRTRAN(SUBSTR(lcMP3Tag, 98,30),CHR(0),[])
        ?ASC(CHRTRAN(SUBSTR(lcMP3Tag,128, 1),CHR(0),[]))

        * Setzen von ID3v1Tag Informationen
        lcMP3Tag    = STUFF(lcMP3Tag,  4,30,PADR([Neuer Titel],    30,CHR(0)))
        lcMP3Tag    = STUFF(lcMP3Tag, 34,30,PADR([Neuer Interpret],30,CHR(0)))
        lcMP3Tag    = STUFF(lcMP3Tag, 64,30,PADR([Neues Album],    30,CHR(0)))
        lcMP3Tag    = STUFF(lcMP3Tag, 94, 4,PADR([1900],            4,CHR(0)))
        lcMP3Tag    = STUFF(lcMP3Tag, 98,30,PADR([Neuer Kommentar],30,CHR(0)))
        lcMP3Tag    = STUFF(lcMP3Tag,128, 1,CHR(16))
        lcMP3String = SUBSTR(lcMP3String,1,LEN(lcMP3String) - 128) + lcMP3Tag
        STRTOFILE(lcMP3String,lcMP3File,0)
    ENDIF
ENDIF

RELEASE lcMP3File, lcMP3String, lcMP3Tag

Montag, 26. November 2007

Hinzufügen von Spalten in SQL SELECT Befehlen / Adding columns to SQL SELECT statements

Eine häufig auftretende Anforderung bei der Arbeit mit Daten bzw. mit ihrer Datenquelle ist es, in der Lage zu sein 'on the fly' diese um zusätzliche Spalten zu ergänzen. 

Nun, dies ist ziemlich einfach, wenn es um das Hinzufügen einer neuen, leeren Spalte geht. Einfach in der Abfrage die SPACE()-Funktion wie folgt einsetzen:
 
SELECT SPACE(30) AS neuespalte FROM tabellenname

Hinweis: Das Wort 'AS' ist in VFP (oder bei SQL Server) eigentlich nicht notwendig, aber einige SQL Dialekte (->INFORMIX) erwarten es. Ganz abgesehen davon, dass es die Lesbarkeit des SQL-Befehls erhöht. 

Gehen wir also davon aus, dass sich in einer Tabelle namens 'muster' Daten wie die folgenden befinden:
 
cVorname  cNachname   iGanzzahl    nDez1    nDez2
Fritz     Müller              0   123.45  3456.78
Hans      Lüdenscheid         3  1111.65   654.32

Würden wir einen zusammenhängenden Namen benötigen um einen 'vollen Namen' zu erhalten, dann könnten wir folgendes machen:
 
SELECT (ALLTRIM( cVorname ) + [ ] + ALLTRIM( cNachname )) AS cKomplettname FROM muster

Auf den ersten Blick erscheint dies ziemlich logisch. Das Interessante daran ist, wenn eine 'künstliche Spalte' erzeugt wird, VFP auf Basis der existierenden zusammengefügten Spalten eine Spaltendefinition vornimmt. 

Da die Spalten 'cVorname' und 'cNachname' jeweils als C(20) definiert wurden ergibt sich hierdurch ein Feld vom Typ C(41) -> 

Mit anderen Worten: 

20 Zeichen für die erste Spalte (cVorname), eine für das Leerzeichen und weitere 20 Zeichen für die zweite Spalte (cNachname). 

Das ist aber ganz OK so, auch wenn es vielleicht ein klein wenig Platzverschwendung ist, aber dafür stellt dies auch sicher, dass wir keine Daten verlieren. Und wollen wir wirklich nur die minimal benötigte Spaltenbreite nutzen, dann können wir mit der PADR() Funktion den Fux wie folgt dazu bringen:
 
SELECT PADR( ALLTRIM( cVorname ) + [ ] + ALLTRIM( cNachname ), 30) AS Komplettname FROM muster

Wenn wir jedoch zwischen verschiedenen Feldtypen konvertieren müssen (bspw. Numerisch nach Character), dann stossen wir auf ein potentielles Problem, da VFP in diesem Fall nicht wissen kann, wie gross das neue Feld tatsächlich sein muss. Alles was der Fux in diesem Fall machen kann ist, den ersten Wert als Basis heranziehen. 

Solch ein Abfrage sähe wie folgt aus:
 
SELECT TRANSFORM( nDez1 ) AS cWert FROM muster

In diesem Fall erhielten wir für cWert eine definierte Spaltengröße von C(6), mit anderen Worten, die Zahl der Zeichen im ersten Wert der Tabelle. Dies bedeutet natürlich, dass der zweite Wert abgeschnitten würde, da er insgesamt 7 Zeichen enthält. An dieser Stelle wird es nun wichtig sicherzustellen, dass die Resultatgröße dem tatsächlichen Platzbedarf entspricht. Hierfür können wir die Funktion PADR() einsetzen um die Formatierung nach dem Transformieren vorzugeben. 

Dies sieht wie folgt aus:
 
SELECT PADR( TRANSFORM( nDez1 ), 10) AS cWert FROM muster

Aber die Einführung des CAST()-Befehls gibt uns eine Alternative die es uns erlaubt, VFP direkt vorzugeben, in welchem Format das Ergebnis aufgenommen werden soll. 

In VFP9 können wir die Abfrage wie folgt aufbauen:
 
SELECT CAST( ALLTRIM( cVorname ) + [ ] + ALLTRIM( cNachname ) AS CHAR(30)) AS Komplettname FROM muster

oder als Alternative: 

SELECT CAST( nDez1 AS CHAR(10)) AS cWert FROM muster

Wenn wir mit existierenden Spalten arbeiten ist dies auch soweit in Ordnung. Aber was passiert, wenn wir eine neue Spalte auf Basis eines bestimmten Datentyps erzeugen müssen? Bei Zeichenketten ist dies relativ einfach. 

Wir benutzen SPACE() oder auch PADL() um die Spalte zu erzeugen:
 
SELECT *, SPACE(30) AS cNeuertext FROM muster

Ähnlich verhält es sich bei Währungsspalten, die mit "$0" definiert werden, und einem neuen Datum mit Hilfe eines leeren Datumsstrings "{}", einer Dezimalspalte mit einem 0-String und logischen Felder mit einem .F., usw.:
 
SELECT $0 AS yGeldbetrag,
       {} AS dBezahlt,
       00000.00 AS nAusgleich,
       .F. AS lGeloescht
FROM muster

Nachdem dies so schön funktioniert, da könnte man versucht sein eine Integer-Spalte auf diese Art und Weise zu erzeugen: 

SELECT 0 AS nNeueGanzzahl FROM muster

Beim Erstellen einer Integer-Spalte wird deren Wert grundsätzlich mit "0" initialisiert. Aus diesem Grund erscheint es naheliegend, VFP mit "0" den Variablentyp als Integer mitzuteilen.Unglückerlicherweise ist dies aber nicht der Fall. Tatsächlich erzeugt VFP eine Dezimalspalte mit 1 Vorkomma und 0 Nachkommastellen. Als Ergebnis lassen sich dann nur Werte von 0-9 hinterlegen. Nicht wirklich das was gewünscht war. Wie bekommen wir also eine Integer-Spalte? Vor VFP 9 war dies ein wenig trickreich und es gab zwei Wege zum Ziel zu gelangen. Zunächst (und am einfachsten) kann die Spalte mit der benötigten Länge an Vorkommastellen definiert werden: 

SELECT *, 0000000000 AS nNeueGanzzahl FROM muster

Das Ergebnis ist nicht wirklich ein Integerwert sondern eine größere Dezimalspalte (N(10,0)) und das ist unter Umständen etwas problematisch. Um einen echten Integerwert zu erzeugen müssen wir den Umweg über ein Kartesisches Produkt gehen. 

Ein Kartesisches Produkt erhalten wir, wenn eine Abfrage zwei Tabellen ohne spezielle Bedingung miteinander verbindet. Das Ergebnis ist, das jeder Datensatz der ersten Tabelle mit jedem Datensatz der zweiten Tabelle verbunden wird. Die Ergebnismenge entspricht demzufolge der Anzahl der Datensätze der ersten Tabelle multipliziert mit der Anzahl der Datensätze der zweiten Tabelle. Auf diese Art und Weise entstehen sehr schnell sehr grosse Ergebnismengen.
 
SELECT * FROM tabelle1, tabelle2

Bei 100 Datensätzen in der ersten Tabelle und 1000 Datensätzen in der zweiten Tabelle würde dies einen Ergebniscursor mit 100.000 Datensätzen ergeben. 

Das klingt nicht wirklich brauchbar. Also wie soll uns das bei unserem Problem mit der Integer-Spalte helfen? Ganz einfach: Wenn wir einen Cursor mit Namen 'crsDummy' erzeugen, der als einzige Spalte einen Integerwert aufweist, und dann einen leeren Datensatz darin erzeugen und diesen Cursor 'crsDummy' in die FROM-Liste aufnimmt wird die Integerspalte zwangsweise integriert. Als Ergebnis erhalten wir somit einen Cursor, der in jedem Datensatz über eine zusätzliche INTEGER-Spalte verfügt. 

Das Ganze funktioniert wie folgt:
 
CREATE CURSOR dummy ( iNeueGanzzahl I )
INSERT INTO dummy VALUES (0)
SELECT * FROM muster, dummy

Derselbe Trick kann in Verbindung mit MEMO, GENERAL oder auch jeder anderen Art von Spalten mit beliebigen Datentypen verwendet werden. Aber, seit VFP9 benötigen wir diese Vorgehensweise nicht mehr. Wieder einmal hilft uns die CAST()-Funktion. 

Mit ihrer Hilfe können wir beliebige Datentypen erzeugen:
 
SELECT *, CAST( 0 AS INT) AS iNeueGanzzahl FROM muster

Tatsächlich können wir CAST() auch einsetzen, um spezielle Datentypen zu liefern. Für Anwender ist es bspw. irritierend, wenn vom Entwickler DatumZeit-Felder genutzt werden. Gerade in Hinblick auf SQL-Kompatibilitäten besteht ab und an diese Notwendigkeit. Wie so oft stehen im Fux viele Wege zur Verfügung. Aber CAST() ist die sauberste Möglichkeit solche Probleme in den Griff zu bekommen.
 
SELECT CAST( tDatumZeit AS date ) AS dDatum FROM muster

In der VFP9-Hilfe gibt es eine Tabelle in der hinterlegt ist, welche Typkonvertierungen mit CAST() durchgeführt werden können. Bspw. können wir mit CAST() Datumswerte als Character, VarChar, Datetime oder Memo, jedoch nicht als Decimal wandeln. Natürlich ist der übliche Grund für ein zusätzliches Feld zumeist darin begründet, dass wir eine Spalte benötigen, um Information über speziell zu verarbeitende Datensätze zu verwalten. Sei es während der Erfassung zur differenzierten Visualisierung von Informationen oder auch bei der Druckausgabe. Hierbei sollten wir jedoch bedenken, dass SQL-Select üblicherweise schreibgeschützt sind. Bis VFP7 mussten wir hierbei zu einem kleinen Trick greifen. 

Hintergrund ist, dass Cursor tatsächlich als temporäre Tabellen implementiert sind. Wird somit ein Cursor ein zweites Mal geöffnet, dann ist VFP gezwungen eine zweite Tabelle zu erzeugen und diese ist nicht mehr schreibgeschützt. 

Der folgende Code erzeugt eine beschreibbaren Cursor und funktioniert in ALLEN VFP und FP Versionen:
 
SELECT * FROM muster INTO CURSOR temp
USE DBF( [temp] ) AGAIN IN 0 ALIAS cur_readwrite
SELECT cur_readwrite
USE IN temp

Mit der Version 7 des Fux wurde der optionale Parameter READWRITE in den SQL-Sprachumfang von VFP aufgenommen und ermöglicht es uns, Select-Ergebnisse direkt beschreibbar zu machen.
 
SELECT * FROM muster INTO CURSOR temp READWRITE

Und wann immer es geht sollten wir diese Syntax auch nutzen. Nichts desto trotz, ist es immer gut, die alte Variante zu kennen. Wir wissen schliesslich nie, wann alter Code geändert werden muss... :-)

Freitag, 23. November 2007

Arbeiten mit Parameterobjekten / Working with parameter objects

PUBLIC Variablen wurden und werden immer noch gerne als alternative Variante für Parametereinstellungen genutzt. In der heutigen objektorienten Welt ist dies jedoch nicht mehr angebracht. Sie widersprechen dem Grundgedanken des gekapselten und unabhängigen Objekts.

Die Auswertung von Parametern in einem Objekt erfolgt häufig durch das vorherige Füllen von Objekteigenschaften durch das aufrufende Objekt. Bei globalen Parametrisierungen, die von den verschiedensten Formularen und Objekten genutzt werden macht es jedoch wenig Sinn, immer wieder aufs Neue die selben Vorgaben zu setzen. An dieser Stelle macht ein zentrales Parameterobjekt mehr Sinn.

Wichtig hierbei ist jedoch die Unterscheidung zwischen Aufrufparametern und Konfigurationsparametern. 

Beim Aufruf einer Anwendung können dieser bspw. eine Mandanten- und eine Auftragsnummer mitgeben werden. Diese Aufrufparameter haben keinen Einfluss auf das grundsätzliche Verhalten der Anwendung selbst. Sie sorgen nur dafür, dass bereits bestimmte Informationen automatisch geladen und angezeigt werden.

Welche Informationen des Auftrags zur Anzeige kommen, wie der Aufbau des Formulars aussieht und welche Tätigkeiten der Anwender innerhalb der Form durchführen darf, all das sind Konfigurationsparameter, die in einem gesonderten Parameterobjekt vorgehalten werden können, da sie sich während der Sitzung nicht mehr ändern werden.

Ein Parameterobjekt läßt sich sehr einfach erstellen. Als Basis sollte hierbei die 'Empty'-Klasse dienen. Sie enthält keine Eigenschaften, Methoden oder Ereignisse. Neue Eigenschaften lassen sich deswegen nicht durch myEmptyClass.AddProperty() hinzufügen. Schliesslich handelt es sich um eine tatsächlich gänzlich leere Klasse. Neue Eigenschaften und Werte lassen sich jedoch mit der Funktion ADDPROPERTY() sowie mit SCATTER NAME...ADDITIVE erzeugen.
 
oParam = CREATEOBJECT([empty])
ADDPROPERTY(oParam, [Caption], [Überschrift])
ADDPROPERTY(oParam, [startdatum], DATE())
ADDPROPERTY(oParam, [endedatum], DATE()+14)

Mit o.a. Code erhalten wir ein Objekt, das beliebig viele Werte 'huckepack' mit sich tragen kann. Ähnlich einem Array, nur viel einfacher anzusprechen und zu verarbeiten.
Der Einsatz dieses Objektes kann zum Einen über die Bereitstellung in einem globalen Parameterhandler oder auch, sozusagen als Ersatz für einzeln übergebene Parameter beim Formularaufruf erfolgen. In diesem Post werde ich gezielt auf den Einsatz beim Aufruf eines Forms eingehen.

Ein entsprechender Aufruf könnte wie folgt aussehen:
 
DO FORM DatumsEingabe WITH oParam TO oErgebnis

Obiger Mustercode nutzt das zu Beginn definierte Parameterobjekt oParam. Nach dem Beenden der aufgerufenen Form befinden sich die Ergebnisse im Objekt oErgebnis. Damit dies auch tatsächlich der Fall ist, müssen zwei Dinge beachtet werden.
  • Erstens ist die aufzurufende Form vom Windowstype auf MODAL einzustellen.
  • Zweitens muss die Form DatumsEingabe über eine Eigenschaft namens oParameters (zumindest in diesem Beispiel) verfügen. Im INIT dieser Form steht folgender Code:
LPARAMETERS vParameterObj as Object
Thisform.oParameters = m.vParameterObj

Somit wird das beim Aufruf übergebene Object an die dafür vorgesehene Formulareigenschaft weitergereicht. Nun können die Parameterwerte aus dem Objekt ausgelesen und an die verschiedenen Eigenschaften und Objekte des Forms übergeben werden.

WITH Thisform
 .caption = .oParameters.Caption
 .text1.value = .oParameters.Startdatum
 .text2.value = .oParameters.Endedatum
ENDWITH

Die Rückgabe der Werte geschieht üblicherweise im UNLOAD-Event des Formulars.

RETURN Thisform.oParameters

Zuvor sollten im DESTROY-Event des Forms die aktuellen Werte in das Parameterobjekt geschrieben werden:

WITH Thisform
  .oParameters.Startdatum = .Text1.Value
  .oParameters.Endedatum = .Text2.Value
ENDWITH

Dies kann nicht innerhalb von UNLOAD erfolgen, da zu diesem Zeitpunkt bereits sämtliche Objekte auf der Form zerstört wurden.
Nachdem die aufgerufene Form beendet wurde stehen dem aufrufenden Objekt die modifizierten Werte in oErgebnis zur Verfügung und können weiterverarbeitet werden. 

Ergänzung vom 04.06.2008:
JoKi hat heute eine Optimierung zu o.a. Code gepostet, den ich gerne an dieser Stelle hinzufüge.
Berechtigterweise schlägt er vor, anstelle der VALUE-Zuweisung ins Textobjekt und dem späteren zurückschreiben ins Parameterobjekt direkt die jeweilige Parameterobjekt-Eigenschaft der ControlSource der korrespondierenden Textbox zuzuordnen.

Somit reduziert sich dieses Codemuster:
 
WITH Thisform
 .text1.value = .oParameters.Startdatum
 .text2.value = .oParameters.Endedatum
ENDWITH
...
WITH Thisform
  .oParameters.Startdatum = .Text1.Value
  .oParameters.Endedatum = .Text2.Value
ENDWITH

auf diesen Code: 

WITH Thisform
 .text1.ControlSource = .oParameters.Startdatum
 .text2.ControlSource = .oParameters.Endedatum
ENDWITH

@JoKi: Danke für den Hinweis :-)

Donnerstag, 22. November 2007

So finden wir das aktuelle Objekt innerhalb eines Grids / How to find the current object within a grid

Klar, das aktuelle Control einer Form ist schnell gefunden:
 
loObj = Thisform.ActiveControl

Wenn es jedoch um eine Text-, Edit- oder Checkbox eines Grids geht, dann funktioniert dies leider nicht, denn VFP liefert uns anstelle von Column und Row nur das Gridobjekt selbst. Das ist aber bei weitem kein Beinbruch. Das Grid verfügt über eine ähnliche Funktion, mit deren Hilfe sich die aktuelle Spalte herausfinden lässt.
 
loObj = Thisform.ActiveControl
IF loObj.Baseclass = [grid]
    liCol = loObj.ActiveColumn
ENDIF

Damit haben wir jedoch nur die Spaltennummer und wissen noch nicht, welches das Ein-/Ausgabecontrol ist. Die Column hat jedoch eine abfragbare Eigenschaft namens CurrentControl. Diese leistet uns in diesem Fall gute Dienste. Im Zusammenspiel mit dem Controls()-Array des Grids können wir uns den Namen des Objektes zusammenbauen und evaluieren. 
 
loObj = Thisform.ActiveControl
IF loObj.Baseclass = [grid]
    liCol = loObj.ActiveColumn
    loObj = EVALUATE([loObj.Columns(liCol).] + loObj.Columns(liCol).CurrentControl
ENDIF

Jetzt müssen wir nur noch eines Bedenken. Was passiert, wenn der Anwender die Reihenfolge der Spalten verändert? In diesem Fall erhalten wir bei der Abfrage über ActiveColumn dummerweise nicht mehr die korrekte Spaltenummer. Genau genommen haben wir sie auch vorher nicht bekommen. Da die Spalten jedoch noch in ihrer ursprünglichen Reihenfolge waren stimmten einfach nur die Werte überein. 

ActiveColumn liefert also nicht die Column-Nummer sondern die ColumnOrder zurück. Um nun den Verweis auf die richtige Spalte zu erhalten müssen wir in einer Schleife den Columns()-Array solange durchlaufen bis der Wert aus ActiveColumn und ColumnOrder übereinstimmt. Auf dieser Basis kann dann die richtige Objektreferenz erzeugt werden.
 
loObj = Thisform.ActiveControl

IF loObj.Baseclass = [grid]
    liCol = loObj.ActiveColumn
    FOR i = 1 TO loObj.ColumnCount
        IF loObj.Columns(i).ColumnOrder = liCol
            liCol = i
            EXIT
        ENDIF
    ENDFOR
    loObj = EVALUATE([loObj.Columns(liCol).] + loObj.Columns(liCol).CurrentControl)
ENDIF

Mittwoch, 21. November 2007

Welche WORD-Version liegt auf dem aktuellen System vor? / Which version of Word is installed on the current system?

Mit jeder Word-Version stehen neue Funktionen für die OLE-Automation zur Verfügung oder das Interface wurde in Kleinigkeiten modifiziert. 

Somit ist es nicht ausreichend, dem Anwender mitzuteilen, dass für bestimmte Funktionen der eigenen Software einfach nur Word installiert sein muss. 

Vielmehr sollten wir als Entwickler dafür Sorge tragen, dass unsere Software auf die verschiedenen Versionen mit individuellen Klassen reagiert. 

Zumindest muss jedoch eine Mitteilung an den Anwender erfolgen, dass u.U. eine neuere Version benötigt wird.
 
FUNCTION LoadWord
    LOCAL liVersion as Integer, lcMsg as String
    liVersion = 0

    * Für diese Demo dem _Screen-Objekt eine Property hinzufügen
    * in der anschliessend das OLE-Objekt abgelegt wird
    IF !PEMSTATUS(_screen,[oWord],5)
        _screen.AddProperty([oWord])
    ENDIF

    * Jetzt versuchen wir eine Instanziierung von Word
    TRY
        _screen.oWord = CREATEOBJECT([Word.Application])
    CATCH
        TEXT TO lcMsg TEXTMERGE NOSHOW PRETEXT 3
            Anscheinend ist Word nicht auf Ihrem Rechner installiert.
            Diese Anwendung wird jedoch benötigt um fortzufahren.
        ENDTEXT
        MESSAGEBOX(lcMsg,0+64+0,[Programminformation])
    ENDTRY

    * Wenn es das Wordobjekt gibt, dann gehts weiter mit der Versionsprüfung
    IF VARTYPE(_screen.oWord) = [O]
        WITH _screen.oWord
         
            IF INLIST(ALLTRIM(.version), [9.0], [10.0], [11.0], [12.0])
                liVersion = INT(VAL(.Version))
            ELSE
                * Office 97 und älter wird nicht mehr unterstützt
                TEXT TO lcMsg TEXTMERGE NOSHOW PRETEXT 3
                    Auf Ihrem Rechner muss Word 2000 oder höher installiert sein,
                    damit die gewünschte Funktionalität bereitgestellt werden kann.
                ENDTEXT
                MESSAGEBOX(lcMsg,0+64+0,[Programminformation])
                .Quit()
             
            ENDIF

        ENDWITH
    ENDIF

    RETURN liVersion
ENDFUNC

Dienstag, 20. November 2007

Meldungen und Infotexte einfach generieren / Messages and infos concatenated easily

Oftmals stehen wir vor der Aufgabe, einen umfangreichen Informationstext (u.U. gefüllt mit Variablenwerten) in einer Message- oder Editbox auszugeben. 

Am Beispiel einer Fehlermeldung könnte dies vielleicht wie folgt aussehen:
 
FUNCTION ShowError
LPARAMETERS vError, vMessage, vCode, vLine, vObject
MESSAGEBOX([in Zeile ] + TRANSFORM(m.vLine) + [: ] + ALLTRIM(m.vCode) ;
+ CHR(13) + [ist folgender Fehler aufgetreten:] + CHR(13) ;
+ CHR(13) + ALLTRIM(STR(m.vERROR)) + [ - ] + ALLTRIM(m.vMessage) + CHR(13) ;
+ CHR(13) + [Der Fehler trat in folgendem Objekt auf:] ;
+ CHR(13) + ALLTRIM(m.vObject), ;
0+64+0, ;
[Programminformation])
ENDFUNC

Abgesehen davon, dass es eine ziemlich aufwendige Sache ist, den Code so 'übersichtlich' aufzubereiten, muss auch sichergestellt werden, dass sämtliche NICHT-Alfanumerischen Felder ordnungsgemäß in Zeichenfolgen gewandelt werden. 

Zudem ist ein nachträgliches Umformatieren/Ergänzen des Textes eine ziemlich nervige Angelegenheit.

Ein einfacherer Weg einen mit Variablen gespickten Text auszugeben UND diesen ggf. schnell und einfach umzuformatieren besteht im Einsatz von TEXT ... ENDTEXT.  

Nutzen wir diese Funktion von Visual FoxPro so sieht die Welt um ein vielfaches einfacher aus:
 
FUNCTION ShowError 
LPARAMETERS vError, vMessage, vCode, vLine, vObject 
LOCAL cMyVar 
TEXT TO cMyVar NOSHOW ADDITIVE TEXTMERGE PRETEXT 2 
in Zeile <<m.vLine>>: <<m.vCode>> 
ist folgender Fehler aufgetreten: 

<<m.vError>> - <<m.vMessage>> 

Der Fehler trat in folgendem Objekt auf: 
<<m.vObject>> 
ENDTEXT 
ENDFUNC 

Praktischerweise müssen wir uns keine Gedanken mehr über den Variablentyp machen. VFP wandelt alle in '<< >>' stehenden Variablen automatisch in Strings.  

Wichtig ist auf jeden Fall der PRETEXT-Parameter. Hier stehen vier additiv anzugebende Werte zur Verfügung.  

1 Entfernt Leerzeichen am Zeilenanfang  
2 Entfernt Tabulatoren am Zeilenanfang  
4 Entfernt Leerzeilen  
8 Entfernt Zeilenvorschübe  

Sollen nun alle Leerzeichen, TABS und Leerzeilen entfernt werden so wird als Flagwert 7 (-> 1+2+4) übergeben. ( Die Entfernung von Zeilenvorschüben <8> ist nur für Datenströme empfehlenswert da die Lesbarkeit doch ein klein wenig leidet. )

Montag, 19. November 2007

Was feuert wann ... Eventtracking in Visual FoxPro

Selbst diejenigen unter uns, die schon eine ganze Weile mit dem Fux arbeiten fragen sich manchmal: Wann feuert nochmal welcher Event. Kommt jetzt zuerst QueryUnload, Unload oder Destroy? Kommt zuerst When oder GotFocus?

Natürlich können wir uns ein Formular bauen, in dem jedes Event in eine Protokolldatei reinschreibt. Aber warum so aufwändig? Schalten wir doch einfach das Eventtracking (-> Ereignisverfolgung) im Debugger ein. Danach können wir genau verfolgen, was in welcher Reihenfolge abgearbeitet wird.
 
Was ist hierfür zu tun?

Zunächst einmal öffnen wir den Debugger. In der Toolbar klicken wir nun auf den ganz rechts gelegenen Button. Im Anschluss erscheint das Ereignisüberwachungsfenster.
 
Abb. 1: Das Ereignisüberwachungsfenster


Hier können nun gezielt die Ereignisse ausgewählt werden, die überwacht werden sollen. Die Events 'MouseEnter', 'MouseMove' und 'MouseLeave' sollten auf jeden Fall aus der Liste entfernt werden. Andernfalls wird das Protokoll von diesen drei Events überschwemmt und die Auswertung unnötig erschwert.

Haben wir alle relevanten Events in der Liste überprüft steht nun die Entscheidung an, ob die Protokollierung sichtbar im Debugger oder in eine zu definierende Protokolldatei erfolgen soll. Abschliessend aktivieren wir der Überwachung. Dies geschieht über das Setzen des Hakens ganz oben im Fenster (-> Ereignisüberwachung aktivieren). Jetzt kann dieses Fenster über [OK] geschlossen werden. 

Sobald nun das nächste Objekt, Programm oder Form aktiviert wird können wir dieses in aller Ruhe im Debugger überwachen oder nach dem Beenden in der Protokolldatei nachlesen.

Freitag, 16. November 2007

Verschwundene Steuerungsbuttons im Visual FoxPro Debugger / Lost buttons in Visual Foxpro's Debugger toolbar

In meinem Entwicklungsverzeichnis gibt es üblicherweise keine foxuser.dbf. Die Nutzung dieser Resource wird innerhalb meiner config.fpw (die sich ebenfalls im Entwicklungsordner befindet) pauschal untersagt (RESOURCE = OFF).
Normalerweise sollte das Problem durch Löschen der 'foxuser.dbf' im Entwicklungsverzeichnis erledigt sein. Hilft dies nicht, so liegt es im allgemeinen an der 'foxuser.dbf' im VFP9-Installationsverzeichnis, für diese gilt jedoch grundsätzlich: 'Finger weg!'.
Somit ist die simple Aussage: "Lösche doch einfach die foxuser.dbf" nicht unbedingt zielführend. Also was tun? 
Gehen wird doch einfach mal umgekehrt an die Sache heran:
Wenn in einer Toolbar ein Button fehlt oder nicht benötigt wird, dann passen wir üblicherweise die entsprechende Toolbar an. Diese Funktion steht uns auch in VFP zur Verfügung. Sollten jetzt hämische Rufe unter dem Motto "im Debugger wird das aber gar nicht angeboten!!" laut werden, so kann ich nicht wiedersprechen, denn das stimmt tatsächlich. Diese Funktion finden wir nicht im Debugger-Fenster sondern im VFP-Hauptfenster!
Um nun die verloren gegangenen Buttons zu reanimieren empfiehlt sich die folgende Vorgehensweise:
1. Das Debug-Fenster öffnen
2. Rechtklick in einen freien Bereich einer Toolbar des Hauptfensters und im erscheinenden Kontextmenü den untersten Punkt 'Anpassen...' anklicken.


3. im nächsten Fenster (-> Symbolleiste anpassen) wählen wir per Linksklick den Punkt 'Debugger' aus.
 

4. Fehlende Buttons platzieren wir nun einfach per Drag'n'Drop auf der Debugger-Toolbar.

Natürlich können wir auf diese Weise auch Buttons aus einer Toolbar in das Fenster 'Symbolleiste anpassen' ziehen, um so die wenig oder nie genutzten Buttons zu entsorgen um u.U. mehr Übersicht zu erhalten. Meine Erfahrungen zeigen jedoch unmissverständlich, dass Murphy bei dieser Vorgehensweise keine Gnade kennt. Zumeist dauert es nur ein paar Tage bis der zuvor niemals genutzte Button dringend benötigt wird. Aber das Wiederfinden solcher Buttons sollte nun ja kein Problem mehr darstellen... ;-)

Donnerstag, 15. November 2007

Windows Scripting Host zur Abfrage von Laufwerksinformationen nutzen / Using Windows Scripting Host to query drive informations

Visual FoxPro bietet mit den Funktionen DISKSPACE() und DRIVETYPE() zwei Werkzeuge, die es uns ermöglichen, diverse Informationen bzgl. der verfügbaren Laufwerke abzufragen. Hier ein kleines Codemuster dazu: 
 
LOCAL laDrivetype(6), lcFont, i, lcL, j
laDrivetype(1) = [unbekannt ]
laDrivetype(2) = [Diskettenlaufwerk ]
laDrivetype(3) = [lokaler Datenträger]
laDrivetype(4) = [Netzlaufwerk ]
laDrivetype(5) = [CD/DVD-ROM Laufwerk]
laDrivetype(6) = [RAM Disk ]
lcFont = _screen.FontName
_screen.FontName = [Courier New]
CLEAR
?[LW Typ Gesamtgröße Freier Speicher]
FOR i = 65 TO 90
    lcL = CHR(i) + [:]
    IF DISKSPACE(lcL,1) > -1
        ? lcL,laDrivetype(DRIVETYPE(lcL))
        FOR j = 1 TO 2
            ?? [ ],TRANSFORM(DISKSPACE(lcL,j)/1000/1000,[999,999]) + [ MB]
            ?? [ ],PADL(ROUND(DISKSPACE(lcL,j)/1024/1024/1024,1),5,[ ]) + [ GB]
        ENDFOR
    ENDIF
ENDFOR
_screen.FontName = lcFont

Im Allgemeinen dürften diese Informationen ausreichend sein. Wenn wir jedoch weiterführende Daten benötigen (z.B. Dateisystem, Seriennummer, Datenträgerbezeichnung), dann streckt der Fux die Flügel. Zur Hilfe kommt uns wie so oft der Scripting Host. Also erstellen wir uns ein sogenanntes File System Object und fragen die benötigten Informationen ab. Wie dies bewerkstelligt wird, darüber gibt uns der folgende Code Aufschluss: 
 
LOCAL laDrivetype(6), lcFont, oFSO
laDrivetype(1) = [unbekannt]
laDrivetype(2) = [Wechseldatenträger]
laDrivetype(3) = [lokaler Datenträger]
laDrivetype(4) = [Netzlaufwerk]
laDrivetype(5) = [CD/DVD-ROM Laufwerk]
laDrivetype(6) = [RAM Disk]
lcFont = _screen.FontName
_screen.FontName = [Courier New]
oFSO = CREATEOBJECT([Scripting.FileSystemObject])

CLEAR

?PADR([LW], 3,[ ])
?? PADR([Typ], 21,[ ])
?? PADR([Dateisystem], 11,[ ])
?? PADL([SerialNo], 13,[ ])
?? [ ]
?? PADR([Datenträgerbezeichnung],33,[ ])
?? PADR([Gesamtgröße], 20,[ ])
?? PADR([Freier Speicher], 20,[ ])
?? PADR([Verfügbarer Speicher], 20,[ ])

FOR EACH oDrive IN oFSO.Drives
WITH oDrive
? PADR(.DriveLetter + [:],3,[ ])
?? PADR(laDriveType(.DriveType + 1),21,[ ])
IF .Isready
?? PADR(.FileSystem,11,[ ])
?? PADL(.SerialNumber,13,[ ])
?? [ ]
?? PADR(IIF(.DriveType = 3,.ShareName,.VolumeName),32,[ ])
?? TRANSFORM(.TotalSize / 1000 / 1000, [999,999]) + [ MB (]
?? PADL(.TotalSize / 1024 / 1024 / 1024,4,[ ]) + [ GB)]
?? TRANSFORM(.FreeSpace / 1000 / 1000, [999,999]) + [ MB (]
?? PADL(.FreeSpace / 1024 / 1024 / 1024,4,[ ]) + [ GB)]
?? TRANSFORM(.AvailableSpace / 1000 / 1000, [999,999]) + [ MB (]
?? PADL(.AvailableSpace / 1024 / 1024 / 1024,4,[ ]) + [ GB)]
ELSE
?? [kein Datenträger eingelegt]
ENDIF
ENDWITH
ENDFOR

_screen.FontName = lcFont
RELEASE laDrivetype, lcFont, oFSO

Mit Hilfe dieses kleinen Beispiels sollte es nun kein Problem mehr sein, die gewünschten Daten in Erfahrung zu bringen.

Mittwoch, 14. November 2007

Am Anfang war der erste Eintrag / In the beginning there was the first entry

Seit geraumer Zeit bin ich nun ein eifriger Leser diverser englischsprachiger Blogs. 

Seien es nun Andy Kramek, Cesar Chalom, Christof Wollenhaupt, Craig Boyd, Doug Hennig oder Rick Schummer oder oder oder. Meine Liste ist mittlerweile recht umfangreich und wächst beständig weiter an. 

Was jedoch alle diese Blogs gemeinsam haben ist, dass sie in englischer Sprache verfasst sind. Fakt ist, dass nicht jeder Entwickler/-in mit dieser Sprache so vertraut ist wie es vielleicht wünschenswert wäre. 

Bisher habe ich die interessantesten Blogs im Intranet meines Arbeitgebers in übersetzter Form bereitgestellt. An sich eine prima Lösung. In Hinblick auf die Bedeutung des Fuxes im Entwicklerumfeld habe ich mich nach langem Zögern dazu durchgerungen einen deutschsprachigen Blog rund um Visual FoxPro bereit zu stellen. 

Zukünftig wird es an dieser Stelle in unregelmäßigen Abständen Einträge mit kleinen Codebeispielen geben die sich aus Erfahrungen und Anforderungen des täglichen Arbeitens mit dem besten Entwicklungstool aller Zeiten ergeben. Lang lebe der Fux...