Thursday, May 12, 2016

Drag and Drop

Drag and Drop is one of the fundamental building blocks of a modern GUI. It is so fundamental it is easy to overlook the potential for enhancement. There are lots of code fragments about if you look for enhancing the basics but most explanations seem a bit partial – hence this tutorial that I hope covers the wider ground.

Starting with the basic default process and a simple (but perhaps unlikely) example.

Consider a Label with some text that you might want to drag into a text box to edit. The TextBox has the AllowDrop attribute set to true. The drag process starts with a mouse down event on the label so we need some code to start the DragDrop process.

private void label1_MouseDown(object sender, MouseEventArgs e) {     DoDragDrop(label1.Text, DragDropEffects.Copy); }

The DoDragDrop method has been passed the label Text property as the data and the DragDropEffects value of Copy set to indicate the intention of the process. The main values are Copy, Link and Move with additional values of Scroll, None and All (multiple values can be combined with the Or operator) . The data can be supplied as an object and in this instance is a string.

Our target Textbox needs to handle two events, DragEnter and DragDrop.

private void textBox1_DragEnter(object sender, DragEventArgs e) {     if (e.Data.GetDataPresent(DataFormats.Text))     {         e.Effect = DragDropEffects.Copy;     } } private void textBox1_DragDrop(object sender, DragEventArgs e) {     textBox1.Text += e.Data.GetData(DataFormats.Text); }

That’s all that is required for this basic example. The drag drop process gets the default visual feedback effects and a drop onto the TextBox copies and adds the Label Text to the TextBox Text.

Now let us consider a more complex scenario. I might have multiple objects that could be dragged and dropped on the same page. It could also be true that things might be dragged with multiple potential purposes – I might be implicitly copying the content if I dropped it at one location but simply moving the content about within another screen area. The user needs feedback on the impact of a drop event at different locations – this makes a case for custom cursors. It is also important that the control receiving the “drop” knows the drag source as this might influence that drop action and certainly identifies the unique “content”.

As the DoDragDrop method accepts type “object” as the data source we could rewrite the code from above to use the Label (or any other) control itself:

private void label1_MouseDown(object sender, MouseEventArgs e) {     DoDragDrop(label1, DragDropEffects.Copy); } private void textBox1_DragEnter(object sender, DragEventArgs e) {     if (e.Data.GetDataPresent(label1.GetType()))     {         e.Effect = DragDropEffects.Copy;     } } private void textBox1_DragDrop(object sender, DragEventArgs e) {     textBox1.Text += ((dynamic)e.Data.GetData(label1.GetType())).Text; }

but this approach is somewhat limited as it would mean writing specific code for each individual data source.

The DataFormats class has a long list of predefined fields ranging from Bitmap through Text (as we have seen) and includes useful types like XAML. This list of DataFormats is the same list as is used by the Windows Copy/Paste functionality and it is perfectly straightforward to add additional types to that list.

The code below declares a new custom DataFormat and then uses that DataFormat identity to store an instance of a class in a DataObject and then passes that DataObject through the Drag and Drop process to the target control. The code can thus support multiple instances of said class generically.

private DataFormats.Format cardFormat = DataFormats.GetFormat("CCard"); private void label1_MouseDown(object sender, MouseEventArgs e) {     CCard mCard = new CCard(); // create a class instance     mCard.CardTitle = "New Card Title"; // set a property value     DataObject mDataObject = new DataObject(cardFormat.Name, mCard);     DoDragDrop(mDataObject, DragDropEffects.Copy); } private void textBox1_DragEnter(object sender, DragEventArgs e) {     if (e.Data.GetDataPresent(cardFormat.Name))     {         e.Effect = DragDropEffects.Copy;     } } private void textBox1_DragDrop(object sender, DragEventArgs e) {     CCard cCard = (CCard)e.Data.GetData(cardFormat.Name); // retrieve the class instance     textBox1.Text += cCard.CardTitle; // and use one or more proprty values }

In our more complex scenario, each data source type might be represented by a specific class and then each data source instance would simply supply an instance of the relevant class. The DragDrop targets can inspect the DataFormat to decide upon the relevant Effect for that DataFormat and the Drop event can process the given class instance where appropriate.

As that may not be the clearest explanation it would probably help to have some demo code. First though I want to look at custom Cursors.

My custom Cursor creation functions are encapsulated within a utility class

namespace Adit.Classes {     public struct IconInfo     {         public bool fIcon;         public int xHotspot;         public int yHotspot;         public IntPtr hbmMask;         public IntPtr hbmColor;     }     public static class CursorUtil     {         [DllImport("user32.dll")]         public static extern IntPtr CreateIconIndirect(ref IconInfo icon);         [DllImport("user32.dll")]         [return: MarshalAs(UnmanagedType.Bool)]         public static extern bool GetIconInfo(IntPtr hIcon, ref IconInfo pIconInfo);         [DllImport("gdi32.dll")]         public static extern bool DeleteObject(IntPtr handle);         [DllImport("user32.dll", CharSet = CharSet.Auto)]         extern static bool DestroyIcon(IntPtr handle);         public static Cursor CreateCursor(Bitmap bm, int xHotspot, int yHotspot, bool resize = true)         {             IntPtr ptr = (resize) ? ((Bitmap)ResizeBitmap(bm, 32, 32)).GetHicon() : bm.GetHicon();             IconInfo inf = new IconInfo();             GetIconInfo(ptr, ref inf);             inf.xHotspot = xHotspot;             inf.yHotspot = yHotspot;             inf.fIcon = false;             IntPtr cursorPtr = CreateIconIndirect(ref inf);             if (inf.hbmColor != IntPtr.Zero) { DeleteObject(inf.hbmColor); }             if (inf.hbmMask != IntPtr.Zero) { DeleteObject(inf.hbmMask); }             if (ptr != IntPtr.Zero) { DestroyIcon(ptr); }             Cursor c = new Cursor(cursorPtr);             c.Tag = (resize) ? new Size(32, 32) : bm.Size;             return c;         }         public static Bitmap ResizeBitmap(Image image, int maxWidth, int maxHeight)         {             double ratio = System.Math.Min((double)maxHeight / image.Height, (double)maxWidth / image.Width);             var propWidth = (int)(image.Width * ratio);             var propHeight = (int)(image.Height * ratio);             var newImage = new Bitmap(propWidth, propHeight);             using (var g = Graphics.FromImage(newImage))             {                 g.DrawImage(image, 0, 0, propWidth, propHeight);             }             return newImage;         }         public static Bitmap GetControlBitmap(Control c, Color transparent)         {             var bm = new Bitmap(c.Width, c.Height);             c.DrawToBitmap(bm, new Rectangle(0, 0, c.Width, c.Height));             if (transparent != null)             {                 bm.MakeTransparent(transparent);             }             return bm;         }         public static Bitmap OverlayBitmap(Bitmap baseBitmap, Bitmap overlay, Point atPosition)         {             using (var g = Graphics.FromImage(baseBitmap))             {                 g.DrawImage(overlay, new Rectangle(atPosition, overlay.Size));             }             return baseBitmap;         }     } }

I have used these functions to create cursors from 96x96 pixel .png files (some originally sourced from the Material Icons download page). I have also used the GetControlBitmap function to create a Cursor from a Bitmap displaying a copy of a control captured at run-time.

private Cursor[] cursors = new Cursor[4];         //later cursors[0] = CursorUtil.CreateCursor((Bitmap)imageList1.Images[2], 0, 0);         //and cursors[2] = CursorUtil.CreateCursor(CursorUtil.GetControlBitmap(label1, SystemColors.Control), 0, 0, false);

Important thing is, we can easily create custom Cursors from bitmaps ready to apply during our Drag and Drop.

Just one quick aside though, the cursor "hot-spot" does not have to be at position 0,0 as in the above examples. Think of (say) the Paint.NET flood-fill or Color-Picker cursors where the hot-spot is marked by a cross or is at the tip of the "eye dropper". You might want to use something like my OverlayBitmap() function to add a suitable symbol to a control image and set the hot-spot to the relevant position in the final image.

OK - one other aside. My cursor building function adds the image size to the Tag property. You might wonder why. If you create a custom cursor (say) from a control image you might be surprised to find that the resulting cursor Size attribute will always return 32,32 (or just possibly 64,64) and not the actual size of the cursor. You can access (say) the true Width of a cursor via the Tag property with code something like:

int cWidth = ((dynamic)myCursor.Tag).Width;

We can apply our custom cursors in a GiveFeedback event handler. so first we might create custom Cursors for Copy and None and then apply them in a function

private Cursor myCopyCursor, myNoneCursor;     myCopyCursor = CursorUtil.CreateCursor((Bitmap)imageList1.Images[2], 0, 0);     myNoneCursor = CursorUtil.CreateCursor((Bitmap)imageList1.Images[3], 0, 0); private void giveFeedback(object sender, GiveFeedbackEventArgs e) {     if(e.Effect == DragDropEffects.Copy)     {         e.UseDefaultCursors = false;         Cursor.Current = myCopyCursor;     }     else if(e.Effect == DragDropEffects.None)     {         e.UseDefaultCursors = false;         Cursor.Current = myNoneCursor;     } }
We add the giveFeedback() function to the GiveFeedback event of the Form - perhaps during the form Load()

this.GiveFeedback += giveFeedback;

and then slightly counter-intuitively set the AllowDrop property to true for the form and all controls over which you anticipate the user might drag. This does not allow a drop to occur at these sites (that requires code to handle DragEnter and DragDrop but it does allow any relevant custom Cursor to be displayed during any drag over the form and control surfaces.

A point worth noting is that DragDrop event arguments include X/Y coordinates and these are screen co-ordinates as drags are not restricted to the originating window. Drag/Drop is a key element of OLE (Object Linking and Embedding). This can make using those conveniently presented values slightly tricky when managing things like local scrolling under program control. Which brings me nicely to my demo application (zipped as a Visual Studio 2015 project ready to download) that manages just that.

First off, this is demo and not production code and thus likely to include bugs, ignore edge cases, certainly takes shortcuts, is unduly verbose in places and might totally fail to explain some key point you are interested in. However it might provide a platform that can be used to try out alternate scenarios and approaches without having to build the whole thing from scratch.


Here you can see a screen image from the demo program. Addresses can be entered in the mini form on the left of the window and dragged onto the FlowLayoutPanel on the right that is acting as an "address store". To save energy, the "Fill" button will populate the form with up to 50 addresses taken randomly from an embedded list.

Addresses in the right hand panel can be dragged back to the address entry form for editing. Addresses can also be dragged up and down the list to reorder them.

We thus have two sorts of object that can be dragged for (is it) three different purposes. Some simple custom icons are created and used during the drag events.

The demo features loading data from an embedded resource (.csv file) using the Visual Basic TextFieldParser class (don't panic this is C# demo code). There are two custom DataFormats declared and used in the Drag operations. The addresses in the notional "address store" are encapsulated within multiple UserControls and these controls use Delegates to communicate with the Form. Dragging an address within the FlowLayoutPanel will trigger an automatic scroll when the scroll bar is active and the drag nears the top or bottom of the panel..

Exercises for further study might include using an image of an AddressControl as a cursor and/or dragging new addresses to a specific location in the FlowLayoutPanel instead of just adding them at the bottom irrespective of the drop location.

An analysis of the code will note that the demo "cheats" when determining the target location of a move within the FlowLayoutPanel and that it is possible to dodge around one or more address and expose the bug.

Inspect or download the demo code at https://github.com/MikeGWem/DragDropDemo

If anyone knows or finds out what DragDropEffects.Scroll actually does then please let me know as so far the answer to that has eluded me.

Addendum:

The "bug fix" (to decide where a drop happens relative to the existing list of addresses in the "store" could be based upon a calculation performed at the same time as the test to see if the panel should be scrolled.

private void maybeScrollpanel(int dragY) {     if ((dragY - panelTop) <= flowLayoutPanel1.VerticalScroll.Value && flowLayoutPanel1.VerticalScroll.Value > 0)     {         flowLayoutPanel1.VerticalScroll.Value -= (flowLayoutPanel1.VerticalScroll.Value > scrollAt) ? scrollAt : flowLayoutPanel1.VerticalScroll.Value;     }     else if (dragY - panelTop >= flowLayoutPanel1.Height - scrollAt && flowLayoutPanel1.VerticalScroll.Maximum > 0)     {         flowLayoutPanel1.VerticalScroll.Value += (flowLayoutPanel1.VerticalScroll.Maximum - flowLayoutPanel1.VerticalScroll.Value > scrollAt) ? scrollAt : flowLayoutPanel1.VerticalScroll.Maximum - flowLayoutPanel1.VerticalScroll.Value;     }     calculatedPosition = calculatePosition(dragY - panelTop); } private int calculatePosition(int dragY) {     if (flowLayoutPanel1.Controls.Count == 1 || dragY < flowLayoutPanel1.Controls[1].Height) // || allows lazy evaluation     {          return 1;     }     Point pt = new Point(flowLayoutPanel1.Width / 2, dragY); // asumes single column of controls in panel     Control cAt = flowLayoutPanel1.GetChildAtPoint(pt);     while (cAt == null && pt.Y < (flowLayoutPanel1.Height + flowLayoutPanel1.VerticalScroll.Maximum))     {         pt.Y += 5; // arbitrary - adjust at will         cAt = flowLayoutPanel1.GetChildAtPoint(pt);     }     return (cAt != null) ? ((AddressControl)cAt).Sequence : (flowLayoutPanel1.Controls.OfType<AddressControl>().Max(a => a.Sequence) + 1); }
Then applied to the drop events like

private void flowLayoutPanel1_DragDrop(object sender, DragEventArgs e) {     // the two paths could share more of the code but...     if (e.Data.GetDataPresent(dragAddress.Name))     {         //handles drop of new address onto the panel         AddressControl newControl = new AddressControl((DragAddress)e.Data.GetData(dragAddress.Name), DoControlDrag, DraggedPast, flowLayoutPanel1.Controls.Count);         foreach(AddressControl ac in flowLayoutPanel1.Controls.OfType<AddressControl>().Where(a => a.Sequence >= calculatedPosition))         {             ac.Sequence++;         }         newControl.Sequence = calculatedPosition;         flowLayoutPanel1.Controls.Add(newControl);         reorderAddresses();         resetPnlEdit();     } else if (e.Data.GetDataPresent(addressControl.Name))     {         // handles drop of an existing AddressControl into (presumably) a new position         int newSeq = 1;         foreach (AddressControl ac in flowLayoutPanel1.Controls.OfType<AddressControl>().OrderBy(a => a.Sequence))         {             if(ac.Sequence == dragItemSequence)             {                 ac.Sequence = calculatedPosition;             } else if(ac.Sequence == calculatedPosition)             {                 ac.Sequence++;                 newSeq = ac.Sequence;             } else             {                 ac.Sequence = newSeq;             }             newSeq++;         }         newSeq = 1;         // is redoing the sequence to avoid breaks a bit too anal?         foreach (AddressControl ac in flowLayoutPanel1.Controls.OfType<AddressControl>().OrderBy(a => a.Sequence))         {             ac.Sequence = newSeq;             newSeq++;         }         reorderAddresses();     } }
For the rest of the code check out GitHub at https://github.com/MikeGWem/DragDropDemo

End Note:
None of this looks like it will work with UWP and .NET Core so I will try and write up a few notes on that once I have something working that looks usable for others.

No comments: