Teil 6 - Drag & Drop
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.
Grundsätzliche Überlegungen
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.
Drag&Drop im Detail
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.
- 1.a) der Klick auf einen Eintrag im TreeView löst das MouseDown-Ereignis des Controls aus. Die Funktion, die auf dieses Ereignis reagiert, liefert diverse Parameter: der Button-Parameter beinhaltet Informationen darüber welche Maustaste gedrückt wurde. Die entsprechenden Tasten werden dabei durch die Bitwert 0, 1 und 2 repräsentiert: was den Dezimalzahlen 1 für die linke, 2 für die rechte und 4 für die mittlere Maustaste entspricht. Die Zahl 3 würde bedeuten, dass die linke und die rechte Maustaste gleichzeitig gedrückt wurden, was bei Maustasten allerdings relativ schwierig zu bewerkstelligen ist! Im Shift-Parameter werden Informationen über den Status der Umschalt-, Alt- und Strg-Tasten übergeben. Die Handhabung gleicht dem Button-Parameter. Noch interessanter sind die X- und Y-Parameter, sie repräsentieren die Koordinaten im TreeView. Das TreeView weiß beim Drücken der Maustaste noch nicht welcher Knoten davon betroffen sein wird, oder ob überhaupt ein Eintrag unter dem Mauszeiger vorhanden ist. Diese Information liefert das TreeView erst nach dem Loslassen der Maustaste, was wiederum das NodeClick-Ereignis auslöst, aber keine Drag&Drop-Aktion einleitet. Drag&Drop bedeutet das Ziehen und Ablegen eines Objektes bei gedrückter Maustaste auf ein anderes Objekt. Aus diesem Grund muss über die xy-Koordinaten der betroffene Eintrag ermittelt werden. Diese Aufgabe übernimmt die HitTest-Methode, der man die Koordinaten in den Parametern übergibt. Die HitTest-Methode liefert einen Objekt-Verweis auf den betroffenen Eintrag zurück. Über dieses Objekt kann sofort die Key-Eigenschaft abgefragt werden und auf eine private, nur im Klassenmodul gültige Variable gesichert werden. Die Variable g_SourceKey beinhaltet den Key des zu verschiebenden Knotens. Wir haben hier zum Mittel der öffentlichen Variable gegriffen da die Drag&Drop-Aktion noch nicht vollständig ist und deshalb diese Information zwischengespeichert werden muss. Wenn kein Knoten beim Drücken der Maustaste betroffen ist, wird ein Fehler mit der Nummer 91 ausgelöst.
- 1.b) Das OLEDragOver-Ereignis des TreeView ist im Grunde genommen nicht besonders wichtig, trotzdem erleichtert es dem Anwender das Zielen. Diese Funktion wird beim Ziehen der Maus im Drag&Drop-Modus, d.h. bei gedrückter Maustaste, ausgelöst. Mit Hilfe der DropHighlight-Eigenschaft und der HitTest-Methode kann man so den jeweiligen Eintrag unter dem Mauszeiger beim Überziehen temporär markiert. Die HitTest-Methode liefert den Verweis auf das Node-Objekt welches gerade überzogen wird und wird auf die DropHighlight-Eigenschaft gesetzt, welche die Markierung des Knotens simuliert. Beim Loslassen der Maustaste muß die DropHighlight-Eigenschaft gelöscht werden. Da die Eigenschaft einen Objekt-Verweis beinhaltet muß die Variable auf Nothing gesetzt werden. Ansonsten könnten Sie zwar den Fokus auf andere Knoten im TreeView verschieben, aber nicht mehr die Markierung!
- 1.c) Das Loslassen der Maustaste nach einer Drag&Drop-Aktion löst das OLEDragDrop-Ereignis aus. Auch jetzt muss der Ziel-Knoten ermittelt und die Key-Eigenschaft zwischengespeichert werden. Die Drag&Drop-Aktion ist damit beendet. Was wir jetzt mit den Informationen anstellen, die wir währenddessen gesammelt haben, ist unsere Sache. Wir möchten die Daten in der Tabelle und im TreeView aktualisieren und rufen deshalb die benutzerdefinierte Basis-Funktion TreeView_DragDrop im globalen Modul vba_TreeView auf.
Datenhandling nach Drag&Drop
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.
- 2.a) So sollen zum Beispiel Zirkelbezüge vermieden werden, deshalb muss geprüft werden, ob der Zielknoten gleich dem Quellknoten, oder ob das Ziel dem Quelleintrag untergeordnet ist. Letztere Aufgabe übernimmt die rekursive Funktion TreeView_IsSubNode (2.d). Genauso muss sichergestellt sein, dass Ziel und/oder Quelle gültige TreeView-Einträge sind, ansonsten müsste die Funktion abgebrochen werden. Der Drag&Drop-Vorgang würde sozusagen ins Leere laufen. Der Basis-Funktion wird außerdem im p_Shift-Parameter der Zustand der Umschalt-, Alt, und/oder Strg-Taste übergeben. Beim Ablegen eines Knotens auf einen Anderen, kann dieser gleich- (tvwNext) oder untergeordnet (tvwChild) zugewiesen werden. Soll der Eintrag in dieselbe Ebene wie der Zielknoten angefügt werden soll, setzen wir voraus, dass beim Loslassen der Maustaste die Alt-Taste gedrückt ist. In diesem Fall muss der übergeordnete Knoten des Zielknoten, bzw. dessen Key, ermittelt werden. Dieser Umstand resultiert aus der Verwendung des AutoJoins. Anschließend werden die entsprechenden Funktionen zum Anpassen der Tabellendaten (2.b) bzw. der TreeView-Ansicht (2.c) aufgerufen. Letztere allerdings nur, wenn die Rücksicherung der Daten keine Probleme verursacht hat.
- 2.b) Nach der Kontrolle, ob Ziel- und Quellkeys korrekt übergeben wurden, wird den Keys jeweils der vorangestellte Buchstabe abgeschnitten (Extract_Index). Im Anschluß wird die betroffene Tabelle geöffnet, nach dem Datensatz des Quellknoten gefiltert, und diesen mit dem Zieleintrag neu verknüpft bzw. untergeordnet (Feld Gruppe#). Das Ergebnis wird an die Basis-Funktion zurückgegeben. Bei Erfolg geht’s weiter mit der Funktion TreeView_DragDrop_Node (2.c).
- 2.c) Diese Funktion aktualisiert die Ansicht im TreeView-Steuerelement. Dazu benötigt wir die Informationen bzw. den Objekt-Verweis auf das TreeView, den Namen der betroffenen Tabelle, den Key des Quell- bzw. des Zielknotens. Diese Angaben werden in den Parametern übergeben. Vielleicht fragen Sie sich jetzt wofür der Tabellenname benötigt wird. Das liegt daran, dass ein Großteil dieser Funktion „alte“, in dieser Artikelfolge früher erstellte, Funktionen aufruft , nämlich zum Löschen (TreeView_Delete_Node) und Anfügen (TreeView_AddNew_Node) von Einträgen. Spätestens jetzt zahlt es sich aus, dass wir unseren Code so allgemeingültig wie möglich gehalten haben!
Abbildung 1: Erweiterung des globalen Moduls um die Drag&Drop-Funktion
Abbildung 2: Drag&Drop-Funktion im TreeView
Listing 1: Drag&Drop-Ereignisse im Klassenmodul
'* 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
Listing 2: Drag&Drop-Funktionen im globalen Modul
'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