Session D-FORM

Customizable Forms mit VFP

Patrick E. Schärer
Business & System / INDISoftware GmbH


Ich will...

...dem Anwender die Möglichkeit geben, sein Formular selbst zu gestalten. Er soll sich seine Spezialwünsche selbst realisieren können, und mich nicht damit aufhalten, einige Textboxen von da nach dort zu schieben oder irgendwelche Farben nach seinem Geschmack zu Gestalten. Vielleicht will er auch einige Felder hinzufügen und diese auf seinem Formular dann natürlich auch anzeigen und bearbeiten.

Wer mit Outlook etwas häufiger arbeitet, wird diese Funktion dort kennen, wo ein Anwender sich seine eigenen Formulare anlegen und bearbeiten kann:

 

 

Einge Menge Arbeit das mit VFP zu realisieren? Bestimmt schon einiges. Das entscheidende ist aber ein richtiges Konzept und die richtige Klassenstruktur. Hier will ich eine Möglichkeit vorstellen:

 

Das Konzept
  1. Das Kernstück wäre eine Klasse, die in der Lage ist, bestimmte Eigenschaften (oder im Extremfall sogar gar  alle Eigenschaften) eines Objekts zu speichern und wiederherzustellen. Die Eigenschaften würden praktischerweise in einer DBF abgelegt, in der für jedes Objekt jeweils ein Datensatz entsteht.
  2. Bestimmte Eigenschaften werden nun auf der Oberfläche dem Anwender freigegeben, z.B. die Position eines Objekts im Formular. Er müsste nun das entsprechende Objekt in Position und ggf. Größe verändern dürfen. Die neuen Werte in diesem Fall der Width / Height bzw. Top / Left-Eigenschaften würden über die zentrale Klasse abgespeichert.
  3. Eine andere Möglichkeit könnte darin bestehen z.B. über die rechte Maustaste ein Kontext-Menü aufzubauen, das beispielsweise bestimmte Editoren für bestimmte Eigenschaften aufruft (wie Farbe etc.), die dann wieder das gleiche machen: Eigenschaften abspeichern über die Zentrale Verwaltungsklasse.
  4. Beim Start des Formulars würde das Objekt alle eingetragenen Eigenschaften sämtlicher Objekte, über die ein Datensatz vorliegt lesen und die tatsächlichen Eigenschaften dieser Objekte im Formular bearbeiten. Erst dann würde das Formular angezeigt.

Die zentrale Eigenschaftenverwaltungs-Klasse

1. Aufgabe: Objekt-Tabelle verwalten

Wann immer ein Objekt die Anfrage macht: „ich möchte diese Eigenschaft von mir abspeichern“, muss dies zuerst dazu führen, dass in einer dafür vorgesehenen Objekt-Tabelle (im Folgenden kitobj genannt) der Datensatz für dieses Objekt gefunden oder aber neu angelegt wird. Ist die Tabelle noch nicht vorhanden, müsste diese erzeugt werden, z.B. so:

CREATE TABLE (tcFileName) FREE (objScx c(50),objref c(120),objprop m,objIndex i)

 

lcOldCollate = SET("COLLATE")

SET COLLATE TO "MACHINE"   && Andernfalls wird Indexlänger überschritten

INDEX ON lower(objScx+objref) FOR !DELETED() TAG refname

INDEX ON lower(objscx+PADR(LEFT(objref,RAT('.',objref)-1),100))+;

          STR(objIndex) FOR !DELETED() TAG refindex

SET COLLATE TO lcOldCollate

 

Man sieht an der obigen Struktur, welche Felder bzw. Indices notwendig wären, um auf diese Weise ein Objekt eindeutig zu identifizieren und zu suchen.

Beide zusammengenommen ergeben dann eine eindeutige Bezeichnung des Objekts.

2. Aufgabe: Objekt-Datensätze verwalten

Bevor nun die Eigenschaften abgespeichert oder gelesen werden können, muss der dafür vorhandene Datensatz gelesen werden. Wenn es sich um ein neues Objekt handelt, darf er neu angelegt werden, sonst nicht.

lcRef  = SYS(1272,tuObject)

lcScx  = this._GetFilename(SYS(1271,tuObject))

lcSeek = LOWER(PADR(lcScx,50)+PADR(lcRef,100))

tuObject = .NULL.

SET ORDER TO TAG refName IN kitobj

 

SEEK lcSeek

 

IF !FOUND()

   INSERT INTO kitobj (objref,objscx,objindex) ;

   VALUES (lcRef,lcscx,tnIndex)

ENDIF  

3. Aufgabe: Eigenschaften abspeichern

Nun bleibt noch die Frage, wohin die Eigenschaftswerte gespeichert werden. Ich schreibe sie in meinem Beispiel – ähnlich wie das VFP selbst in den VCX und SCX-Dateien tut – in ein Memofeld untereinander. Dabei ist natürlich zu berücksichtigen, dass ich in das Memofeld nur Strings speichern kann. Es empfiehlt sich, für einen solchen Vorgang eine eigene Grid-Methode anzulegen, die dann jeweils für jede Eigenschaft aufgerufen werden kann und einen String zusammenbaut, der dann schließlich in das Memofeld abgelegt wird.

 

*-- String für Wert bauen

lcType = VARTYPE(tuValue)

DO CASE

   CASE lcType='L'

       lcValue=IIF(tuValue,'.T.','.F.')

   CASE lcType='N'

       lcValue=ALLTRIM(STR(tuValue))

   CASE lcType='C'

       lcValue = '"'+tuValue+'"'

ENDCASE

 

*-- Positionierung im MemoFeld bzw. tcPropString einbauen

lnLine = ATLINE(upper(tcprop)+'=',upper(kitobj.objprop))

 

IF TYPE('tcPropString')='L'

   lcOldString = kitobj.objprop

ELSE

   lcOldString = tcPropString

ENDIF

IF lnLine # 0

   *-- 1. Fall: Eigenschafts-Eintrag existiert schon.

   lcOldLine = MLINE(lcOldString,lnLine)

   IF !tlNonOverwrite

      *-- Mit diesem Parameter wird gesteuert ob ggf. von bestehenden

      *-- Eigenschaften, der Wert überschrieben werden soll.

      lcNewLine = STUFF(lcOldLine,AT('=',lcOldLine)+1,254,lcValue)

      lcNewString = STUFF(lcOldString,AT(lcOldLine,;

         lcOldString),LEN(lcOldLine),lcNewLine)

   ELSE

      lcNewLine = lcOldLine

      lcNewString = lcOldString

   ENDIF

ELSE

   *-- 2. Fall: Eigenschaftseintrag existiert noch nicht.

   lcNewLine = IIF(EMPTY(lcOldString),'',CHR(13))+;

              tcProp + '=' + lcValue

   lcNewString = lcOldString + lcNewLine

ENDIF

 

IF TYPE('tcPropString')='L'

   *ein Parameter, der Steuert, ob diese Eigenschaft nur an

   *einen String gehängt werden soll. Erst bei der letzten Eigenschaft

   *wird abgespeichert in die Tabelle.

   REPLACE objprop   WITH lcNewString IN kitobj

ELSE

   tcPropString = lcNewString

ENDIF

 

Diese Methode sollte erst bei der letzten Eigenschaft in die Tabelle gespeichert werden (Aufruf ohne den

Parameter tcPropString). Dies muss bei der aufrufenden Stelle erledigt werden.

4. Aufgabe: Eigenschaften lesen

1. tcProp                               --> Der Eigenschaftsname

2. tuObject                            --> Die Objektreferenz

3. tnIndex                              --> Index des Objektes in tuObject (optional)

4. tnNewMember                 --> Was ist das Objekt (optional)

                               0 = Objekt existiert bereits

                               1 = Objekt existiert noch nicht

                               2 = For Later use

 

LPARAMETERS tcProp,toObject,tnIndex,tnNewMember

LOCAL lcLine

 

*-- Wenn das Objekt mit übergeben wurde, signalisiert dies,

*-- daß der richtige Datensatz in kitobj noch nicht ausgewählt wurde,

*-- und gesucht werden muß.

*-- Wenn tnIndex mit übergeben wird, existierte das Objekt noch nicht

*-- und es konnte nur dessen Parent als Objekt übergeben werden

 

lcLine = MLINE(kitobj.objprop,;

ATLINE(lower(tcprop),lower(kitobj.objprop)))

 

IF EMPTY(lcLine)

   *-- Die Eigenschaft tcprop ist nicht vorhanden

   RETURN .null.

ELSE

   lcLine = ALLTRIM(SUBSTR(lcLine,AT('=',lcLine)+1))

   RETURN EVAL(lcLine)

ENDIF

5. Aufgabe: Eigenschaften wieder einstellen.

Im Folgenden findet sich die Methode, wie sie nötig wäre, um sämtliche Eigenschaften eines Objekts zu lesen und einzustellen, die in der Objekt-Tabelle abgelegt sind. 

LPARAMETERS toObject,tnIndex

LOCAL lcLine,lcProp,lcCommand,lcParent,lcValue,lcObject

 

*-- Auf den richtigen Datensatz in KITOBJ positionieren

IF !this.objectGetRecord(toObject,tnIndex)

   RETURN .F.

ENDIF

 

IF TYPE('thisform.parent') # 'O'

   *-- Formular ist in keinem Formset enthalten

   lcParent = 'thisform'

ELSE

   *-- Formular ist in Formset enthalten

   lcParent = 'thisformset'

ENDIF

 

*-- Für alle Zeilen des Memofeldes

FOR licount = 1 to MEMLINES(kitobj.objprop)

   lcLine=MLINE(kitobj.objprop,licount)         && Zeile

   lcProp=LOWER(LEFT(lcLine,AT('=',lcLine)-1))  && Eigenschaft

   lcValue=EVAL(SUBSTR(lcLine,AT('=',lcLine)+1))&& Wert der Eigenschaft

   lcObject=SYS(1272,toObject)                  && Objekthierarchie

 

   *-- Für die Bestimmung des ersten Eintrags von lcObject

   *-- muß dieser nun durch thisform oder thisformset ersetzt werden.

   lcObject=STUFF(lcObject,1,AT('.',lcObject)-1,lcParent)

  

   *-- Auszuführendes Kommando zusammenbauen

   lcCommand=lcObject + '.' + lcLine

 

   *-- Wenn die Eigenschaft nicht schreibgeschützt

   *-- und nicht protected ist:

   IF !PEMSTATUS(toObject,lcProp,1)

      IF !PEMSTATUS(toObject,lcProp,2)

           &lcCommand      &&Kommando-Ausführung

      ENDIF

   ENDIF

ENDFOR

Einstellen von Eigenschaften

Aufruf aus Kontext-Menü

Eine Möglichkeit bestimmte Eigenschaften nun den Anwender verändern zu lassen bestünde in Menüpunkten in einem Kontext-Menü (auf der rechten Maustaste).  Im Folgenden Beispiel haben wir Menü erzeugt mit dem Namen _popScm:

DEACTIVATE POPUP _popScm

DEFINE POPUP _popScm ;

   FROM MROW(),MCOL() ;

   MARGIN ;

   RELATIVE ;

   SHORTCUT

  

**** Menü-Definition

DEFINE BAR 1 OF _PopScm  PROMPT 'Beenden'

DEFINE BAR 2 OF _PopScm  PROMPT 'Größe speichern'

ON SELECTION BAR 1 OF _PopScm _screen.activeform.QueryUnload()

ON SELECTION BAR 2 OF _PopScm _screen.activeform.oPsc.Save('Size',;

   _screen.activeform)

 

**** Menü-Auswahl

ON SELECTION POPUP _popScm DEACTIVATE POPUP _popScm

ACTIVATE POPUP _popScm

RELEASE POPUP _popScm

IF BAR()=0

   RETURN .F.

ENDIF

 

In diesem einfachen Kontextmenü gibt es einen Menüpunkt zum Beenden des Formulars und einen zum Abspeichern der Größe des Formulars. Es wird jeweils ein Objekt oPsc angesprochen. Unter diesem Namen ist das Objekt zum Verwalten von Eigenschaften in das Formular hineingelegt. Die angesprochene Methode Save wäre hier die Methode zum Abspeichern der Eigenschaft. Dabei nimmt sie als ersten Parameter die gewünschte Eigenschaft oder aber die Reihe von Eigenschaften entgegen (hier „Size“) und als zweiten Parameter das Objekt, dessen Eigenschaften gespeichert werden sollen. 

Achtung:

Sie können sich bei der ON SELECTION-Anweisung nicht auf thisform. beziehen, wenn Sie eine Formularmethode ansprechen wollen, da der dieser Befehl gewissermaßen von „außen“ (also außerhalb von „thisform“ aus) ausgeführt wird. Daher wird auf das Formular und dessen Methoden mit _screen.activeform referenziert.

Bewegen von Objekten im Formular

Wenn Sie das Bewegen von Objekten im Formular freigeben wollen (und anschließend das Abspeichern der Formularposition), sollten Sie u.U. die Möglichkeit geben, dass die Objekte in einem bestimmten Raster bleiben.

Dazu justieren Sie die neu eingestellte Objektposition nach, indem Sie beispielsweise über die INT()-Funktion den ganzzahligen Wert einer Division durch den Rasterwert verwenden als justierter Objektpositionswert.

Dann geben Sie dem Objekt aktuell die nachjustierten Eigenschaften (von Top, Left, evtl. auch Width, Height) und speichern diese erst danach ab.

Wiederherstellen sämtlicher Objekteigenschaften

Beim Wiederherstellen verwenden Sie die Objects()-Eigenschaft. Hier sind Objektreferenzen auf sämtliche Unterobjekte des Formulars vorhanden. Prüfen Sie dabei bei jedem Objekt, ob es wieder Unterobjekte hat etc.

Bei jedem Objekt müsste nun in der Tabelle gelesen werden, ob ein Datensatz dafür existiert. Ist dies der Fall müssen alle Eigenschaften wieder eingestellt werden unter Verwendung der oben beschriebenen Methoden.