Dynamische Layouts

15.04.2006 von Lorenz  Hölscher
Es ist sehr praktisch, in einer Datenblattansicht die Spalten selber anordnen zu können. Ein so geändertes Layout speichert Access sogar dauerhaft, so dass es beim nächsten Aufruf der Tabelle oder des Formulars bereits eingestellt ist. Das allerdings führt in Mehrbenutzerumgebungen unweigerlich zu Missstimmung, denn jeder überschreibt des anderen Lieblings-Layout. Die hier vorgestellte Lösung zeigt, wie Sie solche Layouts clever verwalten.

In einem Layout sollen ein beliebiger Name sowie eine Auflistung der vorhandenen Spalten mit ihren wesentlichen Eigenschaften wie Position, Breite und so weiter enthalten sein. Das ist also eine klassische 1:n-Beziehung zwischen den Tabellen tblLayouts und tblSpalten.

Allerdings kann nicht jedes Layout überall aufgerufen werden, weil ja unterschiedliche Spalten enthalten sind. Daher muss der Name des Formulars beziehungsweise der Tabelle auch gespeichert werden.

Wegen der Mehrbenutzerumgebung sollen die Layouts zudem in allgemeine und persönliche unterteilt werden. Allgemeine Layouts können zwar von allen benutzt, aber nicht verändert werden, während persönliche Layouts nur vom jeweiligen Ersteller gespeichert, aufgerufen und gelöscht werden können.

Dazu wird außer dem Formular-/Tabellen-Namen auch der Benutzername gespeichert. Für allgemeine Layouts wird dieser Eintrag einfach manuell in tblLayouts gelöscht, um den Aufwand hier zu Demozwecken gering zu halten.

Layouts unterscheiden

Da allgemeine und persönliche Layouts getrennt behandelt werden sollen, werden zwei Abfragen vorbereitet. Sie finden den SQL-Text in den Listings 1 und 2.

SELECT LID, LObjektname, LName, LHashwert
FROM tblLayouts
WHERE (((LBenutzer) Is Null))
ORDER BY LObjektname, LName;

SELECT LID, LObjektname, LName, LHashwert, LBenutzer
FROM tblLayouts
WHERE (((LBenutzer)=CurrentUser()))
ORDER BY LObjektname, LName;

Die Abfragen unterscheiden sich nur darin, dass entweder der Eintrag in LBenutzer leer sein oder dem aktuell angemeldeten Benutzer entsprechen muss.

Der Rückgriff auf CurrentUser() bedeutet natürlich, dass eine gesicherte Anmeldung mit Zugriffsrechten stattgefunden hat. Bei einer Mehrbenutzerumgebung ist dies aber ohnehin zu empfehlen.

Viele Stolpersteine

Bei der Entwicklung dieser Layout-Verwaltung sind übrigens wieder einige VBA-Merkwürdigkeiten Tabelle offenbar intern ein Formular in der Datenblatt-Ansicht aufmacht.

Dazu passt nicht nur die Tatsache, dass die gleichen Eigenschaften im Überwachungsfenster des VBA-Editors einschließlich Formname vorhanden sind, sondern auch, dass eine Typüberprüfung mit If TypeOf Screen .ActiveDatasheet Is Form Then zum Ergebnis True kommt.

Im Widerspruch dazu steht jedoch, dass eine geöffnete Tabelle nicht als Screen.ActiveForm ansprechbar ist und zu einem Laufzeitfehler führt. Ärgerlicherweise fehlt das hier offenbar benötigte Screen.ActiveTable-Objekt sogar ganz.

Was bin ich?

Um also die notwendigen Informationen ermitteln zu können, braucht es ganz andere Objekte, wie in Listing 3 zu sehen ist:

Function ObjCurrent() As Object
On Error GoTo Mist
Select Case Application.CurrentObjectType
Case acTable
Set ObjCurrent = Screen.ActiveDatasheet
Case acForm
Set ObjCurrent = Screen.ActiveForm
Case Else
End Select
Exit Function
Mist:
Set ObjCurrent = Nothing
End Function

Tatsächlich können Sie mit Application.CurrentObjectType recht zuverlässig den gewünschten Typ des geöffneten Fensters ermitteln.

Ja, Sie haben richtig gelesen: nur „recht“ zuverlässig! Wenn gerade das Datenbankfenster mit einem markierten Formular geöffnet ist, ermittelt diese Eigenschaft ein Formular. Aber Screen.ActiveForm scheitert direkt anschließend mit einem Laufzeitfehler, weil es eben doch keines ist.

Daher enthält die Funktion ObjCurrent die Fehlerbehandlung und wird explizit auf Nothing gesetzt, wenn es weder Formular noch Tabelle ist.

Nun ist damit zwar diese erste Klippe umschifft, aber fragen Sie mich bitte nicht, warum CurrentObjectType überhaupt eine Eigenschaft des Application-Objekts ist. Schließlich gibt es für Bildschirm-Objekte und deren Eigenschaften ja genau das Screen-Objekt.

Immerhin gibt es dazu wenigstens noch die CurrentObjectName-Eigenschaft, die auch den passenden Namen liefert. Na ja, außer wiederum im Datenbankfenster, denn da gibt es lediglich den Namen des markierten Objekts zurück.

Layout speichern

Nachdem also wenigstens einigermaßen klar ist, welches Fenster eigentlich gerade geöffnet ist, sollen dessen Spalten-Eigenschaften gesichert werden. Wie in Listing 4 zu sehen ist, schreiben Sie zuerst den Masterdatensatz in tblLayouts.

Sub Speichern()
Dim objActive As Object
Dim ctlControl As Control
Dim varID As Variant
Dim strObjName As String
Dim strName As String
Dim strNamePlus As String
Dim strSQL As String
Dim lngID As Long
strObjName = Application.CurrentObjectName
strName = InputBox("Bitte geben Sie den neuen Namen ein:", _
"Layout speichern unter")
If strName = "" Then 'also Abbrechen
Exit Sub
End If
strNamePlus = Replace(strName, "'", "'")
varID = DLookup("LID", "qryLayoutsPersoenlich", _
"LName = '" & strNamePlus & "' AND " & _
"LObjektname = '" & strObjName & "'")
If Not IsNull(varID) Then
MsgBox "Der Layout-Name '" & strName & _
"' ist schon vorhanden!", vbCritical
Exit Sub
End If
strSQL = "INSERT INTO tblLayouts " & _
"(LName, LObjektname, LHashwert, LBenutzer) " & _
"VALUES ('" & strNamePlus & "', '" & strObjName & "', '" & _
RechneHashwert() & "', " & CurrentUser() & ")"
CurrentDb.Execute strSQL, dbFailOnError
[...weiter Listing Fehler! Verweisquelle konnte nicht gefunden werden.]

Zuerst wird der gewünschte Name per Input- Box ermittelt, um dann sicherheitshalber darin enthaltene Hochkommata zu maskieren, also zu verdoppeln. Andernfalls gäbe es Konflikte mit der SQL-Syntax, die dieses Zeichen als String-Ende interpretieren würde.

Bild 1: Definition von tblLayouts mit Mehrfachindex.

Damit keine schon vorhandenen Layoutnamen erneut benutzt werden können, wird mit DLookup geprüft, ob für dieses Objekt schon der gewählte Name vorhanden ist. Die LID ist Null, wenn der Name noch frei ist. Danach können Sie mit einer Anfüge-Abfrage die Layout-Daten in tblLayouts schreiben.

Das funktioniert auch mit verknüpften Tabellen, beispielsweise zu Oracle, deren LID im Gegensatz zu Access ja erst nach dem Speichern eines Datensatzes zur Verfügung steht. Der LHashwert ist zurzeit noch ohne Bedeutung und könnte auch einen Leerstring enthalten.

Spalten speichern

Zu den Layoutdaten müssen anschließend die Eigenschaften aller Spalten gespeichert werden. In Listing 5 schreibt eine Schleife über alle Controls deren benötigte Werte in tblSpalten.

[...Fortsetzung von Listing Fehler! Verweisquelle konnte nicht gefunden werden.]
lngID = DLookup("LID", "qryLayoutsPersoenlich", _
"LName='" & strNamePlus & "' AND " & _
"LObjektname = '" & strObjName & "'")
Set objActive = ObjCurrent()
For Each ctlControl In objActive.Controls
If IstTypKorrekt(ctlControl) Then
strSQL = "INSERT INTO tblSpalten " & _
"(SLIDRef, SFeldname, SBreite, " & _
"SPosition, SFixiert, SAusgeblendet) " & _
"VALUES (" & lngID & ", '" & _
ctlControl.Name & "', " & _
ctlControl.ColumnWidth & ", " & _
ctlControl.ColumnOrder & ", " & _
CInt(ctlControl.ColumnOrder < _
objActive.FrozenColumns) & ", " & _
CInt(ctlControl.ColumnHidden) & ")"
CurrentDb.Execute strSQL, dbFailOnError
End If
Next
MsgBox "Layout '" & strNamePlus & "' für '" & _
strObjName & "' wurde gespeichert.", _
vbInformation
End Sub

Bild 2: Definition von tblSpalten.

Beim Speichern der Daten wird pro Spalte ein Datensatz mit den gewünschten Informationen (Name, Breite, Reihenfolge, Ausgeblendet) und die Referenz-ID auf den Layoutnamen eingetragen.

Berechnung des Hash-Werts (Prüfsumme)

Da später herausgefunden werden soll, welches Layout gerade aktiv ist, benötigen Sie ein Erkennungszeichen. Die einfachste Methode besteht darin, alle ermittelten Spalten-Informationen in einer riesigen Zeichenkette hintereinander abzulegen. Bei maximal 255 Spalten mit je fünf Daten und einem Trennzeichen (also maximal 82 Zeichen, siehe Tabelle 1) benötigt diese Information 20.910 Zeichen.

Tabelle 1: Länge des Hash-Werts berechnen

Wert

Eigenschaft

max. Größe

Anzahl Zeichen

Feldname

Name

max. 64 Zeichen

64

Breite

ColumnWidth

< 100.000

6

Reihenfolge

ColumnOrder

<=255

3

Fixiert

<errechnet>

0/-1

2

Ausgeblendet

ColumnHidden

0/-1

2

Trennzeichen

je 1

5

Summe: 82

Eine Reduktion auf weniger Informationen wie etwa eine Quersumme ist nicht machbar, denn dann würde ColumnOrder = 10 und ColumnWidth = 15 zum gleichen Ergebnis führen wie ColumnOrder = 15 und ColumnWidth = 10.

Auch ein Verzicht auf die Trennzeichen ist zu unsicher, denn dann wären die Werte 121/15 mit 12/115 zu verwechseln. Selbst wenn die Feldnamen kürzer und die Spaltenbreiten deutlich kleiner sind, muss es also ein Memofeld sein. Da es sich aber um eher weniger Datensätze mit Hash- Werten handelt, sind die dabei entstehenden Datenmengen zu vernachlässigen. Listing 6 zeigt,wie dieser eindeutige Erkennungswert als Zeichenkette ermittelt wird.

Function RechneHashwert() As String
Dim objActive As Object
Dim ctlControl As Control
Dim strWert As String
Set objActive = ObjCurrent()
strWert = ""
For Each ctlControl In objActive.Controls
If IstTypKorrekt(ctlControl) Then
strWert = strWert & ctlControl.ColumnWidth & "_"
strWert = strWert & ctlControl.ColumnOrder & "_"
strWert = strWert & CBool(ctlControl.ColumnHidden) & "_"
End If
Next
RechneHashwert = strWert
End Function

Control-Typ prüfen

Bei den Layout-geeigneten Ansichten handelt esmsich jedoch nicht nur um Tabellen, sondern auch um Formulare in der Datenblatt-Ansicht. Das bedeutet aber, dass auch Buttons oder Labels oder ähnliche Typen von Kontrollelementen vorhanden sein können.

Selbst wenn in der Datenblatt-Ansicht nur editierbare Controls angezeigt werden, so scheitert doch eine For-Each-Schleife daran, dass alle Controls untersucht würden. Nicht editierbare Controls besitzen keine ColumnWidth-Eigenschaft. Dies führt bei einem Schleifendurchlauf zu einem Laufzeitfehler.

Bild 3: Tabelle mit den Layoutnamen.

Daher wird mit der Funktion IstTypKorrekt aus Listing 7 nur dann True zurückgegeben, wenn es sich um editierbare Controls wie Textboxen, Comboboxen oder Listboxen handelt.

Function IstTypKorrekt(ctlControl As Control) As Boolean
If TypeOf ctlControl Is TextBox Or _
TypeOf ctlControl Is ComboBox Or _
TypeOf ctlControl Is ListBox Then
IstTypKorrekt = True
Else
IstTypKorrekt = False
End If
End Function

Jetzt können Sie sicher sein, dass sich der Hash-Wert auch nur aus den benötigten Controls errechnet und es keine Laufzeitfehler gibt.

Layout aufrufen

Natürlich sollen die gespeicherten Layouts auch wieder benutzt werden können. Dazu müssen Sie im Grunde nur das Gegenstück zum Speichern schreiben, also anhand einer LID die zugehörigen Daten für die Spalten abrufen. In Listing 8 sehen Sie den zugehörigen Code.

Sub LayoutAnzeigen()
Dim RS As Recordset
Dim objActive As Object
Dim ctlControl As Control
Dim strSQL As String
Dim lngID As Long
Set objActive = ObjCurrent()
lngID = CommandBars.ActionControl.Tag
objActive.Controls(0).SetFocus
DoCmd.RunCommand acCmdUnfreezeAllColumns
For Each ctlControl In objActive.Controls
If IstTypKorrekt(ctlControl) Then
ctlControl.ColumnHidden = False
ctlControl.ColumnWidth = -1
End If
Next
Set RS = CurrentDb.OpenRecordset( _
"SELECT * FROM tblSpalten WHERE SLIDRef = " & lngID & _
" ORDER BY SPosition ASC", dbOpenDynaset)
Do Until RS.EOF
With objActive.Controls(RS.Fields("SFeldname").VALUE)
.ColumnOrder = RS.Fields("SPosition").VALUE
.ColumnHidden = RS.Fields("SAusgeblendet").VALUE
.ColumnWidth = RS.Fields("SBreite").VALUE
End With
RS.MoveNext
Loop
RS.MoveFirst
Do Until RS.EOF
With objActive.Controls(RS.Fields("SFeldname").VALUE)
If RS.Fields("SFixiert").VALUE Then
DoCmd.RunCommand acCmdFreezeColumn
End If
End With
RS.MoveNext
Loop
End Sub

Die SLIDRef ist in der Tag-Eigenschaft des Menüeintrags gespeichert, so dass Sie direkt per SQL-WHERE-Klausel bei OpenRecordset nur diejenigen Spaltenwerte aufrufen können, die für dieses Layout gespeichert wurden.

Ein bisschen lästig ist, dass die Eigenschaften sich auch beim Zuweisen anders verhalten, als die Access Hilfe es verspricht. Daher muss in der ersten Schleife jede Spalte auf sichtbar (ColumnHidden = False) mit einer automatischen Spaltenbreite (ColumnWidth = -1) gestellt werden. Erst anschließend werden die tatsächlichen Werte zugewiesen. Und erst in einer dritten Schleife können die Spalten, die am Anfang stehen, fixiert werden. Dies ist deshalb der Fall, weil sich in der Reihenfolge vor einer fixierten Spalte keine unfixierte befinden darf.

Test-Menü erzeugen

Wie Sie vielleicht bemerkt haben, lässt sich diese Prozedur ausschließlich aus einem Menüeintrag heraus aufrufen, da lngID dessen Tag-/Marke- Eigenschaft ausliest. Zu Testzwecken können Sie natürlich vorübergehend lngID = 4 schreiben, dann ist es auch mit [F5] zu starten, aber der Aufruf soll schon bequem per Menü erfolgen. Erstellen Sie also mit Extras/Anpassen zuerst eine neue Menüleiste namens TEST und darin aus der Kategorie Neues Menü den Befehl Neues Menü. Diesen benennen Sie direkt um in Testmenü.

Die eigentlichen Menüeinträge sind hier vom Typ Alle Tabellen, wobei es egal ist, welchen Tabellennamen Sie benutzen. In Access fehlen nämlich die allgemeinen Menüeinträge aus den anderen Office Programmen. Stattdessen gibt es spezialisierte Versionen wie hier die Tabellenaufrufe.

Menü-Eigenschaften

Sie müssen also, wie in Bild 5 zu sehen, die Eigenschaften nachtragen. Dabei geht es vor allem um Bei Aktion (in VBA: OnAction), welche immer die Funktion LayoutAnzeigen aufruft, und Marke (in VBA: Tag) mit der LID aus der Tabelle tblLayouts. Die Beschriftung des Menüeintrags ist dabei beliebig, sollte aber sinnvollerweise dem gespeicherten Namen des Layouts entsprechen.

Jetzt können Sie die Tabelle/Datenblattansicht anzeigen, für die das Layout erstellt wurde, und per Menü umschalten. Damit es deutlich flexibler wird, müssen die Menüeinträge ebenso wie die Layouts dynamisch ermittelt werden. Über dynamische Menüs erfahren Sie mehr in einem separaten Beitrag in der folgenden Ausgabe von Expert´s inside Access.