Session D-GRID

Grid ausgetrixt

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


Problematik

VFP erzeugt Objekte aus Basisklassen

Genau wie Optiongroup, Pageframe, (Commandgroup – selten gebraucht), erzeugt das Grid seine notwendigen Unterobjekte selbst und zwar immer aus den Microsoft-Basisklassen. Indem die entsprechende Count-Eigenschaft (OptionCount, PageCount, ColumnCount) auf den entsprechenden Wert gebracht wird, werden die Objekte aus den Basisklassen eingefügt.

Will man jedoch diese Unterobjekte ebenfalls aus eigenen Klassen erzeugen, so hat man zwei Probleme:

  1. Die Klassen lassen sich nicht visuell erzeugen. Sie müssen also auf den Klassendesigner verzichten und stattdessen die Klasse im Code schreiben (Befehl DEFINE CLASS). Dies lässt sich nicht umgehen.
  2. Sie müssen die Objekte an Ihr Grid, Pageframe, Optiongroup programmatisch anhängen (mit oObjekt.Addobject oder oObjekt.NewObject). Damit hat muss man auf die visuelle Bearbeitung verzichten.

Besonders ärgerlich ist dies alles im Grid,

Ein Lösungsvorschlag

Auf der Suche...

Gesucht ist nun eine Lösung, die
  1. es ermöglicht mit eigenen Klassen für Column, deren Control-Klassen und deren Header zu arbeiten.
  2. die dabei mich aber nicht nötigt, auf die visuelle Arbeitung z.B. des Formulardesigners zu verzichen.

 Das Konzept

Solange wir uns entscheiden, den Formulardesigner gebrauchen können zu wollen, steht fest, dass wir zumindest im Design bei den Microsoft-Klassen für Column und Header bleiben müssen. Doch was wir danach machen ist doch „unsere Sache“!

Damit meine ich folgendes: Wir erzeugen eine Grid-Klasse, die standardmäßig folgendes macht:

  1. Wird das Grid als Objekt erzeugt (also das entsprechende Formular gestartet), sorgt der Init-Event des Grids zuerst einmal dafür, dass sämtliche Einstellungen, die im Designer getroffen wurden, d.h. alle Eigenschaften aller Columns und sämtlicher Unterobjekte (darin eingeschlossen: Header) abgespeichert werden.
  2. Danach werden alle Spalten gelöscht.
  3. Anschließend muss nun eine Spalte nach der anderen wieder aufgebaut werden, und zwar nicht durch das hochsetzen des ColumnCount (was wiederum zum automatischen Aufbau von Microsoft-Columns führen würde), sondern buchstäblich mit AddObject! Dabei wird diesmal eine Klasse verwendet, die zuvor in einer Eigenschaft unserer Gridklasse definiert worden sein muss.
  4. Dann erst wird das Grid angezeigt.

Das Speichern der Eigenschaften der Columns und ihrer Unterobjekte

Vorüberlegungen zum Abspeichern von Eigenschaften

1. Frage: Wohin?

Wohin wollen wir nun all diese Eigenschaften speichern? Der vorteilhafteste Weg besteht meiner Ansicht nach im simplen Abspeichern in einer Tabelle, insbesondere darum, weil dies die Möglichkeit in sich birgt, auch nach beenden der Anwendung eventuell inzwischen vom Anwender geeignete Eigenschaften wieder in dieser veränderten Version zu gebrauchen (siehe dazu meine Session mit dem Thema „Customizable Forms mit VFP“). 

2. Frage: Wie?

Hier machen wir es uns nun zunutze, dass im Grunde alle Container-Objekte von VFP (also objekte, die ihrerseits andere Objekte als Unterobjekte verwalten) eine Eigenschaft haben, die in einem Array die Objektreferenz zu sämtlichen Unterobjekten beinhaltet. Im Falle des Grids ist dies die Columns-Eigenschaft. Wir durchlaufen also sämtliche Columns und Speichern deren Eigenschaften. Um eine Liste aller Eigenschaften zu erhalten, können wir die AMEMBERS()-Funktion verwenden. Wenn wir als 1. Parameter einen Array-Namen angeben (das kann natürlich auch eine Array-Eigenschaft unserer Grid-Klasse sein), als 2. Parameter das Objekt, dessen Eigenschaften erfragt werden sollen, und keinen 3. Parameter angeben, erzeugt die AMEMBERS()-Funktion diese Auflistung aller Eigenschaften.

3. Frage: Was?

Was muss nun abgespeichert werden? Es ist nicht unbedingt notwendig, dass sämtliche Eigenschaften abgelegt werden (um Zeit zu sparen), sondern es reicht aus, sich auf diejenigen zu beschränken, deren Wert nicht der Default-Wert ihrer Klasse ist. Um dies abzufragen steht uns die PEMSTATUS()-Funktion zur Verfügung. Wird als 3. Parameter 0 übergeben (als 1. Parameter immer das Objekt, als 2. Parameter immer die Eigenschaft), so ist dies die Information, die sie uns zurückgibt (als logischen Wert).

Sämtliche Columns werden abgespeichert...

Wir haben bereits die Möglichkeit überlegt, Objekt-Eigenschaften in eine Tabelle zu speichern. Hier könnte nun z.B. für jedes Objekt ein Datensatz angelegt werden. Ist die Tabelle noch nicht vorhanden, sollte sie ggf. programmatisch durch die Grid-Klasse erzeugten werden. Sie müsste über ein Feld verfügen, das für eine eindeutige Kennung des Objekts vorgesehen ist. Diese Kennung kann – um zu einer Eindeutigkeit zu gelangen - zusammengesetzt werden aus der SYS(1272)-Funktion, die die Objekthierarchie eines Objekts als String zurückgibt und der SYS(1271)-Funktion (Name der ausgeführten SCX-Datei).

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. Ein Beispiel dafür findet sich in der Beschreibung der Session „Customizable Forms“.

Dieser Vorgang müsste nun für alle Objekte aller Spalten wiederholt werden. Um dies zu bewerkstelligen könnte für jede Spalte eine Grid-Methode (z.B. „ColumnSaveProp“) verwendet werden, der die aktuelle Spalte als Parameter toCol übergeben wird:

FOR licount1 = 0 to toCol.ControlCount

   *Alle Objekte der Spalte werden durchlaufen

   IF licount1 = 0

      *Erst mal die Spalte selbst.

      loObject=toCol

      *-- Findet oder erzeugt den entsprechenden Datensatz in kitObj

      *-- für dieses Objekt (Column- oder Column-Member-Objekt)

      IF !this.ObjectGetRecord(loObject,tnIndex)

            *Eine Methode, die den entspr. Datensatz in der

            *Objekt-Tabelle ausfindig macht.

            RETURN .F.

      ENDIF

   ELSE

     loObject=toCol.Controls(licount1)

     IF !this.ObjectGetRecord(loObject,liCount1)

           RETURN .F.

     ENDIF

   ENDIF

Es wird also jeweils geprüft, ob das aktuelle Objekt das als Parameter übergebene Column-Objekt ist, oder eines seiner Unterobjekte. Das Objekt, dessen Eigenschaften nun abgespeichert werden sollen erhält die Objektreferenz loObject. Für dieses Objekt muss der Datensatz der Objekttabelle gefunden oder angelegt werden, wo gespeichert werden soll  (s.o.: SYS(1272 und SYS(1271)).

Anschließend müsste ein Array zusammengestellt werden mit allen Eigenschaften, die dann nacheinander gelesen werden:

   LOCAL laProp(1,1)

      =AMEMBERS(laProp,loObject)

  

   lcString = kitobj.objprop

   FOR licount2 = 1 to ALEN(laProp)

      *-- Hier werden nun sämtliche angegebene Eigenschaften

      *-- des Objekts loObject durchlaufen und ggf. in

      *-- den String fürs Memo-Feld gespeichert.

      lcProp=laProp[licount2]

      luValue=loObject.&laProp[licount2]

 

      *-- Nur wenn kein Default-Wert soll Eigenschaft mit

      *-- abgelegt werden!

      *-- ControlCount erscheint immer als Defaultwert,  muß also

      *-- immer abgespeichert werden.

      IF PEMSTATUS(loObject,lcProp,0) ;

         OR INLIST(lower(lcProp),'controlcount','class','controlsource')

        

         IF !this.PropertyWrite(lcProp,luValue,,@lcString,.t.)

            *Methode zum Schreiben der Eigenschaft und ihrer Werte

            *in einen String.

            RETURN .F.

         ENDIF

      ENDIF

   ENDFOR

   *-- Sämtliche Eigenschaften, die nun in den String gelesen wurden

   *-- sind hier für DIESES OBjekt (loObject) abgelegt worden.

   REPLACE kitobj.objprop  WITH lcString IN 'kitobj'

   *nächstes Objekt.

ENDFOR

Das Löschen und neu Aufbauen der Spaltenobjekte

Nachdem alle Eigenschaften nun gelesen wurden, können die Spaltenobjekte gelöscht werden. Dies geschieht sehr einfach, indem die ColumnCount-Eigenschaft des Grids auf 0 gesetzt wird. Vorher müssen wir uns den ursprünglichen ColumnCount merken, damit wir wissen, wie viele Spalten wieder aufgebaut werden müssen. Anschließend können wir nun die Spalten aufgrund der eigenen Klassen, die in einer Grid-Eigenschaft definiert worden sein müssen, wiederaufbauen. Für alle Spalten müsste eine Methode „ColumnCreate“ aufgerufen werden.

LPARAMETERS tnColumnIndex

LOCAL licount,liControlCount,loObject,loCol,lcName,lcClass,

LOCAL lcHeaderName,lcTextboxName

LOCAL lcCurrentControl,loHeader

 

licontrolCount = this.PropertyGet('ControlCount',this,tnColumnIndex,1)

IF ISNULL(licontrolcount)

   *-- Kein Eintrag vorgefunden (Default)

   licontrolcount = 2

ENDIF

 

Zuerst müssen wir feststellen, wie viele Unterobjekte die Spalte zuvor gehabt hatte. Dies stand in der Eigenschaft ControlCount, die wir auf die gleiche Weise auslesen, wie später auch alle anderen Eigenschaften: mit einer dafür vorgesehenen Methode PropertyGet, die das entsprechende Memofeld ausliest und den Wert einer bestimmten Eigenschaft zurückgibt. Nun werden sämtliche Controls durchlaufen, wieder angefangen mit der Spalte selbst (licount = 0).

Für jedes Objekt muss zuerst

  1. Der Name herausgefunden werden.
  2. Das Parent-Objekt (zuerst nur „this“, dann „column“)

Bekannt ist bereits der Member-Index (entweder Spaltennummer oder Objektnummer) und z.T. die Klasse aus der erzeugt werden muss: Sie ist bei Column/Header/Textboxen festgelegt in Grid-Objekteigenschaften. Bei weiteren Controls müsste auch die Klasse gelesen werden aus der Objekttabelle.

FOR licount = 0 to liControlCount

   DO CASE

     CASE licount = 0

          *-- Column-Objekt selbst anlegen

          lcName = this.PropertyGet('name',this,tnColumnIndex,1)

          *-- 4. Parameter = 1, wir suchen Eigenschaft

          *-- eines nicht existierenden Objektes

 


 

          this.addobject(lcName,this.cColClassColumn)

         loObject = this.&lcName

         loCol = loObject

         IF !this.propertyResetAll(loObject,tnColumnIndex)

            RETURN .F.

         ENDIF

   CASE licount = 1     && Header-Objekt anlegen

         lcName = this.PropertyGet('name',loCol,licount,1)

         lcHeaderName = lcName && wird noch gebraucht

         *-- Name im Column-Objekt ablegen

         loCol.cHeaderName = lcName

         loCol.Addobject(lcName,this.cColClassHeader)

         loObject = loCol.&lcName

        

         IF !this.propertyResetAll(loObject,liCount)

            RETURN .F.

         ENDIF

          

   CASE licount = 2  && Textbox in Spalte anlegen

         lcName = this.PropertyGet('name',loCol,licount,1)

         lcTextboxName = lcName && wird noch gebraucht.

         loCol.Addobject(lcName,this.cColClassTextbox)

         loObject = loCol.&lcName

        

         IF !this.propertyResetAll(loObject,liCount)

            RETURN .F.

         ENDIF

   OTHERWISE

         *-- Alle anderen Objekte

         lcName = this.PropertyGet('name',loCol,licount,1)

         lcClass = this.PropertyGet('class')   

         loCol.Addobject(lcName,lcClass)

         loObject = loCol.&lcName

       

         IF !this.propertyResetAll(loObject,liCount)

            RETURN .F.

         ENDIF

   ENDCASE

ENDFOR

 

Erst nachdem nun sämtliche Unterobjekte angelegt wurden, darf die CurrentControl-Eigenschaft der Spalte angelegt werden:

lcCurrentControl = this.propertyget('currentcontrol',loCol)

IF !ISNULL(lcCurrentControl)

   loCol.CurrentControl = lcCurrentControl

ENDIF

 

*-- Das ist das Wichtigste, ansonsten sehen wir gar nichts!

loCol.&lcTextboxName..visible=.t.

loCol.visible = .t.

 

Damit wäre das Grid wieder aufgebaut!

 

Übersicht über Speichern / Löschen / Neuaufbau

 

 

 

Zur Veranschaulichung des technischen Ablaufs beim Austausch der Spalten und Erzeugen aus anderen Spaltenklassen, hier mit den Methoden-Bezeichnungen wie ich sie in der Gridklasse von ClassMaxX verwendet habe, wo eine solche Funktionalität bereits realisiert ist.

Spalten-Funktionalität

Nachdem wir die obige Technik realisiert haben, fällt es nun sehr leicht, dass Benutzer-Einstellungen ebenfalls abgespeichert werden. So könnte die Veränderung der Spaltengröße jeweils auf gleiche Weise in die Objekt-Tabelle als Width-Eigenschaft der Spalte abgelegt werden. Dies müsste nun im Code der Spaltenklasse realisiert werden.

Es wurde schon erwähnt, dass Columns, Options, Pages Basisklassen sind, die es nicht erlauben, visuell bearbeitet zu werden. Wir müssen uns hier also ein kleines PRG anlegen, in das wir unsere Klassendefinition als Code schreiben. Dieses PRG muss mit SET PROC TO angegeben werden, damit die Klasse auch gefunden wird (oder man muss mit der NEWOBJ()-Funktion bzw. NewObject-Methode arbeiten).

 

DEFINE CLASS kicolumn AS column

   *-- Neue Eigenschaften

   cHeaderName = ''

   nColumnIndex = 0

   * Nur Programmatisch notwendig um nach Resize

   * den Click zu verhindern.

 

   PROCEDURE Init

      *-- Normalerweise wird automatisch beim Erzeugen einer Column dazu

      *-- ein "Header1" und "Text1" hinzugefügt.

      *-- Diese müssen hier entfernt werden. Unter bestimmten Umständen

      *-- geschieht dies jedoch nicht, darum noch diese IF-Abfrage.

      IF TYPE('this.header1')='O'

         this.removeobject('Header1')

      ENDIF

      IF TYPE('this.text1')='O'

         this.removeobject('Text1')

      ENDIF

 

      this.nColumnIndex = this.ColumnOrder

   ENDPROC

 

Später kann durch verschieben auch mal die ColumnOrder anders werden. Unter ColumnIndex kennt das Grid aber diese Spalte über seine Columns(n)-Eigenschaft noch immer.

Problem: Wenn im INIT des Grids bereits die Columns neu aufgebaut wurden, sind (z.B. bei Neuaufbau der 1. Column) die anderen Columns noch da und folglich auch die ColumnOrder noch auf z.B. 5 (beim neu Hinzufügen hängt sich hinten dran).

Darum wird in ColumnCreate dieser nColumnIndex nochmals zugewiesen, da dort der effektive nColumnIndex bekannt ist.

Die eigentliche Resize-Methode ist nun ganz einfach: Wir rufen die fertige Methode zum Schreiben von Eigenschaften auf:

 

   PROCEDURE Resize

      this.lDoSetOrder = .f.

      this.parent.PropertyWrite('Width',this.width,this)

      NODEFAULT

   ENDPROC

 

ENDDEFINE