Introduction.
I hope you found, last week’s tutorial on ImageCombo Control useful in your Microsoft Access Projects. With the TreeView ImageCombo Control, we could create a beautiful Drop-down Menu, with several options, and accommodate it in a small space on the form.
Likewise, in an earlier session, we learned how to Add new Nodes in a specific location in the node hierarchy or Delete a Node and add a new one to relocate a Node on the Tree View Control.
This method requests the creation of a new record in the source table for the new node. Or delete an existing record and create a new one to move an existing node to make them permanent. In a way, with the use of Add/Delete Functions, we could add new Nodes or re-arrange the existing Nodes on the TreeView Control. As far as re-arranging Nodes are concerned, we have a better way to do it, rather than deleting Nodes and re-creating them. Drag the Node from its current location and Drop it where we want it to be on the TreeView Control. This is what we are going to learn in this episode
This simple approach needs only updating the change of the ParentID field value of related records, to make the change permanent.
The Topics Covered so far in Earlier Sessions.
- Microsoft TreeView Control Tutorial
- Creating Access Menu with TreeView Control
- Assigning Images to TreeView Control
- Assigning Images to TreeView Control-2
- TreeView Control Check-Mark Add, Delete Nodes
- TreeView ImageCombo Drop-down Access Menu
But, we may face some challenges while using this method and we will come to that little later in this Session.
Demo Data Table and Form.
We need a Table and a Form. We already have a suitable table with the name Sample created in an earlier tutorial Session. If you have already downloaded the Demo Database, from the second link Page given above earlier, then you may use that Database for this Session also. We will use the following objects from that Database for our Drag-Drop experiments:
- Table: Sample
- Form: frmSample
The TreeView Control Image on frmSample with demo data is given below for reference:
You may download the Demo Database (ProjectMenu.zip) from the Second Link Page given above and extract the ProjectMenu.accdb database.
New Form for Drag Drop Trial Runs.
Open the ProjectMenu.accdb database.
Make a copy of the table Sample and name it Sample_bk, to keep the original data safe for later use. When we experiment with the drag-drop method, it is necessary to update the ParentId field value on the Sample demo Table. But, we need the original data later, without these changes.
Create a new Form with the name frmDragDrop.
-
The frmDragDrop Form Design will look like the image given below when you finish with it.
Insert the TreeView Control from the ActiveX Controls’ List and place it on the form, leaving enough space above the control, so that we can create two Command Buttons and a Heading Label above it. Drag the sizing handle at the bottom right corner to make it large enough to display all the Nodes, without scrolling.
Change the Name Property value of TreeView Control to TreeView0.
Insert a Command Button above and left edge of the TreeView Control. Change its Name Property Value to cmdExpand and Caption value to Expand All.
- Insert a second Command Button above and to the right edge of the TreeView Control. Change its Name Property Value to cmdCollapse and the Caption Property Value to Collapse All.
Insert a Label Control above the Command Buttons, wide enough to write the Heading as shown above, and change its Font Size 14.
Ignore the ImageList Control, for the time being, I have commented out the Code Lines that modify the Node ImageList index numbers. Later on, you can Import the ImageList Control with manually uploaded Images, from our earlier Tutorial Demo Database (from the 4th Link Page given above) and use it to display Node-Images on Nodes. When Node positions are changed during Drag Drop actions we need to change Node Images also depending on the Node’s position (Root-level Node or Child-Node) on the TreeView Control.
Drag-Drop Form Module Code.
Display the VBA Code Module of the Form frmDragDrop, Copy and Paste the following VBA Code (this is only the first half of the Form Module Code) into the frmDragDrop Form’s Class Module and Save the Form:
Option Compare Database Option Explicit Dim tv As MSComctlLib.TreeView Dim db As DAO.Database Dim rst As DAO.Recordset Dim imgListObj As MSComctlLib.ImageList Const KeyPrfx As String = "X" Private Sub Form_Open(Cancel As Integer) Set tv = Me.TreeView0.Object 'Set imgListObj = Me.ImageList1.Object 'tv.ImageList = imgListObj LoadTreeView End Sub Sub LoadTreeView() Dim strKey As String Dim strPKey As String Dim strText As String Dim strsQL As String strsQL = "SELECT * FROM Sample ORDER BY ID" Set db = CurrentDb Set rst = db.OpenRecordset(strsQL, dbOpenDynaset) tv.Nodes.Clear 'Add all Items are added as Root Nodes Do While Not rst.BOF And Not rst.EOF strKey = KeyPrfx & CStr(rst!ID) strText = rst!desc tv.Nodes.Add , , strKey, strText 'With tv.Nodes.Item(strKey) ' .Image = 1 ' .SelectedImage = 4 'End With rst.MoveNext Loop 'Prepare to update the Parent-Key of Nodes 'wherever applicable to move and position the Child Nodes strPKey = "" rst.MoveFirst Do While Not rst.EOF strPKey = Nz(rst!parentid, "") If Len(strPKey) > 0 Then strPKey = KeyPrfx & strPKey strKey = KeyPrfx & CStr(rst!ID) strText = rst!desc 'Move the Child Node under it's Parent-Node Set tv.Nodes.Item(strKey).Parent = tv.Nodes.Item(strPKey) 'Update Image and SelectedImage Properties 'with ImageList Index numbers 'With tv.Nodes.Item(strKey) ' .Image = 2 ' .SelectedImage = 3 'End With End If rst.MoveNext Loop rst.Close Set rst = Nothing Set db = Nothing End Sub Private Sub TreeView0_NodeClick(ByVal Node As Object) Dim SelectionNode As MSComctlLib.Node 'Ensure that the clicked node equals the selected node in the tree If Not Node Is Nothing Then Set SelectionNode = Node If SelectionNode.Expanded = True Then SelectionNode.Expanded = False Else SelectionNode.Expanded = True End If End If End Sub Private Sub cmdCollapse_Click() Dim tmpnod As MSComctlLib.Node For Each tmpnod In tv.Nodes If tmpnod.Expanded = True Then tmpnod.Expanded = False End If Next End Sub Private Sub cmdExpand_Click() Dim tmpnod As MSComctlLib.Node For Each tmpnod In tv.Nodes If tmpnod.Expanded = False Then tmpnod.Expanded = True End If Next End Sub
I know you are familiar with the above Code, if you have already gone through the earlier Episodes, except for the LoadTreeView() subroutine with some changes. Here, populating the TreeView Nodes has been divided into a two-step process.
Briefly, this is what happens inside this Subroutine.
All the records on the Sample Table have been loaded as Root-level Nodes of TreeView Control, with an ID field value as Key, in the first step.
Again, these records have been read a second time and checked for a value in the ParentId field, if empty then the Node will be retained as Root-level Node.
If the ParentID field has a Value then identify the Node with the ParentID value as Node-Key and move the current Node as its Child Node, or its [Relative] Parameter (of Add () Method) value gets updated.
Even though it appears that the two-step Node populating procedure is an unnecessary exercise, there is a good reason why we have to follow this method. We will come back to this little later and you will know it without much explaining.
On the Design of the Form, I have given an ImageList Control. You may insert the ImageList ActiveX Control and upload some images in it manually from the disk, or copy and paste this control with images from earlier Demo Database Downloads. In either case, please ensure that the name of the ImageList control is ImageList1. Otherwise, you need to change the name in the Code.
After that, enable the commented-out lines in Form_Open() Event Procedure. Enable the following lines, by removing the comment symbol from the beginning of the line:
'Set imgListObj = Me.ImageList1.Object 'tv.ImageList = imgListObj
The TreeView0_OLEDragDrop() Subroutine (in the 2nd part of the VBA Code) enables the Image Index Parameters of Nodes, by removing the Comment symbols from those lines too. With these changes, the Node Images will appear on the TreeView Control. If you have an ImageList Control of your own with uploaded Images then change the Index Numbers based on what Image you would like to insert on the Nodes.
The TreeView0_NodeClick() Event Procedure Expands the current node, if the Child-Nodes are in a collapsed state, otherwise, child Nodes will be Collapsed. Normally this action is controlled (without Code) by clicking on the +/- Symbol on the Tree-line of the Node with Child Nodes.
The Subroutines cmdExpand_Click() and cmdCollapse_Click() Events Expands all Nodes and collapse all Nodes respectively.
When the above Code Runs the display looks like the Form View Image given below:
You may save the frmDragDrop Form and open it in Normal View. If everything went on well then you will see the above screen. Try out the Expand All and Collapse All Command Buttons and check whether they are working too. If not, then re-check whether the following settings are correct or not:
i) The TreeView Control’s Name is: TreeView0
ii) Display the Property Sheet of Exampand All Command Button and select [Event Procedure] in the On Click Event Property.
iii) Ensure that the same setting is intact for Collapse All Command Button also.
iv) Click on a Node, having Child Nodes, to see whether they collapse or expand on repeated clicks.
v) If ImageList Control is placed on the Form then its name must be ImageList1.
Let us proceed with the second part of the VBA Code that implements the Drag-Drop Events.
Second Half of the VBA Code.
Copy the following Second Part of the VBA Code, on the frmDragDrop Form Module, that implements the Drag-Drop action, and Paste it below the existing Code:
Private Sub TreeView0_OLEStartDrag(Data As Object, AllowedEffects As Long) Set Me.TreeView0.SelectedItem = Nothing End Sub Private Sub TreeView0_OLEDragOver(Data As Object, _ Effect As Long, _ Button As Integer, _ Shift As Integer, _ x As Single, _ y As Single, _ State As Integer) Dim SelectedNode As MSComctlLib.Node Dim nodOver As MSComctlLib.Node If tv.SelectedItem Is Nothing Then 'Select a node if one is not selected Set SelectedNode = tv.HitTest(x, y) If Not SelectedNode Is Nothing Then SelectedNode.Selected = True End If Else If tv.HitTest(x, y) Is Nothing Then 'do nothing Else 'Highlight the node the mouse is over Set nodOver = tv.HitTest(x, y) Set tv.DropHighlight = nodOver End If End If End Sub Private Sub TreeView0_OLEDragDrop(Data As Object, _ Effect As Long, _ Button As Integer, _ Shift As Integer, _ x As Single, _ y As Single) Dim sourceNode As MSComctlLib.Node Dim SourceParentNode As MSComctlLib.Node Dim targetNode As MSComctlLib.Node Dim tmpRootNode As MSComctlLib.Node Dim strtmpNodKey As String Dim ChildNode As MSComctlLib.Node Dim strSPKey As String Dim strTargetKey As String Dim strsQL As String Dim intKey As Integer Dim intPKey As Integer On Error Resume Next Select Case Screen.ActiveControl.Name Case TreeView0.Name Set sourceNode = tv.SelectedItem End Select 'Get Source Parent Node & Target Node Reference Set SourceParentNode = sourceNode.Parent Set targetNode = tv.HitTest(x, y) 'If any errors then exit If Err <> 0 Then MsgBox Err & " : " & Err.Description, vbInformation + vbCritical, "OLEDragDrop()" Err.Clear Exit Sub Else On Error GoTo 0 End If 'Get/define Source parent Node Key to compare it with Target Node Key If SourceParentNode Is Nothing Then strSPKey = "Empty" Else strSPKey = SourceParentNode.Key End If 'Check the Target Node/Location and define the Key Select Case True Case targetNode Is Nothing strTargetKey = "Empty" Case targetNode.Key = "" strTargetKey = "Empty" Set targetNode = Nothing Case Else strTargetKey = targetNode.Key End Select 'Make sure the Target Node is not the source Node's own parent If strTargetKey = strSPKey Then Exit Sub 'Track User's Node move action, check for error. On Error Resume Next If targetNode Is Nothing Then 'If target Node is Nothing (the Node dropped in the empty area), 'then the Node must be moved to the Root-level 'save the original sourceNode.Key strtmpNodKey = sourceNode.Key 'Modify the source Node Key, with addition of some text, say 'Empty', like 'X5Empty' 'So that a temporary Node can be created with the original source Node key. 'Note: Two Nodes with the same Key cannot remain in memory at the same time. 'The Source Node with key 'X5Empty' deleted later, 'temporary Node takes it's droped location. sourceNode.Key = sourceNode.Key & strTargetKey 'Create the temporary Root Node, with original sourceNode Key Set tmpRootNode = tv.Nodes.Add(, , strtmpNodKey, sourceNode.Text) 'define the Root Node image indexes 'With tmpRootNode ' .Image = 1 ' .SelectedImage = 4 'End With 'Move all child Nodes from SourceNode,if any, 'as tmpRootNode's Children Do Until sourceNode.Children = 0 Set sourceNode.Child.Parent = tmpRootNode 'modify Node image indexes 'With sourceNode ' .Image = 2 ' .SelectedImage = 3 'End With Loop 'Delete the Source Node with modified Key from TreeView tv.Nodes.Remove sourceNode.Index 'Move the tmpRootNode with original Key 'to the dropped location on TreeView Set sourceNode = tmpRootNode Else 'Move the sourceNode under targetNode as child Set sourceNode.Parent = targetNode 'modify Node image indexes 'With sourceNode ' .Image = 2 ' .SelectedImage = 3 'End With End If 'Notify, if there was an Error then Exit, else Update PrentID of related Record. If Err <> 0 Then MsgBox Err & " : " & "Unable to move:" & vbCrLf & Err.Description, vbInformation + vbCritical, "DragDrop2()" Exit Sub Else 'Build and execute the SQL statement to update the record If targetNode Is Nothing Then intKey = Val(Mid(sourceNode.Key, 2)) strsQL = "UPDATE Sample SET ParentID = Null" & _ " WHERE ID = " & intKey Else intKey = Val(Mid(sourceNode.Key, 2)) intPKey = Val(Mid(targetNode.Key, 2)) strsQL = "UPDATE sample SET ParentID = " & intPKey & _ " WHERE ID = " & intKey End If 'Modify the table records CurrentDb.Execute strsQL, dbFailOnError 'If an error raised then refresh TreeView and exit If Err <> 0 Then MsgBox Err & " : " & Err.Description LoadTreeView 'Refresh/display TreeView without changes Else 'Sort Nodes If sourceNode.Parent Is Nothing Then sourceNode.Root.Sorted = True Else sourceNode.Parent.Sorted = True End If tv.Nodes(sourceNode.Key).Selected = True End If End If On Error GoTo 0 End Sub Private Sub TreeView0_OLECompleteDrag(Effect As Long) 'Turn off the drophighlight Set tv.DropHighlight = Nothing End Sub Private Sub Form_Close() Set tv = Nothing End Sub
For the Drag-Drop action, there are four subroutines, they have executed automatically when you Drag the Node(s), highlights the Node when moved over other Nodes, and finally, drop it on a different Node or on the Root-level empty area.
The Main Subroutines of the Code.
- TreeView0_OLEStartDrag() - Initializes the selected item and sets the Node to Nothing
- TreeView0_OLEDragOver() - Works like the Mouse Move Event, highlights the Node, when drags a Node above it, on the way to the Target Node.
- TreeView0_OLEDragDrop() – Performs check and controls, position the Node(s) in the Dropped Location, and updates the record on Base Table.
- TreeView0_OLECompleteDrag() - The DropHighlight Property is set to Nothing.
We can do the Drag and Drop works with the TreeView0_OLEDragDrop() Subroutine alone. In that case, there will not be any Node-highlight, when the Source Node moves over other Nodes, from one location to the other, except that the Mouse Pointer changes to dragging a second arrow behind it, like in the sample image given below:
So, we will pay attention to this subroutine and check the Code in detail from the beginning. At the beginning of the subroutine, we have declared the necessary Nodes String Variables, and others.
Instead of repeating line-by-line analyses here, each line/section of code carries a comment line explaining what it does, so that you will understand what is going on when you go through the Code. You may go through them.
The Sequence of Drap Drop Events
Let us understand the sequence of Events, the User Selects a Node, Drags over other Nodes on the way to its final destination, and Drops it on the Target Node. Or Drop it on the empty area on the TreeView Control, to make it a Root-level Node.
When you drag a Node over another Node-Text the Node-Text gets highlighted, saying that your current position is on this Node on the way, wherever you are going from here. When moved out of the Node-text the highlight disappears. This happens all through to the Target Node. The TreeView0_OLEDragOver() Subroutine takes care of this highlighting action.
When you Drop a Node somewhere the TreeView0_OLEDragDrop() Subroutine takes overcharge. Here, we have to analyze the User’s intentions and take appropriate action. The following information must be saved and analyzed to move the Node to the correct location.
The Important Information to Keep Track Of.
The Source Node Reference, Node Key and ParentID Values, Node Children, if any.
The Target Node or location Reference, Node Key.
If the Target is not a Node but the empty area of the TreeView Control, then the Source Node is to be moved into the root-level position.
The Source Node when Dropped on another Node, Target Node becomes the new Parent of the Source Node.
If Source Node has its own children, then they also must be moved with their Parents.
** When the Node is dragged and dropped on its own Parent-Node then Ignore this action.
** For example, check the above image. If we drag the TextBox Node and drop it to its parent-node Controls, or drag the Controls Node and drop it on its Parent Node Form then those moves will be ignored.
-
** If Root-level Node is dragged and dropped into the empty area, then no action will be taken because it is already a Root-level Node.
For all valid moves of Node, we need to update the ParentID field value of the related record on the Sample Table.
Node Drop in the Root-level Empty Area.
In the case of item number 3 above, we have to create a Root level Node, with the same ID Number of the Source Node, which is not allowed. Duplicate Key Value is not allowed in the TreeView hierarchy. This is the only area of the Code, where you will find little confusion in the procedure followed there.
The procedure is as given below:
Modify the existing TreeView Node Key with the addition of some extra text, (say Key X5 change to X5Empty), to avoid Key conflicts, while creating a temporary Node with the original Key.
Create a temporary Node with the Original Key: X5.
Move all Children Nodes from the Source Node, if any, as Child Nodes to the Temporary Node.
Delete the TreeView Source Node with the modified Key: X5Empty from the TreeView Control, but the related record in the Sample table is not touched.
Move the temporary Node with the Original Key X5 with its children to the Root-level position of the TreeView Control.
Update the ParentID field of the related record with a zero-length string (“”) to mark it as a Root-level Node.
Self Experiments of Drag Drop.
You may try out some Drag and Drop experiments yourself and see how it works. Select a Node, click and hold the left mouse button, drag the Node, and drop it on another Node, or drop it in an empty area of the TreeView Control. When you drag the Node over other Node-Text it is highlighted and when you are out of the Node the highlight goes out. The dragged Node will appear in the new location where you have dropped it. You may repeat this drag-drop experiment by selecting a single Node or Node with Children.
Based on this movement of Nodes the related record’s ParentID field value will be updated with the Key value (ID) of the Target Node related record.
Why Two-Step Node Populating Procedure?
Now, we will go back to the LoadTreeView() Subroutine, to take a second look at the two-step process we have adopted for populating all Nodes to the TreeView Control.
All records in the Sample Table are initially added as Root-level Nodes, using the ID field value as Node-Key.
In the second pass of the records, if the ParentID field value is empty, then that Node will remain as Root-level Node, without change.
All other Nodes-related records with ParentID Value are correctly moved under its parent Node.
Naturally, the question comes up, why do we have to do it this way?
We will do a simple experiment to make the answer clear without explaining it in too many words. You might have already done some trial runs of drag and drop trial runs yourself and rearranged Nodes, in the process updated those record’s ParentID values with the change. So, we need to reset the record values to their original state in the Sample Table, before we start a new demo.
We have already created a copy of our Table Sample earlier, with the name Sample_bk as a backup. Delete the Sample Table and make a copy from Sample_bk with the original name: Sample.
Open the Table and view the records and their ParentID field Values. The sample Image of the Table is given below:
The ID Field Values are Auto Numbers and they are all in sequential order and all Id values are unique. The following simple Rule governs the addition of a Child Node to the TreeView Control.
The Simple Child Node Rule: The ParentID field Value (Parent-Key) in a record expects that a Parent Node already exists in the TreeView Control, with the same value as Node-Key (the ID).
Check the third record from the top, in the above Table Image. The ParentID field value is 2 and the ID of the current record is 3. In this case, the record with ID 2 will be added to the TreeView Control before we attempt to add the third record to the Node. Both records aren't necessarily next to each other. Check the record with ID number 21, its ParentID field value is 12, less than the current record ID value of 21.
In either case, when the program encounters the ParentID Value in a record it assumes that the record with the ID value equal to the ParentID was already added as a Node in the TreeView Control in the earlier cycle of populating the Nodes.
Justifying the two Step Procedure.
Let us try some Drag-Drop trial runs. But, before that, we have a Form with the name frmSample, which we used in the first Tutorial Session, and in that, we loaded all the TreeView Nodes in one go. Yes, we have followed the same method so far and we need some change from now on. But, before that let us open the old form and see how the Nodes appear on the Form.
Open the Form frmSample to see what the TreeView display looks like, with the Sample table records, loaded using the old rule.
If you are through viewing the TreeView Nodes then close the Form.
Now, Open the frmDragDrop Form. We are preparing to drag and drop a Node.
Select the Node with the Node-Text-Table, Click and hold the left mouse button, and drag and drop it on the Node, with the Node-Text Form.
The Table Node with its immediate Child-Node Fields and its Children Nodes are moved as Child Nodes under the Form Node.
Close the Form frmDragDrop and open it again. The Nodes will appear correctly, where you have dropped them, like in the image given below.
Now, close the Form frmDragDrop.
Open the Form frmSample to see how this change appears on this Form. You will be greeted with an Error message, Element Not Found with Error Number: 35601.
Select the Debug Command Button to go to the highlighted Code line, where the Error occurred.
Point the mouse on the nodKey Param of Add() method, it shows X3, point the mouse on the ParentKey parameter and it shows X7.
Looking at these two parameter values, we can assume that we are on the record with the ID value 3 and trying to designate this Node as a child Node, to another Node not yet populated into the TreeView Control, with ID value 7.
Press F5 Key to bring up the same dialog box again and Click the End Command Button to stop the Program and bring up the Form on the Database Window. Close the frmSample Form.
Open the Sample Table to view the arrangement of the ParentID Numbers, after our drag and drop action. The Records will look like the image given below and I have highlighted the record that triggered the Error with ParentID value 7 and showing its Parent record position.
Following the earlier Node populating normal procedure, we are at the third record position. Since, the records ParentID value 7, the Nod with ID value 7 must be present in the TreeView Control. The Node with ID Value 7 is not yet populated into the TreeView Control, but we are trying to reference the non-existent Node and this triggers an Error.
Even if you Sort the records in the ParentID field order the records' new arrangement will look like the image given below:
Now, another record’s Parent Node is not in the expected position.
Hence, in these circumstances, our two-step TreeView Nodes loading approach works for both normal and after-drag-drop actions as well.
In the first step populate all records as Root-level Nodes in the TreeView Control using the ID field value as Node-Key.
Now, all the Nodes of all records are available in the TreeView Control. It will be easy to move them around wherever we want. It doesn’t say any of the required Node doesn’t exist in the TreeView.
In the second pass on the same set of records, the records with empty ParentID field values are untouched and allowed to remain as Root-level Nodes. In other cases moves the Node, as Child-Node under its Parent Node, by updating the [Relative] Parameter of the Node with the following Statement:
Set tv.Nodes.Item(strKey).Parent = tv.Nodes.Item(strPKey)
This is what we do through the second pass on the same set of records. You may do it by resetting the Record Pointer to the first record, by executing rst.MoveFirst before the Do . . . Loop, EOF conditions, and rst.MoveNext to access each record as we normally do.
Second Step in Reverse Order.
Or you may do it in reverse order. After populating all records as Root-level Nodes the Record Pointer will be beyond the last record and on the EOF position. You may reset the record pointer to the last record, by executing rst.MoveLast before the Do . . . Loop BOF check, and execute rst.MovePrevious to access each record and move the Nodes correctly under its parent Node. But, the Nodes may load slightly differently in the placement order Nodes.
You may try this out yourself with the above-suggested change of Code and see the result.
Download Demo Database.
- MS-Access Class Module and VBA
- MS-Access VBA Class Object Arrays
- MS-Access Base Class and Derived Objects
- VBA Base Class and Derived Object-2
- Base Class and Derived Object Variants
- MS-Access Recordset and Class Module
- Access Class Module and Wrapper Classes
- Wrapper Class Functionality Transformation