Kontakt-Formular   Inhaltsverzeichnis   Druckansicht  

VisualBasic.tips

Startseite > Microsoft Access VBA > ActiveX Treeview > Teil 6 - Drag & Drop

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.

  • 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 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
Seitenanfang