Udostępnij za pośrednictwem


Shell Style Drag and Drop in .NET - Part 2

This is part of a 3 part series:

  1. Shell Style Drag and Drop in .NET (WPF and WinForms)
  2. Shell Style Drag and Drop in .NET - Part 2
  3. Shell Style Drag and Drop in .NET - Part 3

Introduction

Last week, in Shell Style Drag and Drop in .NET (WPF and WinForms), I looked at hooking up the COM interfaces necessary to implement drag images using the Windows Shell. This week, I'm going to introduce some .NET 3.5 extensions (which can be easily converted into .NET 3.0 and earlier static helper functions) that reduce the code overhead of implementing great drag and drop preview images in your .NET applications.

NOTE: Like last week, this is not an in depth overview of drag and drop in .NET, but instead focuses on utilizing the Shell's drag image manager to develop more tightly integrated drag and drop applications.

Background

For those of you who didn't read last week's post (or found it too long), here is a recap of the important COM interfaces:

  • IDragSourceHelper - Exposed by the Shell to allow an application to specify the image that will be displayed during a Shell drag-and-drop operation.
  • IDropTargetHelper - Exposes methods that allow drop targets to display a drag image while the image is over the target window.
  • IDataObject - Specifies methods that enable data transfer and notification of changes in data (we only use it for the former).

I implemented IDataObject (the COM version, not System.Windows.IDataObject or System.Windows.Forms.IDataObject) and then imported the other interfaces, along with the DragDropHelper class, CoCreated using CLSID_DragDropHelper from the Shell.

The Extensions

Unlike last week, I won't take you step by step into the implementation of these extensions. But I will give you an overview of what I've added.

IDataObject Extensions

For each System.Windows.IDataObject and System.Windows.Forms.IDataObject, I added several extensions to simplify the initialization of the drag image. Since they are very similar, I'll use System.Windows.Forms as my example, and leave the WPF implementation to be explored by you.

Last week, I used code like this to start the drag and drop operation:

 

     void bt_MouseDown(object sender, MouseEventArgs e)
    {
        Bitmap bmp = new Bitmap(100, 100, PixelFormat.Format32bppArgb);
        using (Graphics g = Graphics.FromImage(bmp))
        {
            g.Clear(Color.Magenta);
            using (Pen pen = new Pen(Color.Blue, 4f))
            {
                g.DrawEllipse(pen, 20, 20, 60, 60);
            }
        }
        
        DataObject data = new DataObject(new DragDropLib.DataObject());
        
        ShDragImage shdi = new ShDragImage();
        Win32Size size;
        size.cx = bmp.Width;
        size.cy = bmp.Height;
        shdi.sizeDragImage = size;
        Point p = e.Location;
        Win32Point wpt;
        wpt.x = p.X;
        wpt.y = p.Y;
        shdi.ptOffset = wpt;
        shdi.hbmpDragImage = bmp.GetHbitmap();
        shdi.crColorKey = Color.Magenta.ToArgb();
        
        IDragSourceHelper sourceHelper = (IDragSourceHelper)new DragDropHelper();
        sourceHelper.InitializeFromBitmap(ref shdi, data);
        
        DoDragDrop(data, DragDropEffects.Copy);
    }

 

The overhead of creating and populating the ShDragImage structure can get tedious and dirties the code. Also, if you read the code last week, you may have noticed the usage of the DragDropHelper was always two lines, such as above. One line to create the object and cast to the interface, and one line to call into it. Granted, you could convert this into a one-liner, but it would be ugly, and still more typing than necessary. Because the InitializeFromBitmap method works on an existing IDataObject instance, I decided an extension was appropriate. Basically, the ShDragImage structure has two important pieces of information. The HBITMAP pointer, which indicates our drag image, and the offset of the cursor relative to the image. The size of the bitmap is certainly important, but that information can be derived from the Bitmap instance, so my extension ended up like this:

 

         /// <summary>
        /// Sets the drag image.
        /// </summary>
        /// <param name="dataObject">The DataObject to set the drag image on.</param>
        /// <param name="image">The drag image.</param>
        /// <param name="cursorOffset">The location of the cursor relative to the image.</param>
        public static void SetDragImage(this IDataObject dataObject, Image image, Point cursorOffset)
        {
            ShDragImage shdi = new ShDragImage();

            Win32Size size;
            size.cx = image.Width;
            size.cy = image.Height;
            shdi.sizeDragImage = size;

            Win32Point wpt;
            wpt.x = cursorOffset.X;
            wpt.y = cursorOffset.Y;
            shdi.ptOffset = wpt;

            shdi.crColorKey = Color.Magenta.ToArgb();

            // This HBITMAP will be managed by the DragDropHelper
            // as soon as we pass it to InitializeFromBitmap. If we fail
            // to make the hand off, we'll delete it to prevent a mem leak.
            IntPtr hbmp = GetHbitmapFromImage(image);
            shdi.hbmpDragImage = hbmp;

            try
            {
                IDragSourceHelper sourceHelper = (IDragSourceHelper)new DragDropHelper();

                try
                {
                    sourceHelper.InitializeFromBitmap(ref shdi, (ComIDataObject)dataObject);
                }
                catch (NotImplementedException ex)
                {
                    throw new Exception("A NotImplementedException was caught. "
                       + "This could be because you forgot to construct your DataObject "
                       + "using a DragDropLib.DataObject", ex);
                }
            }
            catch
            {
                DeleteObject(hbmp);
            }
        }

        /// <summary>
        /// Gets an HBITMAP from any image.
        /// </summary>
        /// <param name="image">The image to get an HBITMAP from.</param>
        /// <returns>An HBITMAP pointer.</returns>
        /// <remarks>
        /// The caller is responsible to call DeleteObject on the HBITMAP.
        /// </remarks>
        private static IntPtr GetHbitmapFromImage(Image image)
        {
            if (image is Bitmap)
            {
                return ((Bitmap)image).GetHbitmap();
            }
            else
            {
                Bitmap bmp = new Bitmap(image);
                return bmp.GetHbitmap();
            }
        }

 

You'll notice that the SetDragImage extension function simply takes an IDataObject (in this case it is the System.Windows.Forms.IDataObject), an Image, and a cursor offset. From these pieces of information, we can easily populate the ShDragImage struct and call the IDragSourceHelper.InitializeFromBitmap method.

With this extension, which I placed in the System.Windows.Forms namespace for convenience, the code to begin a drag and drop routine is much simpler:

 

     void bt_MouseDown(object sender, MouseEventArgs e)
    {
        Bitmap bmp = new Bitmap(100, 100, PixelFormat.Format32bppArgb);
        using (Graphics g = Graphics.FromImage(bmp))
        {
            g.Clear(Color.Magenta);
            using (Pen pen = new Pen(Color.Blue, 4f))
            {
                g.DrawEllipse(pen, 20, 20, 60, 60);
            }
        }
        
        DataObject data = new DataObject(new DragDropLib.DataObject());
        data.SetDragImage(bmp, e.Location);
        
        DoDragDrop(data, DragDropEffects.Copy);
    }

 

Ignoring the creation of the Bitmap, I can now start drag and drop using a drag image in 3 lines of code.

IDropTargetHelper Extensions

Much like IDataObject, I can also extend IDropTargetHelper. However, now, instead of extending System.Windows.IDataObject and System.Windows.Forms.IDataObject separately, we extend IDropTargetHelper with methods for both WPF and WinForms. Again, I'll work with the WinForms code, but the WPF extensions are very similar.

Last week, we used code that handled the drag and drop events like this:

 

     protected override void OnDragEnter(DragEventArgs e)
    {
        e.Effect = DragDropEffects.Copy;
        Point p = Cursor.Position;
        Win32Point wp;
        wp.x = p.X;
        wp.y = p.Y;
        IDropTargetHelper dropHelper = (IDropTargetHelper)new DragDropHelper();
        dropHelper.DragEnter(IntPtr.Zero, (ComIDataObject)e.Data, ref wp, (int)e.Effect);
    }

 

The code is simple enough, but again, it gets tedious to declare the Win32Point struct and populate it. So, we can implement some extensions that use the native System.Windows.Forms and System.Drawing types:

         /// <summary>
        /// Notifies the DragDropHelper that the specified Control received
        /// a DragEnter event.
        /// </summary>
        /// <param name="dropHelper">The DragDropHelper instance to notify.</param>
        /// <param name="control">The Control the received the DragEnter event.</param>
        /// <param name="data">The DataObject containing a drag image.</param>
        /// <param name="cursorOffset">The current cursor's offset relative to the window.</param>
        /// <param name="effect">The accepted drag drop effect.</param>
        public static void DragEnter(this IDropTargetHelper dropHelper, Control control,
            IDataObject data, Point cursorOffset, DragDropEffects effect)
        {
            IntPtr controlHandle = IntPtr.Zero;
            if (control != null)
                controlHandle = control.Handle;
            Win32Point pt = SwfDragDropLibExtensions.ToWin32Point(cursorOffset);
            dropHelper.DragEnter(controlHandle, (ComIDataObject)data, ref pt, (int)effect);
        }

        /// <summary>
        /// Notifies the DragDropHelper that the current Control received
        /// a DragOver event.
        /// </summary>
        /// <param name="dropHelper">The DragDropHelper instance to notify.</param>
        /// <param name="cursorOffset">The current cursor's offset relative to the window.</param>
        /// <param name="effect">The accepted drag drop effect.</param>
        public static void DragOver(this IDropTargetHelper dropHelper, Point cursorOffset, DragDropEffects effect)
        {
            Win32Point pt = SwfDragDropLibExtensions.ToWin32Point(cursorOffset);
            dropHelper.DragOver(ref pt, (int)effect);
        }

        /// <summary>
        /// Notifies the DragDropHelper that the current Control received
        /// a Drop event.
        /// </summary>
        /// <param name="dropHelper">The DragDropHelper instance to notify.</param>
        /// <param name="data">The DataObject containing a drag image.</param>
        /// <param name="cursorOffset">The current cursor's offset relative to the window.</param>
        /// <param name="effect">The accepted drag drop effect.</param>
        public static void Drop(this IDropTargetHelper dropHelper, IDataObject data, Point cursorOffset, DragDropEffects effect)
        {
            Win32Point pt = SwfDragDropLibExtensions.ToWin32Point(cursorOffset);
            dropHelper.Drop((ComIDataObject)data, ref pt, (int)effect);
        }

On top of that, I wanted to add one more layer. Since I always need an instance of the DragDropHelper, why not wrap that into some static methods? What I ended up doing is creating a static class called DropTargetHelper. This class maintains a static instance of the DragDropHelper and exposes some static methods. I also put this class in the System.Windows.Forms namespace, since that is generally where it would be used. Note that these methods don't do anything except add a layer of abstraction around the creation of the DragDropHelper class:

 namespace System.Windows.Forms
{
    public static class DropTargetHelper
    {
        /// <summary>
        /// Internal instance of the DragDropHelper.
        /// </summary>
        private static IDropTargetHelper s_instance = (IDropTargetHelper)new DragDropHelper();

        static DropTargetHelper()
        {
        }

        /// <summary>
        /// Notifies the DragDropHelper that the specified Control received
        /// a DragEnter event.
        /// </summary>
        /// <param name="control">The Control the received the DragEnter event.</param>
        /// <param name="data">The DataObject containing a drag image.</param>
        /// <param name="cursorOffset">The current cursor's offset relative to the window.</param>
        /// <param name="effect">The accepted drag drop effect.</param>
        public static void DragEnter(Control control, IDataObject data, Point cursorOffset, DragDropEffects effect)
        {
            SwfDropTargetHelperExtensions.DragEnter(s_instance, control, data, cursorOffset, effect);
        }

        /// <summary>
        /// Notifies the DragDropHelper that the current Control received
        /// a DragOver event.
        /// </summary>
        /// <param name="cursorOffset">The current cursor's offset relative to the window.</param>
        /// <param name="effect">The accepted drag drop effect.</param>
        public static void DragOver(Point cursorOffset, DragDropEffects effect)
        {
            SwfDropTargetHelperExtensions.DragOver(s_instance, cursorOffset, effect);
        }

        /// <summary>
        /// Notifies the DragDropHelper that the current Control received
        /// a DragLeave event.
        /// </summary>
        public static void DragLeave()
        {
            s_instance.DragLeave();
        }

        /// <summary>
        /// Notifies the DragDropHelper that the current Control received
        /// a DragOver event.
        /// </summary>
        /// <param name="data">The DataObject containing a drag image.</param>
        /// <param name="cursorOffset">The current cursor's offset relative to the window.</param>
        /// <param name="effect">The accepted drag drop effect.</param>
        public static void Drop(IDataObject data, Point cursorOffset, DragDropEffects effect)
        {
            SwfDropTargetHelperExtensions.Drop(s_instance, data, cursorOffset, effect);
        }

        /// <summary>
        /// Tells the DragDropHelper to show or hide the drag image.
        /// </summary>
        /// <param name="show">True to show the image. False to hide it.</param>
        public static void Show(bool show)
        {
            s_instance.Show(show);
        }
    }
}

NOTE: You may notice I'm calling my extension methods as static methods. This is because my code can be easily converted to work with .NET 3.0 or before by removing (or compiling out by way of preprocessor directives) the "this" keyword in front of the first parameter of the extension methods. I was going to include source that was compatible with .NET 3.0 but decided against it last minute. Anyway, the extensions, whether called as a static method or as an extension method, will do the same thing.

Now my drag event handlers can be simplified to something like this:

 

     protected override void OnDragEnter(DragEventArgs e)
    {
        e.Effect = DragDropEffects.Copy;
        DropTargetHelper.DragEnter(this, e.Data, Cursor.Position, e.Effect);
    }

 

Much simpler, and much less tedious to implement across 50 Forms in some enterprise application.

Conclusion

Drag and drop is a powerful tool for the end user experience. With a little work, we can implement a set of drag and drop helpers that keep our code tidy and add a great user experience for a minimal cost.

Source Code

The sources are available here:

File Description
View DragDropLib.cs online A complete compilation of all the source set into a single CS file so you can drop it into your project(s). Includes WPF and SWF implementations. No samples.
A complete compilation of all the source set into a single CS file so you can drop it into your project(s). Includes WPF and SWF implementations. No samples.
A complete set of projects. Includes a complete DragDropLib project with WPF and SWF implementations, plus separated implementations. Includes sample projects.
What's Next?

Implementing the fundamentals is all good, but nothing feels better than putting it to use. In my next post, we'll look at an example of really putting this technology to use in the end user experience.

Comments

  • Anonymous
    February 10, 2009
    How do I pass my own custom data? I'm developing a WPF application and if I try to set data to the DataObject I get: Cannot SetData on a frozen OLE data object. For example I have tried this within your example solution: private void rect_MouseDown(object sender, MouseButtonEventArgs e) { Rectangle rect = sender as Rectangle; DataObject data = new DataObject(new DragDropLib.DataObject()); data.SetDragImage(rect, e.GetPosition(rect)); data.SetData("mydata", "myobject"); DragDrop.DoDragDrop(rect, data, DragDropEffects.Copy); } That does not work but since it's using the DataObject from the DragDropLib internally. But how do I set data to my drag and drop? I mean the preview is great but if I can not pass internal application data within the drag and drop it's kind of limited. I could store it somewere else and retreive it but that seems way too fishy... Any advice is appreciated.

  • Anonymous
    February 18, 2009
    Karin, try using the SetDataEx methods provided by WpfDataObjectExtensions. Because we are providing a COM implementation of IDataObject, we can't use the SetData functions provided by the System.Windows.DataObject. Look through WpfDragSourceHelper or read Part 3 of these articles for the full explanation.