Monday, July 6, 2009

Allow Users to Drag & Drop Controls

Many times your users want to view their data in a particular way, but most people think it is too difficult to allow them to customize or don't even think it is possible. This reasoning may come from the plethora of poor information and general ignorance found when searching for such capabilities. Most methods are ridiculously difficult, overly vebose, or a pain to implement, and as usual there is a fairly simple way to this.
First, build a form and drop on 2 textboxes and 2 buttons. Next we will build a few events. Go into the code and from the left dropdown select Button1, and from the second choose MouseDown, this creates an event for us. Do the same for MouseMove and MouseUp, both for the Button1 control only.
Rename the subs for ease of use to startDrag (MouseDown), whileDragging (MouseMove), and endDrag (MouseUp). This allows us to more easily see what we're doing, and lets .NET build the required events.
The handles will be removed later, but you need each to say Handles Button1.'event' because we want to test as we build to demonstrate.
Next, just under the Class, we need to create a few variables.

Dim dragging As Boolean
Dim startX As Integer
Dim startY As Integer

You will see what these do shortly. First, in the startDrag, set dragging to True and set the x and y coordinates:

dragging = True
startX = e.X
startY = e.Y

startX and startY are the x and y location of where the click occurred and will be used in the movement calculation, which in the whileDragging sub is:

If dragging = True Then
sender.Location = New Point(sender.Location.X + e.X - startX, sender.Location.Y + e.Y - startY)
End If
Me.Refresh()

The refresh simply forces a refresh of the screen when we're done, the work happens on the line above where as we move and get a new location for the mouse, we add the current x and y to the current x and y and subtract the initial number to get our current coordinates. If you don't know what sender means, you will when i do the post on it :) for now, just know that when you want to use the same sub for multiple controls, sender will pass in the name of the control causing the event. In this situation that sender is Button1, though that will change soon enough.
Finally on the stopDrag turn of dragging:

dragging = False

Run your program and drag Button1 around to check it out. Now you might see an issue when dragging a button, that being that once you click it and unclick it, you have fired of the buttons click event. Luckily, the buttons event is fired before the MouseUp event so you can handle the issue by placing this around the code:

If dragging = False Then
'code
End If

Next, lets set all controls to be movable. Know that you can make any control movable or not movable, and that you can easily turn on or off dragging so you can build a setup mode. I may go into that later if there is interest or I feel like it.
First, delete the Handles Button1.MouseUp etc from each of the three events. on the forms load event place this code:

For Each Control As Control In Me.Controls
AddHandler Control.MouseDown, AddressOf startDrag
AddHandler Control.MouseMove, AddressOf whileDragging
AddHandler Control.MouseUp, AddressOf endDrag
Next

This takes each control and assigns a sub to an event so that when Control.MouseDown is fired, startDrag is begun. Try out the program again and try moving bothe textboxes and both buttons. Thates it for the movement, now we have something thats even harder to find information on, and that is saving the location of the controls so when you close the program it won't reset.
First go into the Project->'project name' Properties and create a setting (more on seeting in another post). Click Settings on the left and create a new setting named controlLocations, setting the type to System.Collections.Specialized.StringCollection. Now a very important part to prevent work later, click the ellipses in the 'value' column and place a 0 in the editor and select ok, this creates the file that will store all of the location data. Now we need to populate the collection.
In the endDrag event place this:

My.Settings.controlLocations.Clear()
For Each Control As Control In Me.Controls
My.Settings.controlLocations.Add(Control.Name & "!" & Control.Location.X & "!" & Control.Location.Y)
Next
My.Settings.Save()

(After publishing I noticed that my vertical bar did not show so i used !, there is no significance other than it is a rarely used symbol we can use for delimiting text.)
That will store the x and y coordinates of all the controls, next we need to load that when the form opens, so on the form load event:

For Each Control As Control In Me.Controls
For Each item In My.Settings.controlLocations
If Split(item, "!")(0) = Control.Name Then
Control.Location = New Point(Split(item, "!")(1), Split(item, "!")(2))
End If
Next
Next

I had a flickering problem I thought I ought to bring up, if it occurs to you, make sure you have no handles on your events. I had put them back in while writing and forgot to remove them again.
There you go, we easily made a user defined form that allows for saving and loading of control locations. Enjoy.