Windows-Kontextmenü mit TreeView verwalten

Möchtest Du den gesamten Artikel lesen? Und vielleicht sogar den Artikel im PDF-Format und die Beispieldateien herunterladen? Dann hole Dir den Artikel gleich hier - völlig kostenlos!

Im Artikel “UstIdNr, IBAN und Co. per Kontextmenü” (www.vbentwickler.de/480) haben wir gezeigt, wie wir die Windows-Kontextmenüs erweitern können. In diesem Fall haben wir Daten, die man gegebenenfalls nicht alle im Kopf hat, aber regelmäßig benötigt, einfach über ein Kontextmenü in die Zwischenablage einfügen kann, um diese dann an der gewünschten Stelle beispielsweise in einem Bestellformular einträgt. Um das Hinzufügen und Aktualisieren dieser Einträge weiter zu vereinfachen und die zahlreichen Handgriffe zu ersparen, zeigen wir im vorliegenden Artikel eine Access-Lösung, mit der wir die Kontextmenü-Befehle mit einem einfachen TreeView verwalten und erweitern können.

Dazu wollen wir die Struktur der benutzerdefinierten Kontextmenü-Einträge im Windows-Menü in einem TreeView-Steuerelement abbilden.

Darin können wir Ordner, Unterordner und die enthaltenen Befehle verwalten. Nachdem wir diese Elemente angelegt haben, wollen wir diese per Mausklick direkt in die Registry schreiben können.

Datenmodell für die Anwendung

Wir wollen die Daten, wir wie sie zum Windows-Kontextmenü hinzufügen wollen, strukturiert in Tabellen ablegen, damit wir sie dynamisch ergänzen und bearbeiten können. Dazu benötigen wir zwei Tabellen:

  • tblMenuFolders: Enthält die Ordner und die Unterordner.
  • tblMenuEntries: Enthält die Einträge zu den jeweiligen Ordnern und Unterordnern.

Die Tabelle tblMenuFolders gestalten wir wie in Bild 1. Das Feld ParentMenuFolderID verknüpfen wir mit dem Feld MenuFolderID der gleichen Tabelle, damit wir die Zuordnung von Unterordnern zu Ordnern definieren können.

Tabelle zum Speichern der Menü-Ordner

Bild 1: Tabelle zum Speichern der Menü-Ordner

Die zweite Tabelle tblMenuEntries nimmt die eigentlichen Einträge auf (siehe Bild 2). Sie nimmt die ID, die Beschriftung des Menü-Eintrags und den Text auf, der beim Betätigen des Menü-Eintrags in die Zwischenablage kopiert werden soll.

Tabelle zum Speichern der Menü-Einträge

Bild 2: Tabelle zum Speichern der Menü-Einträge

Außerdem wird über das Feld ParentMenuFolderID die Zuordnung zu einem Ordner angegeben.

Im Datenmodell sehen wir die anzulegenden Beziehungen (siehe Bild 3). Die Tabelle tblMenuEntries ist über das Feld ParentMenuFolderID mit der Tabelle tblMenuFolders verknüpft. Die Tabelle tblMenuFolders ist außerdem über das Feld ParentMenuFolderID mit dem Feld MenuFolderID der gleichen Tabelle verknüpft. Für beide Beziehungen haben wir referenzielle Integrität mit Löschweitergabe definiert, damit beim Löschen eines übergeordneten Menüeintrags auch alle untergeordneten Elemente gelöscht werden.

Datenmodell der Anwendung

Bild 3: Datenmodell der Anwendung

Aufbau des TreeView-Steuerelements

Wir fügen einem neuen Formular namens frmWindowsKontextmenue ein TreeView-Steuerelement und ein ImageList-Steuerelement hinzu. Diese nennen wir ctlTreeView und ctlImageList (siehe Bild 4).

Formular mit TreeView- und ImageList-Steuerelement

Bild 4: Formular mit TreeView- und ImageList-Steuerelement

Damit können wir nun den Code zusammenstellen, der zum Anzeigen der Einträge aus den Tabellen tblMenuFolders und tblMenuEntries verwendet wird.

Die Prozedur soll beim Laden des Formulars ausgelöst werden und sieht wie in Listing 1 aus. Hier deklarieren wie zunächst eine modulweit erreichbare Variable für das TreeView-Steuerelement mit dem Schlüsselwort WithEvents:

Private objTreeView As MSComctlLib.TreeView
Private Sub Form_Load()
     Dim db As dao.Database
     Dim rstImages As dao.Recordset
     Dim rstFolders As dao.Recordset
     Dim rstEntries As dao.Recordset
     Dim strKey As String
     Dim objNode As MSComctlLib.Node
     Dim objImageList As MSComctlLib.ImageList
     
     Set db = CurrentDb
     Set objImageList = Me.ctlImageList.Object
     objImageList.ImageHeight = 16
     objImageList.ImageWidth = 16
     Set rstImages = dbc.OpenRecordset("SELECT * FROM MSysResources WHERE type = ''img''", dbOpenDynaset)
     Do While Not rstImages.EOF
         amvAddIconToImageListFromResources objImageList, rstImages!ID, rstImages!Name
         rstImages.MoveNext
     Loop
     
     
     Set objTreeView = Me.ctlTreeview.Object
     objTreeView.Nodes.Clear
     Set objTreeView.ImageList = objImageList
     
     Set rstFolders = db.OpenRecordset("SELECT * FROM tblMenuFolders WHERE ParentMenuFolderID IS NULL", dbOpenDynaset)
     Do While Not rstFolders.EOF
         strKey = "f" & rstFolders!MenuFolderID
         Set objNode = objTreeView.Nodes.Add(, , strKey, rstFolders!MenuFolderCaption, "folder")
         objNode.Expanded = True
         Call AddSubfolders(db, rstFolders!MenuFolderID, strKey)
         Call AddEntries(db, rstFolders!MenuFolderID, strKey)
         rstFolders.MoveNext
     Loop
     
     Set rstEntries = db.OpenRecordset("SELECT * FROM tblMenuEntries WHERE ParentMenuFolderID IS NULL", dbOpenDynaset)
     Do While Not rstEntries.EOF
         strKey = "e" & rstEntries!MenuEntryID
         objTreeView.Nodes.Add , , strKey, rstEntries!MenuEntryCaption, "breakpoint"
         rstEntries.MoveNext
     Loop
End Sub

Listing 1: Prozedur zum Initialisieren des TreeView- und des ImageList-Steuerelements

Private objTreeView As MSComctlLib.TreeView

Die Prozedur Form_Load füllt zunächst das ImageList-Steuerelement mit allen Bildern, die wir in der Tabelle MSysResources finden. Die benötigten Icons haben wir zuvor hinzugefügt, in dem wir ein Formular in der Entwurfsansicht geöffnet und die gewünschten Icons mit dem Befehl Bild einfügen über das Ribbon hinzugefügt haben. Diese Bilder werden automatisch in der Tabelle MSysResources gespeichert (siehe Bild 5).

Bilder für die TreeView-Einträge aus der Tabelle MSysResources

Bild 5: Bilder für die TreeView-Einträge aus der Tabelle MSysResources

Diese Bilder müssen wir nun zum ImageList-Steuerelement hinzufügen, damit wir sie für die TreeView-Einträge verwenden können. Das erledigen die ersten Anweisungen der Prozedur Form_Load. Hier referenzieren wir die Eigenschaft Object des ImageList-Steuerelements mit der Variablen objImageList. Dann stellen wir Höhe und Breite der Bilder jeweils auf 16 ein.

Schließlich öffnen wir ein Recordset auf Basis der Tabelle MSysResources für alle Einträge, deren Typ img lautet. Diese durchlaufen wir ein einer Do While-Schleife und rufen jeweils die Prozedur amvAddIconToImageList auf. Diese fügt die einzelnen Bilder aus der Tabelle in das ImageList-Steuerelement ein. Die Prozedur amvAddIconToImageList und ihre Hilfsfunktionen wollen wir aus Platzgründen an dieser Stelle nicht erläutern.

Indem wir die Bilder dynamisch einlesen, können wir diese auch einmal ersetzen, ohne dass wir umständlich am Entwurf des Steuerelements operieren müssen.

Nun referenzieren wir das TreeView-Steuerelement mit der Variablen objTreeView, die wir bereits weiter oben deklariert haben.

Wir leeren eventuell noch vorhandene Nodes mit der Clear-Methode und weisen diesem Steuerelement das ImageList-Steuerelement als Quelle für die Icons zu.

Dann erstellen wir ein neues Recordset auf Basis der Einträge der Tabelle tblMenuFolders, aber nur für die Einträge, die keinen übergeordneten Eintrag haben. Diese durchlaufen wir in einer weiteren Do While-Schleife.

In dieser Schleife stellen wir einen eindeutigen Schlüssel für jedes Node-Element im TreeView-Steuerelement zusammen, das für Ordner aus dem Buchstaben f und der ID des jeweiligen Datensatzes besteht (zum Beispiel f1).

Danach fügen wir das neue Element mit der Add-Methode der Nodes-Auflistung des TreeView-Steuerelements hinzu.

Dabei geben wir den Key an, den Namen aus dem Feld MenuFolderCaption und das zu verwendende Bild (folder). Außerdem stellen wir die Eigenschaft Expanded für das neue Element auf True ein, damit untergeordnete Einträge direkt angezeigt werden.

Schließlich rufen wir zwei weitere Prozeduren auf. AddSubfolders fügt die untergeordneten Ordner hinzu und AddEntries die eigentlichen Einträge.

Beiden übergeben wir einen Verweis auf das Database-Objekt, die ID des übergeordneten Ordners und den Key, den wir für das übergeordnete Element verwendet haben.

Da wir auch Elemente in der obersten Ebene anzeigen wollen, fügen wir noch Code hinzu, der die Einträge der Tabelle tblMenuEntries durchläuft, die im Feld ParentMenuFolderID den Wert NULL enthalten. Für diese legen wir als Key den Buchstaben e (für Entry) sowie den anzuzeigenden Text fest.

Unterordner zum TreeView hinzufügen

Die Prozedur AddSubfolders (siehe Listing 2) soll die Unterordner hinzufügen, die dem mit dem Parameter lngParentFolderID entsprechenden Datensatz der Tabelle tblMenuFolders untergeordnete sind.

Private Sub AddSubfolders(db As dao.Database, lngParentFolderID As Long, strKey As String)
     Dim rstSubfolders As dao.Recordset
     Dim strSubkey As String
     Dim objNode As MSComctlLib.Node
     
     Set rstSubfolders = db.OpenRecordset("SELECT * FROM tblMenuFolders WHERE ParentMenuFolderID = " _
         & lngParentFolderID, dbOpenDynaset)
     Do While Not rstSubfolders.EOF
         strSubkey = "f" & rstSubfolders!MenuFolderID
         Set objNode = objTreeView.Nodes.Add(strKey, tvwChild, strSubkey, rstSubfolders!MenuFolderCaption, "folder")
         objNode.Expanded = True
         Call AddSubfolders(db, rstSubfolders!MenuFolderID, strSubkey)
         Call AddEntries(db, rstSubfolders!MenuFolderID, strSubkey)
         rstSubfolders.MoveNext
     Loop
End Sub

Listing 2: Prozedur zum Einfügen der untergeordneten Ordner

Dazu öffnen wir ein Recordset, das genau diese Datensätze aus der Tabelle tblMenuFolders liefert. In der folgenden Do While-Schleife durchlaufen wir diese Datensätze und stellen zunächst in der Variablen strSubkey den Key für die zu erstellenden Untereinträge zusammen. Diese bestehen wiederum aus dem Buchstaben f und dem Primärschlüsselwert des neuen Elements, also beispielsweise f2.

Dann erstellen wir ein neues Node-Objekt unterhalb des übergeordneten Objekts. Das übergeordnete Element referenzieren wir mit dem Key dieses Elements und geben mit dem zweiten Parameter tvwChild an, dass das neue Element unterhalb dieses Elements angeordnet werden soll. Der dritte Parameter nimmt den neuen Key auf und der vierte den anzuzeigenden Text. Mit dem fünften Parameter übergeben wir wieder das Icon folder. Auch für dieses Element stellen wir wieder die Eigenschaft Expanded auf True ein.

Danach rufen wir erneut die Prozedur AddSubfolders auf, falls noch weitere Unterordner vorhanden sind.

Außerdem verwenden wir die Prozedur AddEntries, um die enthaltenen Einträge hinzuzufügen.

Elemente zum TreeView hinzufügen

Die Prozedur AddEntries (siehe Listing 3) fügt die eigentlichen Einträge hinzu. Sie erwartet die gleichen Einträge wie AddSubfolders. Hier definieren wir ein Recordset, dass alle Datensätze der Tabelle tblMenuEntries enthält, die dem mit lngParentFolderID übergebenen Ordner zugeordnet sind.

Private Sub AddEntries(db As dao.Database, lngParentFolderID As Long, strKey As String)
     Dim rstEntries As dao.Recordset
     Dim strSubkey As String
     Dim objNode As MSComctlLib.Node
     
     Set rstEntries = db.OpenRecordset("SELECT * FROM tblMenuEntries WHERE ParentMenuFolderID = " _
         & lngParentFolderID, dbOpenDynaset)
     Do While Not rstEntries.EOF
         strSubkey = "e" & rstEntries!MenuEntryID
         Set objNode = objTreeView.Nodes.Add(strKey, tvwChild, strSubkey, rstEntries!MenuEntryCaption, "breakpoint")
         rstEntries.MoveNext
     Loop
End Sub

Listing 3: Prozedur zum Einfügen der untergeordneten Einträge

Für jeden Datensatz dieses Recordsets fügen wir wieder ein Element zum TreeView-Steuerelement hinzu, wobei wir ähnlich wie in der zuvor beschriebenen Prozedur vorgehen.

Testen des TreeView-Steuerelements

Nachdem wir manuell einige Einträge zu den Tabellen hinzufügt haben, werden diese beim Öffnen des Formulars frmWindowsKontextmenue wie in Bild 6 dargestellt.

TreeView-Steuerelement mit einigen Einträgen

Bild 6: TreeView-Steuerelement mit einigen Einträgen

Kontextmenüs zum Hinzufügen und Löschen von Einträgen anlegen

Wir wollen die Tabelleneinträge nicht immer direkt in die Tabellen eingeben, sondern diese über ein Kontextmenü hinzufügen. Der Hauptknoten Daten sollte beispielsweise die Einträge Neuer Ordner und Neues Element anzeigen. Für die Unterordner sollen die Einträge Neuer Ordner, Neues Element und Ordner löschen erscheinen. Schließlich sollen die Elemente im Kontextmenü den Befehl Element löschen anbieten.

Die Kontextmenüs zeigen wir an, wenn das Ereignis MouseDown des TreeView-Steuerelements ausgelöst wird (siehe Listing 4). Hier prüfen wir, ob die rechte Maustaste zum Anklicken verwendet wurde.

Private Sub ctlTreeview_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal x As Long, ByVal y As Long)
     Dim objNode As MSComctlLib.Node, lngID As Long, strChar As String, cbr As CommandBar
     Dim cbbNewFolder As CommandBarButton, cbbNewEntry As CommandBarButton, cbbDeleteFolder As CommandBarButton
     Dim cbbDeleteEntry As CommandBarButton
     
     If Button = 2 Then
         Set objNode = objTreeView.HitTest(x, y)
         If Not objNode Is Nothing Then
             strChar = Left(objNode.Key, 1)
             lngID = Mid(objNode.Key, 2)
             Select Case strChar
                 Case "f"
                     On Error Resume Next
                     CommandBars("Folders").Delete
                     On Error GoTo 0                    
                     Set cbr = CommandBars.Add("Folders", msoBarPopup, , True)
                     Set cbbNewFolder = cbr.Controls.Add(msoControlButton, , , , True)
                     cbbNewFolder.Caption = "Ordner hinzufügen"
                     cbbNewFolder.OnAction = "=AddFolder(" & lngID & ")"
                     Set cbbDeleteFolder = cbr.Controls.Add(msoControlButton, , , , True)
                     cbbDeleteFolder.Caption = "Ordner löschen"
                     cbbDeleteFolder.OnAction = "=DeleteFolder(" & lngID & ")"
                     Set cbbNewEntry = cbr.Controls.Add(msoControlButton, , , , True)
                     cbbNewEntry.Caption = "Element hinzufügen"
                     cbbNewEntry.OnAction = "=AddEntry(" & lngID & ")"
                     cbr.ShowPopup
                 Case "e"
                     On Error Resume Next
                     CommandBars("Entries").Delete
                     On Error GoTo 0
                     Set cbr = CommandBars.Add("Entries", msoBarPopup, , True)
                     Set cbbDeleteEntry = cbr.Controls.Add(msoControlButton, , , , True)
                     cbbDeleteEntry.Caption = "Element löschen"
                     cbbDeleteEntry.OnAction = "=DeleteElement(" & lngID & ")"
                     cbr.ShowPopup
             End Select
         Else
             On Error Resume Next
             CommandBars("Folders").Delete
             On Error GoTo 0
             Set cbr = CommandBars.Add("Folders", msoBarPopup, , True)
             Set cbbNewFolder = cbr.Controls.Add(msoControlButton, , , , True)
             cbbNewFolder.Caption = "Ordner hinzufügen"
             cbbNewFolder.OnAction = "=AddFolder(0)"
             Set cbbNewEntry = cbr.Controls.Add(msoControlButton, , , , True)
             cbbNewEntry.Caption = "Element hinzufügen"
             cbbNewEntry.OnAction = "=AddEntry(0)"
             cbr.ShowPopup
         End If
     End If
End Sub

Listing 4: Prozedur zum Anzeigen der Kontextmenüs

Danach holen wir mit den Koordinaten x und y aus den Parametern über die Funktion HitTest das Objekt, das angeklickt wurde. Ist objNode danach nicht leer, können wir das Kontextmenü für das entsprechende Element anzeigen.

Anderenfalls hat der Benutzer in den leeren Raum außerhalb der Elemente geklickt, was ein Kontextmenü mit den Befehlen Ordner hinzufügen und Element hinzufügen anzeigen soll.

Schauen wir uns erst einmal den Fall an, dass der Benutzer auf einen bestehenden Eintrag geklickt hat. In diesem Fall holen wir uns mit Zeichenkettenfunktionen aus der Eigenschaft Key des Elements den Buchstaben und die ID und schreiben diese in die Variablen strChar und lngID.

Im Falle des Buchstaben f hat der Benutzer einen Ordner angeklickt. Dann löschen wir ein eventuell bereits vorhandenes Kontextmenü namens Folders und legen dieses im nächsten Schritt erneut an.

Dann fügen wir eine Schaltfläche mit dem Titel Ordner hinzufügen hinzu und legen für seine Eigenschaft OnAction den Ausdruck zum Aufruf der Funktion OrdnerHinzufuegen fest, wobei wir gleich die ID für den übergeordneten Ordner hinzufügen.

Der Ausdruck sieht zum Beispiel wie folgt aus:

=OrdnerHinzufuegen(1)

Auf die gleiche Weise fügen wir den Button mit der Beschriftung Ordner löschen und einem Befehl wie dem folgenden hinzu:

=DeleteFolder(1)

Schließlich folgt noch eine Schaltfläche mit dem Text Element hinzufügen, welche beispielsweise den folgenden Aufruf enthalten soll:

=AddEntry(1)

Schließlich zeigen wir das Kontextmenü mit der Methode ShowPopup an (siehe Bild 7).

Kontextmenü eines Ordners

Bild 7: Kontextmenü eines Ordners

Danach folgt das Kontextmenü, falls der Benutzer eines der Elemente angeklickt hat. Der Key dieser Elemente beginnt mit e, also prüfen wir in der Select Case-Bedingung auf diesen Buchstaben.

Hier löschen wir das gegebenenfalls bereits vorhandene Kontextmenü namens Entries und fügen es erneut hinzu. Dieses erhält nur einen Button mit der Beschriftung Element löschen und der folgenden Aktion:

=DeleteElement(1)

Danach zeigen wir das Kontextmenü mit ShowPopup an.

Schließlich folgt noch der Fall, dass der Benutzer außerhalb eines der Elemente geklickt hat.

Das Kontextmenü für diesen Fall soll die Befehle Ordner hinzufügen und Element hinzufügen anzeigen, welche Aufrufe wie die folgenden enthalten:

=AddFolder(0)
=AddEntry(0)

Funktionen für die Kontextmenübefehle

Damit hätten wir auch die Kontextmenüs hinzugefügt und können nun die aufzurufenden Funktionen erstellen. Die nachfolgend vorgestellten Funktionen tragen wir in einem neuen Standardmodul namens mdlCommandbars ein und deklarieren mit dem Schlüsselwort Public, da diese sonst nicht aufrufbar wären.

Die Funktion zum Hinzufügen eines Ordners unterhalb eines bestehenden Ordners finden wir in Listing 5. Hier nehmen wir die ID des übergeordneten Ordners entgegen und fragen dann per InputBox den Namen des neuen Ordners ab. Liefert die InputBox keine leere Zeichenkette und ist lngID nicht 0, trägt die Prozedur den neuen Datensatz in die Tabelle tblMenuFolders ein.

Public Function AddFolder(lngID As Long)
     Dim db As DAO.Database
     Dim strFolder As String
     Dim objNode As MSComctlLib.Node
     Dim strKey As String
     Dim strSubkey As String
     Dim lngSubID As Long
     Set db = CurrentDb
     strFolder = InputBox("Name des Ordners:", "Neuer Unterordner")
     If Not Len(strFolder) = 0 Then
         If Not lngID = 0 Then
             db.Execute "INSERT INTO tblMenuFolders(MenuFolderCaption, ParentMenuFolderID) VALUES(''" & strFolder & "'', _
                 " & lngID & ")", dbFailOnError
             lngSubID = db.OpenRecordset("SELECT @@IDENTITY").Fields(0)
             strSubkey = "f" & lngSubID
             strKey = "f" & lngID
             Set objNode = Forms!frmWindowsKontextmenues!ctlTreeview.Object.Nodes.Add(strKey, tvwChild, _
                 strSubkey, strFolder, "folder")
         Else
             db.Execute "INSERT INTO tblMenuFolders(MenuFolderCaption) VALUES(''" & strFolder & "'')", dbFailOnError
             lngSubID = db.OpenRecordset("SELECT @@IDENTITY").Fields(0)
             strSubkey = "f" & lngSubID
             Set objNode = Forms!frmWindowsKontextmenues!ctlTreeview.Object.Nodes.Add(, , strSubkey, strFolder, "folder")
         End If
         objNode.Expanded = True
         objNode.Parent.Expanded = True
     End If
End Function

Listing 5: Funktion zum Einfügen eines untergeordneten Ordners per Kontextmenü

Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

den kompletten Artikel im PDF-Format mit Beispieldatenbank

diesen und alle anderen Artikel mit dem Jahresabo

Schreibe einen Kommentar