Ausgabe 04/2001
Im letzen Teil dieser Artikelfolge widmen wir uns ganz der Drag&Drop–Funktion des TreeView-Controls. Das TreeView-Steuerelement eignet sich hervorragend dazu Strukturen und hierarchische Beziehungen kompakt abzubilden. Leider haben Strukturen und Beziehungen die „schlechte“ Angewohnheit sich zu ändern. Mit herkömmlichen Mitteln ist dieser Tatsache oft nur mühsam beizukommen: Programmieraufwand, Nutzen und Anwenderfreundlichkeit kollidieren dabei nicht selten. Mit Hilfe eines TreeView und dessen Drag&Drop - Funktion lässt sich relativ leicht Abhilfe schaffen. Zudem ist vielen Anwendern die Drag&Drop– Funktion vom Windows-Explorer bekannt.
Bevor wir uns an die Umsetzung begeben, müssen wir noch einige grundsätzliche Überlegungen machen. Die Daten, die in der Strukturansicht verändert und verschoben werden, müssen auch wieder in die Tabellen zurückge¬schrieben werden. Leider nimmt uns Access diese Arbeit nicht wie gewohnt ab. Welche Vorgehensweise empfiehlt sich beim Drag&Drop? Vielleicht ist Ihnen ja schon folgender Gedanke gekommen: lassen wir den Anwender im TreeView verschieben und verändern wie er möchte und sichern die Daten anschließend, nach Aufforderung, in die Datenbank zurück.
Diese Vorgehensweise hört sich einfach an, stößt aber in der Praxis auf einige Probleme. Für unserBeispiel hätte dieses Vorgehen fast noch funktionieren können, da wir die Struktur in einer einzigen Tabelle abbilden (per AutoJoin). Aber genau dieser Umstand ist es auch der uns Probleme bereitet. Excel-Anwendern ist eine vergleichbare Situation unter dem Begriff Zirkelbezug bekannt. Ein Zirkelbezug ist eine Formel in einer Zelle, die sich auf sich selbst bezieht. Genauso so ist es theoretisch möglich, dass ein Eintrag sich selbst oder einem seiner untergeordneten Einträge untergeordnet würde. Während Sie die ersten Möglichkeit noch mit Feldrestriktionen in der Form [Index#] [Gruppe#] beikommen, ist die zweite Möglichkeit doch ein bißchen aufwendiger zu verhindern. Wenn kein AutoJoin sondern mehrere Tabellen als Quelle im TreeView verwendet werden, können Tabellenrestriktionen, wie zum Beispiel die Einhaltung der referentiellen Integrität, das Rücksichern der Daten in die Tabellen erschweren oder gar verhindern. Aus diesen Gründen ist es ratsam die Aktualisierung der Daten zuerst bzw. sofort nach jedem Drag&Drop–Vorgang durchzuführen. Wenn diese Aktion erfolgreich war, kann das TreeView im zweiten Schritt der neuen Situation angepasst werden.
Zunächst wollen wir die Drag&Drop-Funktion in Ihre elementaren Schritte zerlegen. Die Funktionen in Listing 1 und 2 kann man sich eine nach der anderen vornehmen und besprechen, da jede Funktion einen solchen Schritt beinhaltet.
Dabei stellen die Prozeduren im Listing 1 die Ereignisse der Drag&Drop-Aktion im Klassenmodul des Formulars dar und die Funktionen im Listing 2 erledigen die mühsame Arbeit der Aktualisierung der Daten in der Tabelle und im TreeView.
In der schematischen Abbildung 1 sehen Sie die Erweiterung des globalen Moduls um die Funktionen zur Implementierung der Drag&Drop-Funktion. Auch hier arbeiten die Funktionen unabhängig vom betroffenen TreeView oder der betroffenen Tabelle, diese Informationen werden allesamt der Basis-Funktion übergeben, die diese wiederum sinnvoll an die untergeordneten Funktion zur Aktualisierung der Tabelle und der Strukturansicht weitergibt. Vorher und dazwischen sind allerdings einige Plausibilitätsuntersuchungen bzw. Abbruchbedingungen nötig.
'* Globale Variablen werden in der Formularklasse verwendet
Private g_SourceKey As String
Private g_TargetKey As String
'a)*** Drag'N'Drop bei gedrückter linker Maustaste auslösen ***
Private Sub TreeView_MouseDown(ByVal Button As Integer, _
ByVal Shift As Integer, _
ByVal x As Long, _
ByVal y As Long)
On Error GoTo RunError
Select Case Button
Case acLeftButton
'* Quellknoten ermitteln und speichern
g_SourceKey = Me![TreeView].HitTest(x, y).Key
End Select
RunError:
Select Case Err.Number
Case 0
Case 91
'* kein Node markiert (x,y nicht verfügbar)
Err.Clear
Case Else
MsgBox Err.Description, vbCritical, "No. " & Err.Number
End Select
End Sub
'b)*** Ereignis beim Ziehen mit der Maus über das TreeView ***
Private Sub TreeView_OLEDragOver(Data As Object, Effect As Long, _
Button As Integer, Shift As Integer, _
x As Single, y As Single, _
State As Integer)
'* Knoten markieren, über den beim Drag'N'Drop gezogen wird
Set Me![TreeView].DropHighlight = Me![TreeView].HitTest(x, y)
End Sub
'c)*** Drag'N'Drop ausführen ***
Private Sub TreeView_OLEDragDrop(Data As Object, Effect As Long, _
Button As Integer, Shift As Integer, _
x As Single, y As Single)
On Error GoTo RunError
Dim tvw As Object
Dim ndX As Node
Dim yon As Boolean '* YesOrNot
'* Zielknoten ermitteln und speichern
g_TargetKey = Me![TreeView].HitTest(x, y).Key
'* Drag'N'Drop auslösen
yon = TreeView_DragDrop(TreeView:=Me![TreeView], _
p_TableName:="tbl_Struktur", _
p_SourceNode:=g_SourceKey, _
p_TargetNode:=g_TargetKey, _
p_Shift:=Shift)
'* DropHighlight entfernen
Set Me![TreeView].DropHighlight = Nothing
Set tvw = Me![TreeView]
'* Aktuell markierten Knoten ermitteln
Set ndX = tvw.SelectedItem
'* Node-Klick-Ereignis anstossen
Call TreeView_NodeClick(ndX)
Me.Refresh
RunExit: '* globale Variablen zurücksetzen
g_SourceKey = ""
g_TargetKey = ""
RunError:
Select Case Err.Number
Case 0 '* Kein Fehler aufgetreten
Case 91 '* Kein Objekt verfügbar HitTest nicht erfolgreich
Err.Clear
Case Else
MsgBox Err.Description, vbCritical, "No. " & Err.Number
End Select
End Sub
'a)* Basisfunktion, die die Funktionen zur Aktualisierung der Daten
'* in Tabelle und TreeView verwaltet
Public Function TreeView_DragDrop(ByVal TreeView As Object, _
ByVal p_TableName As String, _
ByVal p_SourceNode As Variant, _
ByVal p_TargetNode As Variant, _
ByVal p_Shift As Long) As Boolean
On Error GoTo RunError
Dim tof As Boolean
TreeView_DragDrop = False
'* Plausibilitäts-Kontrolle *
'* Ist Quell- und Zielknoten identisch, dann Abbruch!
If p_SourceNode = p_TargetNode Then GoTo RunError
'* Wurden gültige Quell- und Zielknoten übergeben?
If p_SourceNode = "" Then GoTo RunError
If p_TargetNode = "" Then GoTo RunError
'* Wenn Ziel-Knoten ein untergeordneter Knoten ist, dann Abbruch
If TreeView_IsSubNode(TreeView:=TreeView, _
p_SubNode:=p_TargetNode, _
p_Node:=p_SourceNode) = True Then GoTo RunError
'* Wenn in gleiche Ebene verschoben werden soll (gedrückte ALT-Taste), ...
If p_Shift = acAltMask Then
'* ... dann übergeordneten Knoten ermitteln!
p_TargetNode = TreeView.Nodes(p_TargetNode).Parent.Key
End If
'* Drag'N'Drop in Tabelle und TreeView ausführen *
'* neuen Knotenzuordnung in der Tabelle vornehmen
tof = TreeView_DragDrop_Table(p_TableName:=p_TableName, _
p_Index:=p_SourceNode, _
p_Gruppe:=p_TargetNode)
'* Wenn Aktion fehlgeschlagen, dann Abbruch!
If tof = False Then GoTo RunError
'* neue Knotenzuordnung im TreeView vornehmen
tof = TreeView_DragDrop_Node(TreeView:=TreeView, _
p_TableName:=p_TableName, _
p_Key:=p_SourceNode, _
p_ParentKey:=p_TargetNode)
'* Wenn Aktion fehlgeschlagen, dann Abbruch!
If tof = False Then GoTo RunError
TreeView_DragDrop = True
RunError:
Select Case Err.Number
Case 0 '* kein Fehler
'* - kein übergeordneter Node (Parent) vorhanden
'* - kein Node markiert
Case 91:
p_TargetNode = Null
Resume Next
Case Else
MsgBox Err.Description, vbCritical, "No." & Err.Number
End Select
End Function
'b)* Funktion aktualisiert Daten in Tabelle
Public Function TreeView_DragDrop_Table(ByVal p_TableName As String, _
ByVal p_Index As Variant, _
ByVal p_Gruppe As Variant) As Boolean
On Error GoTo RunError
Dim dbs As Database
Dim trg As Recordset
TreeView_DragDrop_Table = False
'* Plausibilitäts-Kontrolle *
'* Index aus "Quellknoten" extrahieren
p_Index = Extract_Index(p_Index)
'* Wenn "Zielknoten" nicht NULL, ...
If Not IsNull(p_Gruppe) Then
'* ... dann Index aus "Zielknoten" extrahieren.
p_Gruppe = Extract_Index(p_Gruppe)
End If
'* Drg'N'Drop in Tabelle vornehmen *
Set dbs = CurrentDb()
Set trg = dbs.OpenRecordset(Name:=p_TableName, _
Type:=dbOpenDynaset)
'* Nach Quell-Knoten filtern
trg.Filter = "[Index#]=" & p_Index
Set trg = trg.OpenRecordset()
'* Wenn kein Datensatz verhanden, dann Abbruch!
If trg.RecordCount = 0 Then GoTo RunExit
'* Eintrag für übergeordneten Datensatz
'* im Feld "Gruppe#" vornehmen.
trg.Edit
trg![Gruppe#] = p_Gruppe
trg.Update
TreeView_DragDrop_Table = True
RunExit:
'* DAO-Variablen schliessen
trg.Close
dbs.Close
'* DAO-Variablen terminieren
Set trg = Nothing
Set dbs = Nothing
RunError:
If Err Then MsgBox Err.Description, vbCritical, "No." & Err.Number
End Function
'c)* Funktion aktualisiert Ansicht im TreeView
Public Function TreeView_DragDrop_Node(ByVal TreeView As Object, _
ByVal p_TableName As String, _
ByVal p_Key As Variant, _
ByVal p_ParentKey As Variant) As Boolean
On Error GoTo RunError
Dim tof As Boolean
Dim v_Text As String
Dim v_Key As Variant
TreeView_DragDrop_Node = False
'* 1. Zuerst den Text des Knotens zwischenspeichern
v_Text = TreeView.Nodes(p_Key).Text
'* 2. den alten Eintrag entfernen!
tof = TreeView_Delete_Node(TreeView, p_Key)
'* Wenn Aktion fehlgeschlagen, dann Abbruch!
If tof = False Then GoTo RunError
'* Index aus "Quellknoten" extrahieren
p_Key = Extract_Index(p_Key)
'* Wenn "Zielknoten" nicht NULL, ...
If Not IsNull(p_ParentKey) Then
'* ... dann Index aus "Zielknoten" extrahieren.
p_ParentKey = CStr(Extract_Index(p_ParentKey))
End If
'* Drag'N'Drop im TreeView vornehmen *
'* 3. den verschobenen Eintrag neu einfügen!
v_Key = TreeView_AddNew_Node(TreeView:=TreeView, _
p_TableName:=p_TableName, _
p_NewIndex:=p_Key)
'* Wenn Aktion fehlgeschlagen, dann Abbruch!
If IsNull(v_Key) Then GoTo RunError
'* 4. Alle eventuell untergeordneten Einträge ebenfalls neu anfügen
Call DAO_Initialize(p_TableName)
Call TreeView_Fill(TreeView:=TreeView, _
p_ParentKey:=Extract_Index(v_Key))
'* 5. Markierung auf neu angefügten Node setzen.
TreeView.Nodes(v_Key).Selected = True
'* Wenn nötig Bildlauf durchführen.
TreeView.Nodes(v_Key).EnsureVisible
TreeView_DragDrop_Node = True
RunError:
If Err Then MsgBox Err.Description, vbCritical, "No." & Err.Number
End Function
'd)* durchforstet die untergeordneten Eintrag, ob ZielKnoten darin enthalten ist
Public Function TreeView_IsSubNode(ByVal TreeView As Object, _
ByVal p_SubNode As Variant, _
ByVal p_Node As Variant, _
Optional ByVal p_IsSubNode As Boolean = False) As Boolean
On Error GoTo RunError
Dim dbs As Database
Dim sys As Recordset
Dim ndX As Node
Dim yon As Boolean '* YesOrNot
Dim cnt As Long '* CouNTer
Dim lp0 As Long '* Loop0
'* Funktion auf FALSE setzen
TreeView_IsSubNode = p_IsSubNode
'* Objekt-Variable mit Quell-Knoten belegen
Set ndX = TreeView.Nodes(p_Node)
'* Anzahl der direkt untergeordneten Knoten ermitteln
cnt = ndX.Children
If cnt > 0 Then
Set ndX = ndX.Child
Set ndX = ndX.FirstSibling
For lp0 = 1 To cnt
If ndX.Key = p_SubNode Then
p_IsSubNode = True '* Funktion (erfolgreich!)
Else
p_IsSubNode = TreeView_IsSubNode(TreeView:=TreeView, _
p_SubNode:=p_SubNode, _
p_Node:=ndX.Key)
End If
'* Wenn Unterknoten gleich WAHR, dann Abbruch
If p_IsSubNode = True Then GoTo Cancel
'* Nächster gleichgeordneter Knoten
Set ndX = ndX.Next
Next
End If
Cancel:
'* Funktion auf TRUE/FALSE setzen (erfolgreich?)
TreeView_IsSubNode = p_IsSubNode
Set ndX = Nothing
RunError:
If Err Then MsgBox Err.Description, vbCritical, "No. " & Err.Number
End Function