Teil 6 - Drag & Drop

Ausgabe 04/2001

Download Beispieldatei

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.

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.

Abbildung 1: Erweiterung des globalen Moduls um die Drag&Drop-Funktion

Abbildung 1: Erweiterung des globalen Moduls um die Drag&Drop-Funktion

Abbildung 2: Drag&Drop-Funktion im TreeView

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