TreeView aufgebohrt

15.06.2006 von André Minhorst
Das TreeView-Steuerelement offenbart sich als eines der flexibelsten Steuerelemente, wenn es um die Anzeige hierarchischer Daten geht. Es kann aber noch mehr: Mit ein wenig Know-how bringen Sie einem Steuerelement sogar bei, wie es den Benutzern über Drag&Drop hinaus beim Verwalten der Daten helfen kann.

Die Grundlagen zum TreeView-Steuerelement sowie das Füllen mit Daten und die Bereitstellung von Drag&Drop-Funktionen haben wir bereits vorgestellt, etwa in den Ausgaben 12/2000, 1/2001, 4/2001 oder 3/2003.

Außer Drag&Drop bietet das TreeView-Steuerelement keine auf den ersten Blick erkennbaren Funktionen zum Verwalten von Daten. Wie bringt man einem Kontextmenü ein wenig mehr Interaktivität bei – etwa um von dort aus Datensätze zu löschen, hinzuzufügen oder zum Bearbeiten zu öffnen? Die Antwort ist im wahrsten Sinne des Wortes nicht offensichtlich: Rüsten Sie das TreeView-Steuerelement beziehungsweise dessen Einträge einfach mit Kontextmenüs aus, die weitere Funktionen verfügbar machen.

TreeView-Steuerelemente und Kontextmenüs

Im vorliegenden Beispiel soll das TreeView-Steuerelement die Artikelkategorien der Nordwind-Datenbank und die enthaltenen Artikel anzeigen. Die Einträge in Form von Kategorien und Artikeln sollen im Kontextmenü je nach Objekttyp verschiedene Einträge anzeigen. Das ist nicht gerade trivial, immerhin erfordert es zunächst eine Prüfung des aktuell markierten Objekts und in der Folge die Anzeige der benötigten Funktionen.

Es gibt drei unterschiedliche Kontextmenüs: Klickt der Benutzer auf einen Punkt im leeren Bereich des TreeView-Steuerelements, soll etwa ein Kontextmenü mit dem folgenden Eintrag erscheinen:

Es ist leicht zu erkennen, dass es nicht ausreicht, beim Rechtsklick im TreeView die Art des angeklickten Elements zu ermitteln: Fast alle Optionen erfordern die Kenntnis der individuellen ID des betreffenden Eintrags.

Beispielanwendung

Die Beispielanwendung enthält die Kategorien und Tabellen der Nordwind-Datenbank. Ein TreeView-Steuerelement in einem neuen Formular sorgt für die Anzeige der in den Tabellen enthaltenen Daten.

Das TreeView-Steuerelement fügen Sie dem Formular in der Entwurfsansicht über den Menüeintrag Einfügen/ActiveX-Steuerelement hinzu und geben ihm den Namen ctlTreeView. ActiveXSteuerelementein Formularen füllt man am besten erst durch eine Prozedur, die durch das Ereignis Beim Öffnen ausgelöst wird. Andernfalls ist das Steuerelement gegebenenfalls noch nicht initialisiert.

Bild 1: IntelliSense mit ActiveXSteuerelementen funktioniert nur mit passender Deklaration.

ActiveX-Steuerelemente bieten keine automatische IntelliSense-Unterstützung wie die üblichen Access-Steuerelemente. Für IntelliSense- Unterstützung müssen Sie im Code ein passendes Objekt mit einem Verweis auf das Steuerelementdeklarieren und instanzieren. Erst dann liefert IntelliSense die passenden Elemente für das gewünschte Steuerelement anstelle von Standardeinträgen (Bild 1).

TreeView mit Kategorien und Artikeln füllen

Das Füllen des TreeView-Steuerelements übernimmt die Routine TreeViewFuellen aus Listing 1, die von der Ereignisprozedur Form_Open aufgerufen wird. Sie enthält zwei verschachtelte Do- While-Schleifen: Die äußere durchläuft alle Datensätze der Tabelle Kategorien und legt für jede Kategorie einen Knoten im TreeView-Steuerelement an. Die innere Do-While-Schleife öffnet für jede Kategorie eine Datensatzgruppe mit den zur Kategorie gehörenden Artikeln und fügt diese als Child-Elemente zum jeweiligen Kategorie Knoten hinzu.

Option Compare Database
Option Explicit
Private objTreeView As MSComctlLib.TreeView
Private Sub Form_Open(Cancel As Integer)
TreeViewFuellen
End Sub
Private Sub TreeViewFuellen()
Dim db As DAO.Database
Dim rstKategorien As DAO.Recordset
Dim rstArtikel As DAO.Recordset
Dim objNode As MSComctlLib.Node
Set objTreeView = Me.ctlTreeView.Object
Set db = CurrentDb
Set rstKategorien = db.OpenRecordset("Kategorien", dbOpenDynaset)
With objTreeView
Do While Not rstKategorien.EOF
Set objNode = objTreeView.Nodes.Add()
With objNode
.Key = "kat" & rstKategorien![Kategorie-Nr]
.Text = rstKategorien!Kategoriename
End With
Set rstArtikel = db.OpenRecordset("SELECT [Artikel-Nr], Artikelname
FROM Artikel WHERE [Kategorie-Nr] = " & rstKategorien![Kategorie-Nr], dbOpenDynaset)
Do While Not rstArtikel.EOF
objTreeView.Nodes.Add objNode, tvwChild, "art" & rstArtikel![Artikel-
Nr], rstArtikel!Artikelname
rstArtikel.MoveNext
Loop
rstKategorien.MoveNext
Loop
End With
End Sub

Erwähnenswert ist in diesem Zusammenhang, dass die Key-Eigenschaft jeweils aus der einer Zeichenkette („kat“ für Kategorie-Knoten und „art“ für Artikel-Knoten) und dem Primärschlüsselwert des jeweiligen Datensatzes besteht. Damit sorgen Sie nicht nur für die Vergabe eindeutiger Schlüssel, sondern erleichtern auch die Anzeige des Kontextmenüs für den jeweiligen Eintrag: Über die ersten drei Buchstaben („kat“ oder „art“) ermittelt die später vorgestellte Routine den Elementtyp und zeigt das passende Kontextmenü an.

Bild 2: Dieses TreeView- Steuerelement dient als Grundlage für das Hinzufügen von Kontextmenüs.

Die Optik des Ergebnisses genügt nicht unbedingt höchsten Ansprüchen, reicht aber als Grundlage für das Hinzufügen von Kontextmenüs aus (Bild 2).

Kontextmenüs hinzufügen

Prinzipiell müssen Sie nun nur noch das Kontextmenü erstellen und zum Steuerelement hinzufügen.Moment: Das Bauen der Kontextmenüs ist ja leicht, aber das TreeView-Steuerelement enthält ja gar keine Eigenschaft, mit der man ihm ein Kontextmenü zuweisen könnte! Und müsste nicht jeder Eintrag ein eigenes Kontextmenü haben? Nun, in der Tat würde die für die Access- Steuerelemente verfügbare Eigenschaft Kontextmenüleiste hier nicht weiterhelfen – ganz davon abgesehen, dass das TreeView-Steuerelement diese gar nicht zur Verfügung stellt. Dafür bietet das TreeView-Steuerelement aber Ereignisprozeduren, die etwa ein Mausklick auf das Steuerelement auslöst. Da müssen Sie ansetzen! Aber zunächst erstellen Sie die benötigten Kontextmenüs. Im Schnelldurchgang sieht das so aus:

  1. Rechtsklick auf eine Symbolleiste, Auswahl des Kontextmenüeintrags Anpassen.

  2. Dialog Anpassen, Registerseite Symbolleisten, Schaltfläche Neu, Name der Symbolleiste: TreeView_KeinEintrag.

  3. Dialog Anpassen: Klick auf die Schaltfläche Eigenschaften, Eigenschaft Typ für die neue Symbolleiste auf Popup ändern, Eigenschaftsfenster schließen

  4. Neues Popup-Menü anzeigen: Dialog Anpassen, Registerseite Symbolleisten, Eintrag Kontextmenü aktivieren – es erscheint eine Leiste mit allen Kontextmenüs.

  5. Dialog Anpassen, Registerseite Befehle, Kategorie Datei, Befehl Benutzerdefiniert als neuen Eintrag in das Kontextmenü ziehen (Bild 3).

  6. Rechtsklick auf den neuen Eintrag, Kontextmenü- Eintrag Eigenschaften auswählen, Beschriftung auf &Neue Kategorie anpassen, Bei Aktion auf =mnuNeueKategorie() einstellen.


Bild 3: Erzeugen eines Kontextmenüs.

Damit haben Sie das Kontextmenü für einen Klick in einen leeren Bereich des TreeView-Steuerelements erzeugt und die beim Betätigen dieses Eintrags auszuführende Routine angegeben. Um diese kümmern Sie sich später, genau wie um den Aufruf des Kontextmenüs.

Zunächst legen Sie die beiden übrigen Kontextmenüs nach dem obigen Schema an. BeimKlick auf einen Kategorie-Knoten soll Access das Kontextmenü TreeView_Kategorien anzeigen. Es enthält Einträge mit den folgenden Beschriftungen:

Sie wundern sich, warum die Liste keine Funktionsaufrufe enthält? Nun, im Gegensatz zum ersten Kontextmenü steht hier noch nicht fest, für welches Kategorie-Element der Benutzer das Kontextmenü aufruft. Das ergibt sich erst beim Rechtsklick auf den jeweiligen Eintrag, und erst dann weist eine passende Routine dem Menüeintrag die auszuführende Aktion zu.

Das dritte Kontextmenü soll die Anwendung beim Rechtsklick auf ein Artikel-Element anzeigen. Es heißt TreeView_Artikel und enthält Einträge mit den folgenden Beschriftungen:

Auch hier folgt das Zuweisen der auszuführenden Routine erst beim Aufruf des Menüeintrags.

Kontextmenü im TreeView aufrufen

Die Menüstruktur steht, allein der Aufruf fehlt. Hier kommen nun die Ereignisse des TreeView- Steuerelements ins Spiel. Welche das sind, offenbart ein Blick in den Objektkatalog (Bild 4).

Bild 4: Der Objektkatalog bietet einen Überblick über die Ereigniseigenschaft en des TreeView- Steuerelements.

Anlegen lassen sich die passenden Ereignisprozeduren am einfachsten, indem Sie im Codefenster im linken Kombinationsfeld das TreeView- Steuerelement und im rechten Kombinationsfeld das gewünschte Ereignis auswählen.

Welche Ereigniseigenschaft ist in diesem Fall die richtige? Klar, dass es etwas mit einem Mausklick zu tun hat, aber dazu bietet das TreeView- Steuerelement eine ganze Reihe von Ereignissen. Was passiert denn genau beim Auswählen eines Kontextmenüeintrags? Der Benutzer drückt die rechte Maustaste, wählt einen Eintrag des Kontextmenüs aus und lässt die rechte Maustaste wieder los. Und das ist der Knackpunkt: Erst beim Loslassen steht fest, welchen Eintrag der Benutzer ausgewählt hat. Die Wahl des richtigen Ereignisses fällt nun leicht: MouseUp ist Ihr Kandidat.

Legen Sie also das Gerüst der passenden Ereigniseigenschaft an und ergänzen Sie es um die fehlenden Codezeilen. Den Code finden Sie in Listing 2, die Erläuterungen im Folgenden:

Private Sub ctlTreeView_MouseUp(ByVal Button As Integer, ByVal Shift As Integer,
ByVal x As Long, ByVal y As Long)
Dim cbr As Office.CommandBar
Dim objNode As MSComctlLib.Node
Dim strType As String
Dim lngID As Long
If Button = 2 Then
If ctlTreeView.HitTest(x, y) Is Nothing Then
'leerer Bereich getroffen
Set cbr = CommandBars("TreeView_KeinEintrag")
Else
'Node getroffen
Set objNode = ctlTreeView.HitTest(x, y)
strType = Left(objNode.Key, 3)
lngID = Mid(objNode.Key, 4)
If strType = "kat" Then
'Kategorie-Knoten
Set cbr = CommandBars("TreeView_Kategorien")
cbr.Controls("Kategorie bearbeiten").OnAction = "=mnuKategorieBearbeiten("
& lngID & ")"
cbr.Controls("Kategorie löschen").OnAction = "=mnuKategorieLoeschen("
& lngID & ")"
cbr.Controls("Neuer Artikel").OnAction = "=mnuNeuerArtikel(" &
lngID & ")"
ElseIf strType = "art" Then
'Artikel-Knoten
Set cbr = CommandBars("TreeView_Artikel")
cbr.Controls("Artikel bearbeiten").OnAction = "=mnuArtikelBearbeiten("
& lngID & ")"
cbr.Controls("Artikel löschen").OnAction = "=mnuArtikelLoeschen(" &
lngID & ")"
End If
End If
If Not cbr Is Nothing Then
cbr.ShowPopup
End If
End If

Option Compare Database
Option Explicit
Public Function mnuNeueKategorie()
Dim strKategorie As String
Dim lngKategorieID As Long
DoCmd.OpenForm "frmKategorien", DataMode:=acFormAdd, WindowMode:=acDialog
If IstFormularGeoeffnet("frmKategorien") Then
strKategorie = Forms!frmKategorien!Kategoriename
lngKategorieID = Nz(Forms!frmKategorien![Kategorie-Nr], 0)
If Not lngKategorieID = 0 Then
Forms!frmTreeviewMitKontextmenue.ctlTreeView.Nodes.Add , , "kat" &
lngKategorieID, strKategorie
DoCmd.Close acForm, "frmKategorien"
End If
End If
End Function
Public Function mnuKategorieBearbeiten(lngKategorieID As Long)
Dim strKategorie As String
DoCmd.OpenForm "frmKategorien", DataMode:=acFormEdit, WindowMode:=acDialog,
WhereCondition:="[Kategorie-Nr] = " & lngKategorieID
If IstFormularGeoeffnet("frmKategorien") Then
strKategorie = Forms!frmKategorien!Kategoriename
Forms!frmTreeviewMitKontextmenue.ctlTreeView.Nodes("kat" & lngKategorieID).
Text = strKategorie
DoCmd.Close acForm, "frmKategorien"
End If
End Function
Public Function mnuKategorieLoeschen(lngKategorieID As Long)
CurrentDb.Execute "DELETE FROM Kategorien WHERE [Kategorie-Nr] = " & lngKategorieID,
dbFailOnError
Forms!frmTreeviewMitKontextmenue.ctlTreeView.Nodes.Remove "kat" & lngKategorieID
End Function
Public Function mnuArtikelBearbeiten(lngArtikelID As Long)
Dim strArtikel As String
DoCmd.OpenForm "frmArtikel", DataMode:=acFormEdit, WindowMode:=acDialog, Where-
Condition:="[Artikel-Nr] = " & lngArtikelID
If IstFormularGeoeffnet("frmArtikel") Then
strArtikel = Forms!frmArtikel!Artikelname
Forms!frmTreeviewMitKontextmenue.ctlTreeView.Nodes("art" &
lngArtikelID).Text = strArtikel
DoCmd.Close acForm, "frmArtikel"
End If
End Function
Public Function mnuArtikelLoeschen(lngArtikelID As Long)
CurrentDb.Execute "DELETE FROM Artikel WHERE [Artikel-Nr] = " & lngArtikelID,
dbFailOnError
Forms!frmTreeviewMitKontextmenue.ctlTreeView.Nodes.Remove "art" & lngArtikelID
End Function
Public Function mnuNeuerArtikel(lngKategorieID As Long)
Dim lngArtikelID As Long
Dim strArtikel As String
Dim objNode As MSComctlLib.Node
DoCmd.OpenForm "frmArtikel", DataMode:=acFormAdd, WindowMode:=acDialog, OpenArgs:=
lngKategorieID
If IstFormularGeoeffnet("frmArtikel") Then
lngArtikelID = Nz(Forms!frmArtikel![Artikel-Nr], 0)
If lngArtikelID > 0 Then
strArtikel = Forms!frmArtikel!Artikelname
lngKategorieID = Forms!frmArtikel![Kategorie-Nr]
Set objNode = Forms!frmTreeviewMitKontextmenue.ctlTreeView.Nodes("kat"
& lngKategorieID)
Forms!frmTreeviewMitKontextmenue.ctlTreeView.Nodes.Add objNode, tvw-
Child, "art" & lngArtikelID, strArtikel
DoCmd.Close acForm, "frmArtikel"
End If
End If
End Function

MouseUp

Das Ereignis MouseUp wird nicht nur durch einen Rechtsklick ausgelöst, sondern auch durch andere Mausklicks inklusive Varianten mit bestimmten Tastenkombinationen. Der Parameter Button liefert einen Integer-Wert, der Auskunft über die gedrückte Taste liefert. Hier reicht die Information, dass ein Klick auf die rechte Maustaste den Wert 2 liefert. Nach der Prüfung, ob der Klick von der rechten Maustaste kommt, steht die Frage nach dem Ziel des Mausklicks an. Hat der Klick einen Kategorie-Knoten oder einen Artikel- Knoten getroffen, oder zielte er etwa auf einen leeren Bereich des TreeView-Steuerelements?

Auch diese Information erhalten Sie über die Parameter des MouseUp-Ereignisses – in diesem Fall allerdings indirekt. Es liefert nämlich die xund die y-Position zum Zeitpunkt des Mausklicks. Und die liefert mittels der Methode Hit- Test einen Hinweis auf das getroffene Node-Element.

Hier prüft die Routine zunächst, ob die Methode HitTest überhaupt einen Verweis auf ein Element des TreeView-Steuerelements zurückgibt. Falls nicht, hat der Klick im leeren Bereich des TreeViews stattgefunden. In dem Fall soll das Kontextmenü mit dem Befehl zum Anlegen einer Kategorie angezeigt werden. Entsprechend stellt die Routine die Objektvariable cbr auf die Symbolleiste TreeView_KeinEintrag ein.

Wenn die HitTest-Methode einen Verweis auf ein Element des TreeViews zurückliefert, wertet die Routine zunächst dessen Key-Eigenschaft aus: Die ersten drei Zeichen enthalten entweder die Zeichenkette „kat“ oder „art“ und werden in der Variablen strType gespeichert, die folgenden die ID des zugehörigen Eintrags in der entsprechenden Tabelle.

Zunächst ist die Zeichenkette interessant: Damit kann die Routine entscheiden, ob sie das Kontextmenü für eine Kategorie oder einen Artikel anzeigen soll. Je nach gewähltem Kontextmenü stellt die Routine nun die beim Klick auf einen der Kontextmenüeinträge aufzurufenden Funktionen ein. Dazu verwendet sie die Controls- Auflistung des jeweiligen Commandbar- Objekts und weist den enthaltenen Einträgen die passende Funktion zu. Dabei muss die Routine in den meisten Fällen die ID des aktuellen Elements übergeben: Die Funktion zum Löschen eines Artikels etwa muss schließlich wissen, welchen Artikel sie überhaupt löschen soll. Die Zuweisung sieht in dem Fall beispielsweise wie folgt aus (in einer Zeile):

cbr.Controls("Artikel löschen").OnAction = "=mnuArtikelLoeschen("& lngID & ")"

Bild 5: Einfügen eines neuen Artikels per Kontextmenü.

Die Kontextmenüs sind somit fertig. Bild 5 zeigt, wie das Kontextmenü eines Kategorie-Eintrags in Aktion aussieht.

Menüeinträge mit Funktionen versehen

Es fehlen nur noch die hinter den Einträgen des Kontextmenüs stehenden Funktionen. Aus Platzgründen können wir diese nicht in aller Ausführlichkeit vorstellen, und die im Beispiel aufgeführten Funktionen sind auf die wichtigsten Anweisungen beschränkt. Das heißt, dass etwa die Fehlerbehandlung fehlt oder die Routinen nur Kategorien und Artikel löschen, die nicht von Datensätzen aus anderen Tabellen referenziert werden.

Zusammengefasst wird jede der Funktionen – egal, ob diese Datensätze anlegt, ändert oder löscht – an zwei Stellen tätig: Sie ändert sowohl den zugrunde liegenden Datensatz als auch das dazu gehörende Element im TreeView-Steuerelement. Dazu gibt es zwei Möglichkeiten: Entweder man lädt den kompletten Baum neu, was erstens viel Zeit kosten kann und zweitens einen enormen Aufwand bedeutet, wenn man den vorherigen Zustand speichern und wiederherstellen möchte – das bezieht sich auf auf- oder zugeklappte Kategorien oder den aktuell markierten Eintrag. Oder man passt lediglich das betreffende Element an, indem man es je nach Anforderung hinzufügt, ändert oder löscht.

Am einfachsten lassen sich die Funktionen zum Löschen von Kategorien oder Artikeln realisieren. Sie benötigen lediglich die ID des zu löschenden Datensatzes und entfernen diesen dann sowohl aus der zugrunde liegenden Tabelle als auch aus dem TreeView-Steuerelement.

Sonstige Funktionen

Alle anderen Funktionen zeigen eines der beiden in der Beispieldatenbank enthaltenen Formulare frmArtikel oder frmKategorien an. Beim Anlegen eines neuen Datensatzes öffnet die jeweilige Funktion das passende Formular mit dem Parameter DataMode:=acFormAdd, wodurchdas Formular direkt einen neuen, leeren Datensatz anzeigt. Die OK-Schaltfläche der Formulare schließt diese nicht, sondern macht sie lediglich mit der Anweisung Me.Visible = False unsichtbar. Auf diese Weise kann die aufrufende Funktion die Eigenschaften des neuen Datensatzes auslesen, die sie im TreeView gegebenenfalls anpassen muss, und das Formular anschließend schließen.

Das Ändern eines vorhandenen Artikels oder einer vorhandenen Kategorie sieht ähnlich aus: Hier ruft die passende Funktion das Formular mit dem Parameter DataMode:=acFormEdit auf und übergibt mit dem Parameter WhereCondition Informationen über den anzuzeigenden Datensatz.

Zusammenfassung

Mit der hier vorgestellten Integration von Kontextmenüs in TreeView-Steuerelementen erweitert sich deren Benutzerfreundlichkeit deutlich. Die beschriebenen Vorgehensweisen lassen sich auch beim ListView-Steuerelement anwenden.