Kontakt-Formular   Inhaltsverzeichnis   Druckansicht  

VisualBasic.tips

Startseite > Microsoft Access VBA > ActiveX Treeview > Teil 2 - Erste Schritte

Teil 2 - Erste Schritte

Ausgabe 12/2000

Download Beispieldatei

Im ersten Teil der Artikelfolge haben wir Ihnen die Grundlagen zum ActiveX-Control comctl32.oxc (mscomctl.ocx) am Beispiel des TreeView-Steuerelementes vorgestellt. Dabei haben wir das TreeView „von Hand“ mit „statischen“ Informationen gefüllt. Was aber, wenn die Struktur des „Baumes“ vom Inhalt einer Tabelle abhängig ist? In diesem Teil soll deshalb damit begonnen werden eine kleine Datenbank zur Verwaltung  der Firmenhierarchie mit Hilfe des TreeView übersichtlich und komfortabel darzustellen.

Mögliche Einsatzgebiete

Durch die Verknüpfung von Tabellen im Beziehungsfenster erhalten wir nicht selten eine Hierarchietiefe, die sich mit Hilfe von Formularen oft nur unzureichend darstellen lässt. Will man zum Beispiel zu einem Kunden die Rechnungsdaten und die Rechnungspositionen auf einem Blick haben, wird das häufig mit 2 verknüpften Unterformularen realisiert. Möchte man allerdings die Unter¬nehmensstruktur in Access erfassen und abbilden, gibt es zwei grundsätzlich unterschiedliche Möglichkeiten der Umsetzung:

  • Jede Hierarchieebene wird in einer separaten Tabelle erfasst. Die Tabellen werden 1:n untereinander in Beziehung gesetzt (z.B. Standort – Bereich – Abteilung – Büro).

Was aber, wenn die Tiefe der Hierarchie von Standort zu Standort variiert. Oder Verwaltung und Produktion abweichende Hierarchiebezeichnungen haben?

  • Die Hierarchieebenen werden alle in einer gemeinsamen Tabelle verwaltet. Die Bezeichnung der einzelnen Hierarchieebene wird über zusätzliches Feld implementiert (in unserem Beispiel: Typ#).

Auch diese Lösung ist nicht immer problemlos anwendbar, hat sich aber in der Vergangenheit gut bewährt. Deshalb möchten wir im Folgenden auf diese Variante näher eingehen.

Planung und Konzeption

Das Beispiel ist bewusst allgemeingültig gehalten und erhöht so den Effekt der Wiederverwendbarkeit.
Um eine Hierarchie in einer einzelnen Tabelle zu erfassen, muss man den sogenannten Auto Join anwenden, das ist eine Abbildung der Tabelle auf sich selbst (Tabelle 1). In vielen Fällen ist es außerdem sinnvoll referentielle Integrität (inkl. Löschweitergabe) zu gewährleisten, siehe Bild 1. Wenn zum Beispiel ein Standort aufgelöst wird, wirkt sich dies auch auf den Rest des Hierarchiebaums aus. Allerdings sollte man auch in Erwägung ziehen die Löschweitergabe von Fall zu Fall dem Benutzter zu überlassen. Dann allerdings kommt man um eine Implementierung in VBA nicht herum. Mit Hilfe der Technik einer rekursiven Funktion, die wir auch zum Füllen des TreeView verwenden, sollte dies jedoch kein größeres Problem darstellen.

Den Aufbau für den Entwurf der Tabelle tbl_Struktur entnehmen Sie bitte der Tabelle 2. Die Felder mit dem #-Literal kennzeichnen Schlüsselfelder, also Primär- oder Frmdschlüsselfelder. Das Autowertfeld haben wir der Einfachheit halber Index# genannt, der Inhalt des Feldes Gruppe# bildet einen Auto Join auf das Feld Index#, somit wird der Datensatz einem anderen (schon existierenden) Datensatz untergeordnet (Child). Oder anders: der im Feld Gruppe# angegebene Index repräsentiert den übergeordneten (Parent) Datensatz. Das Feld Typ# stellt eine universelle Möglichkeit dar, die Einträge kategorisch zu trennen, z.B. durch eine namentliche Zuordnung des Eintrags zum einer Hierarchiebene. Das Feld Sortierung beschleunigt den Sortierungsvorgang beim Aufbau des TreeView, erlaubt aber auch eine individuelle Reihenfolge der Einträge.

Tabelle 1: Beispiel eines Auto Join

Tabelle 1: Beispiel eines Auto Join

Tabelle 2: Entwurf der Tabelle tbl_Struktur

Feldname

Felddatentyp

Index#

Autowert

Gruppe#

Long Integer

Eintrag

Text [100]

Typ#

Long Integer

Sortierung

Long Integer

Abbildung 1: Referentielle Integrität in einem Auto Join

Abbildung 1: Referentielle Integrität in einem Auto Join

Automatisches Füllen des TreeView

Wer schon mal ein großes Projekt in  Access umgesetzt hat, weiß wie viel Codezeilen da zusammenkommen können. Die Verwaltung des Codes im Klassenmodul des Formulars bringt oft genug nur scheinbare Ordnung in das „Chaos“. Hat man Formulare, die gleich oder ähnlich aufgebaut sind, kommt der gleiche Code wiederholt zum Einsatz.
In unserem Beispiel werden die immer gleichen Funktionen zum Füllen des TreeView in einem globalen Modul gespeichert. Über entsprechende Parameter wird den Funktionen „mitgeteilt“ auf welche Tabellen bzw. Steuerelemente sie sich jeweils beziehen sollen (siehe Listing 2).
Beim Öffnen des Formulars soll das TreeView automatisch mit den Tabelleneinträgen gefüllt werden. Dazu erstellt man im Ereignis Beim Öffnen eine Ereignisprozedur und ruft die Initialisierungsfunktionen im globalen Modul auf.

   Call DAO_Initialize("tbl_Struktur")
   Call TreeView_Initialize(Me![TreeView])

Die erste Funktion initialisiert die DAO-Umgebungsvariablen. Die DAO-Variablen werden später in der Funktion zum Füllen des TreeView verwendet. Da sich Funktion in einer Schleife immer wieder selbst aufruft, würde für jeden Durchgang eine neue DAO-Variable (Instanz) erzeugt, die ihrerseits neuen  Speicher belegt. Im Parameter übergibt man den Namen der Tabelle, die als Quelle für die Einträge im TreeView dienen soll. Bitte achten Sie darauf das Ereignis Form_Load zu verwenden und nicht Form_Open. Beim Form_Open-Ereignis stehen die Steuerelemente noch nicht zur Verfügung, d.h. sie sind noch nicht in das Formular geladen worden. Deshalb würde eine Referenz auf das Objekt Me![TreeView] nicht zum gewünschten Erfolg führen.
Analog zur Initialisierung ruft man beim Schließen des Formulars (Form_Unload) eine Funktion auf, welche die Objekt-Variablen wieder freigibt (terminiert). Die Begriffe Initialize und Terminate sind hier frei gewählt und sollen nur eine mögliche Vorgehensweise andeuten.
In der  zweiten Funktion (TreeView_Initialize) werden lediglich diverse Eigen¬schaften eingestellt, um sicher zu gehen, dass jedes TreeView gleich aussieht bzw. gleich zu bedienen ist. - Auch wenn diese Methode einige Codezeilen und einen Funktionsaufruf mehr erfordert, hat sie sich in der Vergangenheit als äußerst nützlich erwiesen. - Im Parameter wird ein Verweis auf das Objekt, dessen Eigenschaften verändert werden sollen, gleich mitgegeben.

Rekursive Funktion

Bei einer rekursiven Funktion ruft sich die betreffende Funktion in einer Schleife selbst immer wieder auf. Allerdings sollte man dabei besonders sorgfältig arbeiten, um keine Endlosschleife zu produzieren. Die Funktion TreeView_Fill (Listing 3) übernimmt die Aufgabe das TreeView zu füllen.
Auch wenn der Aufbau eines solchen Hierarchiebaumes eine große Regel-mäßigkeit ausweist, gibt es auch hier eine Ausnahme von dieser Regel: die Einträge der ersten Ebene. Diese Nodes haben keinen ParentKey (übergeordneten Eintrag), d.h. dieser ist NULL (gesprochen Nall), nicht zu verwechseln mit Zahl 0. NULL steht in Access für NICHTS, also keinen Eintrag. Auch nicht zu verwechseln mit dem Leerstring (String der Länge Null), repräsentiert durch den Begriff Empty.
Der optionale Parameter p_ParentKey der TreeView_Fill-Funktion wird auto-matisch NULL gesetzt, wenn er im Funktionsaufruf nicht übergeben wird.
Durch die folgende IF-Then-Anweisung berücksichtigt man ebenso die besondere Stellung von NULL in der Abfragetechnik:

If IsNull(p_ParentKey) Or IsMissing(p_ParentKey) Then
   rstQuelle.Filter = "[Gruppe#] IS NULL"

Der p_ParentKey-Parameter wird nur beim ersten Aufruf nicht übergeben, denn alle nachfolgenden Aufrufe haben den Zweck untergeordnete Nodes anzufügen. Deshalb stellt die Else-Anweisung den Regelfall dar:

Else
   rstQuelle.Filter = "[Gruppe#] = " & p_ParentKey
   p_ParentKey = "A" & p_ParentKey
End If

Hier werden alle die Datensätze gefiltert, die den im Parameter p_ParentKey übergebenen Index in der Spalte [Feld#] eingetragen haben, d.h. alle unterge-ordneten Datensätze.
Es sei noch mal erwähnt, das die Arbeit mit der Eigenschaft Key des Objektes Node sehr sinnvoll ist. Bei jedem Einfügen eines Nodes wird automatisch ein Index vergeben, ähnlich dem Autowertfeld. Dieser besitzt jedoch in der Regel keinerlei Aussagekraft. Die Arbeit mit dem Key ist zwar etwas umständlicher, dafür kann man den Datensatzindex der Tabelle einarbeiten. Der Key muss unbedingt mit einem Buchstaben beginnen, sonst kann logischerweise keine Unterscheidung zwischen Index und Key erfolgen. Denn grundsätzlich kann sowohl über den Index als auch über den Key auf ein Node referenziert werden. In den Key kann man außerdem durch geschickte Wahl der Buchstaben die Hierarchieebene einbauen (z.b. A = 1. Ebene, B = 2. Ebene, usw.).

Set NodeX = TreeView.Nodes.Add(p_ParentKey, tvwChild)

Diese Anweisung fügt ein neues Node in die Struktur ein. Die Angaben der ersten beiden Positionsparameter ParentKey und Relation sind zwar ebenfalls optional, allerdings würde ohne sie der neue Knoten immer am Ende angefügt werden.

Abbildung 2: mögliches Anwendungsbeispiel eines TreeView-Control

Abbildung 2: mögliches Anwendungsbeispiel eines TreeView-Control

Listing 1: vorbereitende und abschließende „Objekt-Arbeit“ im Klassenmodul des Formulars
Private Sub Form_Load()
   Call DAO_Initialize("tbl_Struktur")
   Call TreeView_Initialize(Me![TreeView])
   Call TreeView_Fill(Me![TreeView])
End Sub
Private Sub Form_Unload(Cancel As Integer)
   '** Terminierung der Objekt-Variablen
   Call Variable_Terminate
End Sub
Listing 2: Ereignis-Prozedur beim Node-Klick
Private Sub TreeView_NodeClick(ByVal Node As Object)
   Dim Index As Long
   Index = Right(Node.Key, Len(Node.Key) - 1)
   '** übereinstimmenden Datensatz suchen.
   Me.RecordsetClone.FindFirst "[Index#] = " & Index
   Me.Bookmark = Me.RecordsetClone.Bookmark
   '* Kompletten Pfad anzeigen
   Me![txt_Pfad] = Me![TreeView].Nodes(Node.Index).FullPath
End Sub
Listing 3: universeller Code im globalen Modul
'** private Variablen deklarieren
Private dbs As Database
Private rstQuelle As Recordset
Private NodeX As Node   '* Knoten-Objekt

Public Function DAO_Initialize(ByVal p_Table As String)
   Set dbs = CurrentDb()
   Set rstQuelle = dbs.OpenRecordset(p_Table, dbOpenDynaset)
End Function

Public Function TreeView_Initialize(ByVal TreeView As Object)
   TreeView.Nodes.Clear
   With TreeView
      .Style = tvwTreelinesPlusMinusPictureText
      .LineStyle = tvwRootLines
      .LabelEdit = tvwManual
      .Indentation = 250
      .PathSeparator = "/"
      .HideSelection = False
   End With
End Function
'*** REKURSIVE Funktion zum Füllen des Treeview
Public Function TreeView_Fill(ByVal TreeView As Object, _
                      Optional ByVal p_ParentKey As Variant = Null)

   Dim rstFilter As Recordset

   '** Wenn kein übergeordneter Knoten im Parameter übergeben wurde, ....
   If IsNull(p_ParentKey) Or IsMissing(p_ParentKey) Then
      '* ... dann nach Datensätzen der ersten Ebene filtern (d.h. NULL)
      rstQuelle.Filter = "[Gruppe#] IS NULL"
   Else
      '* ... sonst nach untergeordneten Datensätzen filtern ....
      rstQuelle.Filter = "[Gruppe#] = " & p_ParentKey
      '* ... und den Indexwert in einen TreeView-üblichen KEY umwandeln.
      p_ParentKey = "A" & p_ParentKey
   End If

   rstQuelle.Sort = "[Sortierung]"
   Set rstFilter = rstQuelle.OpenRecordset()

   If rstFilter.RecordCount = 0 Then GoTo RunError:
   rstFilter.MoveFirst

   '** Schleife zum Füllen des TreeView
   While Not rstFilter.EOF
      Set NodeX = TreeView.Nodes.Add(p_ParentKey, tvwChild)
         NodeX.Key = CStr("A" & rstFilter![Index#])
         NodeX.Text = Nz(rstFilter![Eintrag], "")
         Call TreeView_Fill(TreeView, rstFilter![Index#])
      rstFilter.MoveNext
   Wend
   TreeView.Nodes(1).Expanded = True
   Set rstFilter = Nothing

End Function

Public Function Variable_Terminate()

   Set dbs = Nothing
   Set rstQuelle = Nothing
   Set NodeX = Nothing

End Function
Seitenanfang