Ausgabe 01/2001
Nachdem wir im zweiten Teil dieser Artikelfolge erläutert haben, wie man den Inhalt eines TreeView dynamisch mit den Einträgen einer Tabelle füllt, möchten wir Ihnen jetzt die Handhabung zur Laufzeit erläutern. Denn ein TreeView eignet sich nicht nur dazu, schon vorhandene Daten anzuzeigen, sondern auch, um Daten unmittelbar zur Laufzeit einzugeben. Natürlich gibt es auch hierzu mehrere Möglichkeiten, die zum selben Ergebnis führen.
Grundsätzlich stellt sich die Frage:
Um das Handling mit dem TreeView zu erläutern, möchten wir Ihnen letztere Variante näher bringen ohne diese zu favorisieren. Letztendlich sollten die Anforderungen oder persönliche Vorlieben die Vorgehensweise beeinflussen.
Auch in diesem Folge soll der Code allgemeingültig und wiederverwendbar gestaltet werden, was allerdings eine gewisse Fragmentierung des Quellcodes nach sich zieht. Fragmentierung soll heißen, dass für jeden kleinen Schritt eine eigene Funktion zuständig sein soll. Dabei kann man leicht den Überblick verlieren. Die Namensgebung spielt dabei eine wichtige Rolle. Schon im 2. Teil haben wir ein globales Modul angelegt, welches den allgemeingültigen Code zum Füllen (bzw. Initialisieren) des TreeView enthält. Dieses Modul wird nun um die Funktionen zum Einfügen (AddNew) bzw. Löschen (Delete) ergänzt. In Abbildung 1 ist der logische Aufbau dargestellt. Der linke „Ast“ wurde bereits in der letzten Folge implementiert.
Bei der Neuanlage (mittlerer Ast) und beim Löschen (rechter Ast) von Daten sind jeweils zwei elementare Schritte nötig: die Eintragung muss in das TreeView und in die Tabelle vorgenommen werden. Die Reihenfolge spielt dabei grundsätzlich keine Rolle. Allerdings hat es sich gezeigt, dass es sinnvoll ist, zuerst die Speicherung in die Tabelle vorzunehmen, bevor das TreeView angepasst wird. Eventuelle Probleme (z.B. beim Speichern), die einen Abbruch des Vorgangs nötig machen, treten auf bevor das TreeView angepasst wird und können so frühzeitig abgefangen werden - etwa durch Fehlerbehandlung oder Fehlermeldung. Meist entfällt so das Rückgängig machen der Aktion, was z.B. beim Löschen relativ aufwendig ist.
Eine jeweilige „Basis“-Funktion koordiniert dabei diverse „Unter“-Funktionen, die ihrerseits eine bestimmte Teilaufgaben erledigen. Dadurch muss beim Klick auf eine Schaltfläche nur eine einzelne Funktion aufgerufen werden. Die Bereitstellung der jeweiligen Parameter bzw. eventuelle Modifikationen für nachgelagerte Funktionen übernimmt die Basis-Funktion. Außerdem werden einige Parameter erst durch den Aufruf einer vorgelagerten Funktion erzeugt.
Folgendes Beispiel soll das verdeutlichen: Ein neuer Eintrag soll angelegt werden: Dafür muss die Basis-Funktion [TreeView_AddNew] wissen welches TreeView-Steuerelement (Formular- und Steuerelementname) und welche Tabelle (die den Eintrag speichern soll) von der Aktion betroffen sind. Außerdem spielt die Beziehung des neuen Eintrags zum aktuell markierten Knoten (Node) eine entscheidende Rolle. Sie erinnern sich vielleicht, dass wir uns darauf geeinigt haben jeden Eintrag im TreeView einen eindeutigen Schlüssel für geben, den wir der auf die Node-Eigenschaft Key schreiben. Dieser soll mit einem Buchstaben beginnen (hier: A) und mit dem Index des Tabelleneintrages enden. Der Index existiert aber erst ab dem Zeitpunkt, an dem Datensatz angelegt wird. Aus diesem Grund ist es in unserem Beispiel nötig den Tabelleneintrag zuerst vorzunehmen, den neuen Index zu ermitteln und der nachgelagerten Funktion zum Anpassen des TreeView zur Verfügung zu stellen.
In Abbildung 2 sehen Sie unser altes Formular, welches durch diverse Schaltflächen um einige Handling-Funktionen erweitert wurde. Um einen neuen Eintrag vorzunehmen, stehen zwei Optionen offen; einen Eintrag auf derselben Ebene, wie der zum Klick-Zeitpunkt markierte Knoten, oder einen untergeordneten Eintrag (Child). Die Quellcode für beide Aktionen weicht nur geringfügig voneinander ab und kann in den Funktionen leicht berücksichtigt werden. Denn in beiden Fällen müssen sie den übergeordneten Knoten (Parent) ermitteln. Wenn sie einen Child-Knoten einfügen möchten, ist der aktuell markierte Eintrag der Parent-Knoten. Auch wenn sie einen Eintrag auf derselben Ebene vornehmen möchten, müssen Sie trotzdem ermitteln in welcher Ebene sie sich gerade. Gibt es keinen Parent-Knoten, dann ist entweder kein Knoten markiert oder sie befinden sich gerade auf der obersten Ebene. Um diesen Fall abzufangen, muss eine Fehlerbehandlungsroutine implementiert werden, da der Zugriff auf einen nicht existierenden Parent-Knoten einen Fehler verursacht (Fehler-Nr. 91).
Select Case ...
Case ...
Case 91:
v_ParentIndex = Null
Resume Next
Case ...
End Select
Ist ein Parent-Knoten vorhanden, wird die Key-Eigenschaft ausgelesen, da diese den Index des entsprechenden Datensatzes aus der Tabelle enthält. Dieser Index muss noch extrahiert werden:
Erst jetzt kann die Funktion zum Anlegen eines neuen Datensatzes in der Tabelle aufgerufen werden:
v_AddedIndex = TreeView_AddNew_Table(p_TableName, v_ParentIndex)
Diese Funktion braucht ihrerseits nur den Namen der betreffenden Tabelle und die Information für den Eintrag in das Feld [Gruppe#], d.h. welchem Datensatz der neue Eintag untergeordnet ist. Ganz wichtig hierbei ist das Ergebnis der Funktion. Der Rückgabewert stellt den Index des neuen Datensatzes dar. Nur wenn dieser ungleich 0 ist, war die Neuanlage erfolgreich.
Der Index (ungleich 0) wird anschließend an die nachgelagerte „Teil“-Funktion, zum Hinzufügen eines Knoten ins TreeView, übergeben:
TreeView_AddNew = TreeView_AddNew_Node(TreeView, v_AddedIndex, v_ParentIndex)
Das Ergebnis dieser Funktion, der Key des neuen Nodes, kann für weiter-führende Zwecke verwendet werden. In unserem Beispiel, um die Markierung auf den neu angefügten Knoten zu setzen, und um eventuell nötige Bildläufe (bzw. Ast aufklappen) durchzuführen.
Ähnlich wie beim Anlegen verhält sich das Vorgehen beim Löschen eines Datensatzes. Erst sollte man sicherstellen, dass die Löschung überhaupt erfolgreich war, bevor man den Eintrag aus dem TreeView entfernt (Listing 2).
Im Listing 3 sehen sie die Klick-Ereignis-Prozeduren der Schaltflächen des Formulars. Die beiden Prozeduren zum Anfügen eines Knoten zeigen außerdem, wie man den Rückgabewert (den Key) sinnvoll weiterverwenden kann, um z.B. nach der Aktualisierung des Formularinhalts ein Node-Klick-Ereignis zu simulieren, damit der neue Datensatz sofort im Formular angezeigt wird. Anschließend wechselt das TreeView in den Editiermodus.
Me.Requery
Call TreeView_NodeClick(Me![TreeView].SelectedItem)
TreeView.StartLabelEdit
Das Betätigen der Eingabetaste nach der Änderung des Knoten löst zwei Ereignisse aus: BeforeLabelEdit und AfterLabelEdit. Diese Ereignisse können weiter Aktionen beinhalten.
Die LabelEdit-Eigenschaft (siehe Listing 1 des 2. Teils - Funktion TreeView_Initialize) des TreeView regelt dabei, ob der Editiermodus für den Knoten manuell [tvwManual] oder automatisch [tvwAutomatic], durch zwei einfache Mausklicks, ausgelöst werden muss bzw. kann.
'*** Basis-Funktion: neuen Eintrag vornehmen ***
'***********************************************
Public Function TreeView_AddNew(ByVal TreeView As Object, _
ByVal p_TableName As String, _
Optional ByVal p_Relation As Long = tvwLast) As String
On Error GoTo RunError
Dim v_ParentIndex As Variant '* Index des übergeordneten Nodes
Dim v_AddedIndex As Long '* Index des angefügten Datensatzes
Select Case p_Relation '* ParentNode ermitteln,
Case tvwLast '* Key des übergeordnete Node ermitteln
v_ParentIndex = TreeView.Nodes(TreeView.SelectedItem.Key).Parent.Key
Case tvwChild '* Key des aktuell markierten Nodes ermitteln.
v_ParentIndex = TreeView.SelectedItem.Key
End Select
If Not IsNull(v_ParentIndex) Then
'* ... dann im Key enthaltenen Index extrahieren,
v_ParentIndex = Right(v_ParentIndex, Len(v_ParentIndex) - 1)
End If
'* Neuen Datensatz in Tabelle einfügen,
v_AddedIndex = TreeView_AddNew_Table(p_TableName, v_ParentIndex)
If v_AddedIndex = 0 Then GoTo RunError
'* Neuen Datensatz im TreeView anfügen
TreeView_AddNew = TreeView_AddNew_Node(TreeView, v_AddedIndex, v_ParentIndex)
TreeView.Nodes(TreeView_AddNew).Selected = True '* Markierung setzen
TreeView.Nodes(TreeView_AddNew).EnsureVisible '* Bildlauf durchführen.
RunError:
Select Case Err.Number
Case 0 '* kein Fehler
Case 91: '* - nichts markiert oder kein übergeordneter Node (Parent) vorhanden
v_ParentIndex = Null
Resume Next
Case Else
MsgBox Err.Description, vbCritical, "No." & Err.Number
End Select
End Function
'*** Sub-Funktion: neuen Eintrag in Tabelle vornehmen ***
'*************************************************************
Public Function TreeView_AddNew_Table(ByVal p_TableName As String, _
Optional ByVal p_ParentIndex As Variant = Null) As Long
Dim rstZiel As Recordset
Dim v_Index As Long
TreeView_AddNew_Table = 0
Set dbs = CurrentDb()
Set rstZiel = dbs.OpenRecordset(p_TableName, dbOpenDynaset)
rstZiel.AddNew
'* Index des neuen Datensatzes auslesen
v_Index = rstZiel![Index#]
rstZiel![Eintrag] = "Neuer Eintrag"
rstZiel![Gruppe#] = p_ParentIndex
rstZiel.Update
rstZiel.Close
dbs.Close
TreeView_AddNew_Table = v_Index
Set rstZiel = Nothing
Set dbs = Nothing
End Function
'*** Sub-Funktion: Eintrag in TreeView vornehmen ***
'***************************************************
Public Function TreeView_AddNew_Node(ByVal TreeView As Object, _
ByVal v_NewIndex As Long, _
Optional ByVal p_ParentIndex As Variant = Null, _
Optional ByVal p_Text As String = "Neuer Eintrag") As Variant
TreeView_AddNew_Node = Null
Set NodeX = TreeView.Nodes.Add(("A" + p_ParentIndex), tvwChild)
NodeX.Key = CStr("A" & v_NewIndex)
NodeX.Text = p_Text
TreeView_AddNew_Node = NodeX.Key
End Function
'*** Basis-Funktion: markierten Eintrag löschen ***
'**************************************************
Public Function TreeView_Delete(ByVal TreeView As Object, _
ByVal p_TableName As String, _
Optional ByVal p_NodeKey As Variant = Null) As Boolean
On Error GoTo RunError
Dim v_DelIndex As Long '* Index des zu löschenden Datensatzes
Dim tof As Boolean '* TrueOrFalse
TreeView_Delete = False
'* Wenn Node-Key nicht übergeben wurde
If IsNull(p_NodeKey) Then
DoCmd.Beep
GoTo RunError
Else '* ... sonst im Key enthaltenen Index extrahieren,
v_DelIndex = Right(p_NodeKey, Len(p_NodeKey) - 1)
End If
'* Eintrag zuerst aus Tabelle löschen
tof = TreeView_Delete_Table(p_TableName, v_DelIndex)
If tof = False Then GoTo RunError
'* Löschung des Node aus dem TreeView
tof = TreeView_Delete_Node(TreeView, p_NodeKey)
TreeView_Delete = tof
RunError:
If Err Then MsgBox Err.Description, vbCritical, "No." & Err.Number
End Function
'*** Sub-Funktion: Eintrag im TreeView löschen ***
'*************************************************
Public Function TreeView_Delete_Table(ByVal p_TableName As String, _
ByVal p_Index As Long) As Boolean
On Error GoTo RunError
Dim sql As String
TreeView_Delete_Table = False
sql = ""
sql = sql & "DELETE"
sql = sql & " [Index#]"
sql = sql & " FROM"
sql = sql & " [" & p_TableName & "]"
sql = sql & " WHERE"
sql = sql & " [Index#]=" & p_Index
sql = sql & ";"
DoCmd.SetWarnings False '* Systemmeldungen deaktivieren
DoCmd.RunSQL sql, True '* SQL-Befehl ausführen
TreeView_Delete_Table = True
RunError:
DoCmd.SetWarnings False '* Systemmeldungen wieder aktivieren
If Err Then MsgBox Err.Description, vbCritical, "No." & Err.Number
End Function
'*** Sub-Funktion: Eintrag im TreeView löschen ***
'*************************************************
Public Function TreeView_Delete_Node(ByVal TreeView As Object, _
Optional ByVal p_NodeKey As Variant = Null) As Boolean
TreeView_Delete_Node = False
TreeView.Nodes.Remove p_NodeKey
'*' oder: '*' Call TreeView.Nodes.Remove(p_NodeKey)
TreeView_Delete_Node = True
End Function
'*** Schaltfläche: neuen untergeordneten Eintrag ***
'***************************************************
Private Sub btn_Node_AddNew_Child_Click()
Call TreeView_AddNew(Me![TreeView, "tbl_Struktur", tvwChild)
Me.Requery '* Daten aktualisieren
'* Node-Click-Ereignis anstossen
Call TreeView_NodeClick(Me![TreeView].SelectedItem)
TreeView.StartLabelEdit '* Editiermodus
End Sub
'*** Schaltfläche: neuen Eintrag (gleicher Ebene) ***
'****************************************************
Private Sub btn_Node_AddNew_Last_Click()
Call TreeView_AddNew(Me![TreeView], "tbl_Struktur, tvwLast)
Me.Requery
'* Node-Click-Ereignis anstossen
Call TreeView_NodeClick(Me![TreeView].SelectedItem)
TreeView.StartLabelEdit
End Sub
'*** Schaltfläche: Eintrag löschen ***
'*************************************
Private Sub btn_Node_Delete_Click()
Dim tof As Boolean '*True OR False
'* Eintrag aus Tabelle und TreeView löschen
tof = TreeView_Delete(Me![TreeView], _
"tbl_Struktur, _
Me![TreeView].SelectedItem.Key)
If tof = True Then
Me.Requery
Call TreeView_NodeClick(Me![TreeView].SelectedItem)
End If
End Sub
'*** Schaltfläche: aktualisieren ***
'***********************************
Private Sub btn_Treeview_Actualize_Click()
Call Form_Load
End Sub
'*** Node nach Editierung speichern ***
'**************************************
Private Sub TreeView_AfterLabelEdit(Cancel As Integer, _
NewString As String)
Me![Eintrag] = NewString
Me.Refresh
End Sub