LÖSUNGEN

Dynamische Menüs

15.05.2006
Bei größeren Datenbanken ergibt sich sehr schnell die Notwendigkeit, Menüeinträge dynamisch zu erzeugen, um auf bestimmte Vorbedingungen zu reagieren. Das kann die Deaktivierung einzelner Einträge wegen fehlender Rechte oder die Umbenennung je nach vorhandenen Daten sein. Dieser Artikel erläutert den Umgang mit solchen Menüs, die erst zur Laufzeit erzeugt werden.
Bild 1: Beispiel eines dynamischen Menüs (vgl.auch Bild 4).

Ausgangspunkt für das benutzte Beispiel ist der Artikel „Dynamische Layouts“ in Ausgabe 4/2006 von Expert´s inside Access, der den Aufruf selbstdefinierter Layouts aus einem Menü heraus erlaubt. Dabei soll jeweils ein tblLayouts-Datensatz einen Menüeintrag ergeben, so dass kein Umweg über einen Dialog mit Listenfeld nötig ist.

Wann wird das Menü aktualisiert?

Das erste Problem dabei ist, wann das Menü aktualisiert wird. Ganz einfach: so spät wie möglich! Das bedeutet, dass nicht dann aktualisiert wird, wenn sich die Bedingungen ändern (also beispielsweise beim Löschen oder Hinzufügen eines Datensatzes). Vielmehr muss das Menü erst dann korrekt sein, wenn es benutzt werden soll.

Wie jeder Menüeintrag verfügt auch ein Menütitel über die Fähigkeit, beim Klick eine Prozedur aufzurufen. Diese macht für dynamische Menüs nichts anderes, als das Menü schnell zu erzeugen, bevor es dann angezeigt wird.

Nachdem Sie in einem Modul eine Prozedur wie in Listing 1 geschrieben haben, lässt sich diese wie in Bild 2 über Extras Anpassen diesem Menütitel als Wert Bei Aktion ebenso zuweisen wie jedem anderen Menüeintrag. Lassen Sie sich nicht davon irritieren, dass diese Prozedur in der Liste nicht angeboten wird, es funktioniert trotzdem.

Sub MenuTitelPiept()
Beep
End Sub

Bild 2: Ein Menütitel ruft eine Aktion auf.

Letztendlich wird damit natürlich nicht ein einfaches Piepen ausgelöst, sondern das Untermenü erstellt, welches anschließend ausgeklappt wird. Die einzige Bedingung ist, dass der Msg Box-Befehl nicht erlaubt ist, weil er dann statt des Untermenüs erscheint.

Menü dynamisch aufrufen

Die tatsächliche Aktion wird die Prozedur ErzeugeMenue sein, welche Sie entweder in den Steuerelement-Eigenschaften wie in Bild 2 ändern oder per VBA-Code wie in Listing 2 erzeugen. Damit der Code funktioniert, muss

Option Compare Database
Option Explicit
Dim booGefunden As Boolean
Const conMenueName = "Dynamisch"
Const conMenueTitel = "&Layouts"
Sub EinmalMenueVorbereiten()
Dim barMenu As CommandBar 'MS-Office Bibliothek einbinden!
Set barMenu = CommandBars(conMenueName)
barMenu.Controls(conMenueTitel).OnAction = "ErzeugeMenue"
End Sub
Sub ErzeugeMenue
'noch leer
End Sub

Damit kennen Sie auch schon wesentliche Teile des notwendigen Codes, um die übrigen Funktionalitäten für ein dynamisches Menü programmieren zu können.

Die beiden modul-öffentlichen Konstanten conMenueName für den Symbolleistennamen und conMenueTitel für den Menütitel erleichtern den Zugriff in den Prozeduren und vermeiden Schreibfehler.

Menüeinträge löschen

Es mag Sie vielleicht überraschen, dass das Erzeugen neuer Menüeinträge mit dem Löschen von alten beginnt. Andernfalls wird das Menü immer länger, denken Sie also rechtzeitig an das Aufräumen.

Listing 3 zeigt, welche Variablen bereitgestellt werden müssen und wie die vorhandenen Menüeinträge mit einer einfachen Schleife gelöscht werden können.

Sub ErzeugeMenue()
Dim RS As Recordset
Dim barMenu As CommandBar 'MS-Office Bibliothek einbinden!
Dim ctlMenuItem As CommandBarButton
Dim strObjName As String
Dim i As Integer
Set barMenu = CommandBars(conMenueName).Controls(conMenueTitel).CommandBar
For i = 1 To barMenu.Controls.Count
barMenu.Controls(1).Delete
Next
' weiter in Listing 4

Beachten Sie bitte, dass in der Schleife ausdrücklich immer der erste Menüeintrag gelöscht wird. Falls Sie dort stattdessen barMenu.Controls(i).Delete schreiben würden, würde nur jeder zweite Eintrag entfernt und nach der Hälfte der Schleife ein Laufzeitfehler auftreten.

Menüeinträge erstellen

Bevor neue Menüeinträge hinzugefügt werden, sollte der Code zuerst überprüfen, ob überhaupt ein für das Layout geeigneter Fensterinhalt vorliegt. In einem Datenbankfenster oder einem Bericht etwa lassen sich gar keine Spalten bearbeiten. Daher prüft Listing 4 zuerst, ob die benutzerdefinierte Funktion ObjCurrent mit dem Wert Nothing kein geeignetes Objekt zurückgibt. Dann wird nur der deaktivierte Menüeintrag <keine erlaubt> ohne OnAction-Eigenschaft erzeugt.

Die eigentliche Erstellung neuer Menüeinträge wird der Übersichtlichkeit halber in zwei Prozeduren NeuerEintrag und NeueGruppe ausgelagert.

...Fortsetzung von Listing 3

If ObjCurrent() Is Nothing Then
Set ctlMenuItem = NeuerEintrag(barMenu, _
0, "<keine erlaubt>", "", False)
Beep
Else
Set ctlMenuItem = NeuerEintrag(barMenu, _
0, "&Löschen...", "Loeschen", True)
Set ctlMenuItem = NeuerEintrag(barMenu, _
0, "Speichern &unter...", "Speichern", True)
strObjName = Application.CurrentObjectName
booGefunden = False
NeueGruppe barMenu, "Allgemeine für '" & _
strObjName & "':", "qryLayoutsAllgemein"
NeueGruppe barMenu, "Persönliche von '" & CurrentUser() & "' für '" & _
strObjName & "':", "qryLayoutsPersoenlich"
If Not booGefunden Then
Set ctlMenuItem = NeuerEintrag(barMenu, _
0, "<Benutzerdefiniert>", "", True)
ctlMenuItem.State = msoButtonDown
End If
End If
End Sub

Neuer Menüeintrag

Die wesentlichen Eigenschaften eines neuen Menüeintrags werden der Funktion NeuerEintrag als Parameter mitgegeben, wie Listing 5 zeigt.

Function NeuerEintrag(barMenu As CommandBar, _
strIDTag As String, strCaption As String, strAction As String, _
booEnabled As Boolean) As CommandBarButton
Set NeuerEintrag = barMenu.Controls.Add()
With NeuerEintrag
.Tag = strIDTag
.Caption = strCaption
.OnAction = strAction
.Enabled = booEnabled
End With
End Function

Im Tag wird unsichtbar die eindeutige ID des gewünschten Datensatzes gespeichert, da die OnAction-Eigenschaft in den meisten Fällen die Prozedur LayoutAnzeigen aufruft, die wiederum die Details dieses bestimmten Datensatzes tbl Layouts benötigt.

In den übrigen Menüeinträgen, beispielsweise für das Löschen oder Speichern in dieser Tabelle tblLayouts, wird als ID im Tag einfach eine 0 eingetragen und nicht weiter benutzt. Das Deaktivieren eines Menüeintrags ist erforderlich für den Eintrag <keine erlaubt> sowie die Zwischenüberschriften der jeweiligen Gruppen.

Neue Gruppe

Nach den beiden grundsätzlichen Funktionalitäten Löschen und Speichern unter folgen einfach alle Datensätze der Tabelle tblLayouts, allerdings unterteilt in allgemeine und benutzerspezifische.

Bild 3 zeigt die Ergebnisse der beiden Abfragen, die als allgemeine Layouts solche mit LBenutzer Is Null und als persönliche Layouts solche mit LBenutzer = CurrentUser() unterscheiden.

Bild 3: Getrennte Abfragen für tblLayouts, mit oder ohne LBenutzer.

Die Funktion NeueGruppe wie in Listing 6 errechnet zuerst eine eindeutige Kennung als Hashwert für das aktuelle Layout, damit der zugehörige Menüeintrag als State = msoButton-Down auch gleich mit einem Häkchen versehen werden kann.

Function NeueGruppe(barMenu As CommandBar, strCaption As String, _
strQueryName As String)
Dim RS As Recordset
Dim ctlMenuItem As CommandBarButton
Dim strHash As String
strHash = RechneHashwert()
Set ctlMenuItem = NeuerEintrag(barMenu, _
0, strCaption, "", False)
ctlMenuItem.BeginGroup = True
Set RS = CurrentDb.OpenRecordset("SELECT * FROM (" & _
strQueryName & ") WHERE LObjektname = '" & _
Application.CurrentObjectName & "'")
Do Until RS.EOF
Set ctlMenuItem = NeuerEintrag(barMenu, _
RS.Fields("LID").VALUE, _
RS.Fields("LName").VALUE, "LayoutAnzeigen", True)
If strHash = RS.Fields("LHashwert").VALUE Then
ctlMenuItem.State = msoButtonDown
booGefunden = True
End If
RS.MoveNext
Loop
End Function

Dann beginnt die Gruppe mit einer deaktivierten Überschrift, damit für den Benutzer ersichtlich ist, welche Layouts anschließend angezeigt werden. Unter Windows wäre dafür zwar eher ein Untermenü üblich, das würde hier jedoch die notwendigen Mausklicks und -bewegungen unnötig erhöhen.

Nach dieser Zwischenüberschrift folgt für jeden Datensatz der entsprechenden Abfrage ein Menüeintrag. Dazu greift die Schleife ebenfalls auf die Funktion NeuerEintrag von Listing 5 zurück.

Menüeinträge markieren

Wie Sie in Listing 6 schon gesehen haben, wird das passende Layout bereits mit Häkchen markiert. Wenn aber bereits manuell Änderungen an den Spalten vorgenommen wurden, passt keines der Layouts.

Für diesen Fall steht die modul-öffentliche Variable booGefunden aus Listing 3 immer noch auf False. Nur dann wird am Ende von ErzeugeMenue noch ein neuer, deaktivierter Eintrag <Benutzerdefiniert> angehängt, der mit Häkchen markiert ist.

Bild 4: Dynamisches Layout mit zusätzlichem Eintrag, siehe auch Bild 1.

Obwohl <Benutzerdefiniert> ein aktiver Menüeintrag ist, löst es mangels OnAction-Zuweisung beim Klick darauf kein Makro aus. Schließlich soll es vor allem anzeigen, dass keines der übrigen Layouts aktiv ist.

Speichern und Löschen

Die einzigen Menüeinträge, die sich eigentlich nie ändern, sind Löschen und Speichern unter. Es ist allerdings am einfachsten, alle Inhalte des Menüs per Schleife zu entfernen und diese zwei trotzdem immer sofort neu zu erzeugen. In Listing 7 finden Sie den Code für die notwendigen Prozeduren beim Erzeugen des Menüs.

Sub Speichern()
'siehe Artikel "Dynamische Layouts"
End Sub
Sub Loeschen()
DoCmd.OpenForm "frmLayoutsLoeschen", , , , , _
acDialog, Application.CurrentObjectName
End Sub

Das Löschen ruft tatsächlich nur ein Formular wie in Bild 5 auf, mit dem eines der gespeicherten Layouts zum Löschen markiert werden kann. Da allgemeine Layouts in diesem Beispiel nicht gelöscht werden dürfen, werden hier nur die persönlichen entsprechend der Abfrage aus Bild 3 angezeigt. Der Code hinter dem Formular ist in Listing 8 zu sehen.

Bild 5: Dialog zum Löschen.

Option Compare Database
Option Explicit
Private Sub btnAbbrechen_Click()
DoCmd.Close
End Sub
Private Sub btnLoeschen_Click()
Dim strSQL As String
strSQL = "DELETE * FROM tblSpalten WHERE SLIDRef = " & _
Me.lstLayouts.VALUE
CurrentDb.Execute strSQL, dbFailOnError
strSQL = "DELETE * FROM tblLayouts WHERE LID = " & _
Me.lstLayouts.VALUE
CurrentDb.Execute strSQL, dbFailOnError
DoCmd.Close
End Sub
Private Sub Form_Load()
If Not IsNull(Me.OpenArgs) Then
Me.lstLayouts.RowSource = _
"SELECT * FROM qryLayoutsPersoenlich " & _
"WHERE LObjektname ='" & Me.OpenArgs & "'"
Me.lblLayouts.Caption = "Persönliche Layouts für '" & _
Me.OpenArgs & "':"
End If
End Sub

Im Grunde ist es mit recht wenig Aufwand möglich, Menüs dynamisch der jeweiligen Aufrufsituation anzupassen. Die hier beispielhaft benutzte Anzeige von Datensätzen als Menüeinträge sollte natürlich auf geringe Datenmengen beschränkt sein, damit die Menüs nicht „überlaufen“.