views:

1285

answers:

5

I have created a small Windows Forms test application to try out some drag/drop code. The form consists of three PictureBoxes. My intention was to grab a picture from one PictureBox, display it as a custom cursor during the drag operation, then drop it on another PictureBox target.

This works fine from one PictureBox to another as long as they are on the same form.

If I open two instances of the same application and attempt to drag/drop between them, I get the following cryptic error:

This remoting proxy has no channel sink which means either the server has no registered server channels that are listening, or this application has no suitable client channel to talk to the server.

For some reason, however, it does work to drag/drop to Wordpad (but not MS Word or Paintbrush).

The three PictureBoxes get their events hooked up like this:

foreach (Control pbx in this.Controls) {
    if (pbx is PictureBox) {
     pbx.AllowDrop = true;
     pbx.MouseDown    += new MouseEventHandler(pictureBox_MouseDown);
     pbx.GiveFeedback += new GiveFeedbackEventHandler(pictureBox_GiveFeedback);
     pbx.DragEnter    += new DragEventHandler(pictureBox_DragEnter);
     pbx.DragDrop     += new DragEventHandler(pictureBox_DragDrop);
    }
}

Then there are the four events like this:

void pictureBox_MouseDown(object sender, MouseEventArgs e) {
    int width = (sender as PictureBox).Image.Width;
    int height = (sender as PictureBox).Image.Height;

    Bitmap bmp = new Bitmap(width, height);
    Graphics g = Graphics.FromImage(bmp);
    g.DrawImage((sender as PictureBox).Image, 0, 0, width, height);
    g.Dispose();
    cursorCreatedFromControlBitmap = CustomCursors.CreateFormCursor(bmp, transparencyType);
    bmp.Dispose();

    Cursor.Current = this.cursorCreatedFromControlBitmap;

    (sender as PictureBox).DoDragDrop((sender as PictureBox).Image, DragDropEffects.All);
}


void pictureBox_GiveFeedback(object sender, GiveFeedbackEventArgs gfea) {
    gfea.UseDefaultCursors = false;
}


void pictureBox_DragEnter(object sender, DragEventArgs dea) {
    if ((dea.KeyState & 32) == 32) { // ALT is pressed
     dea.Effect = DragDropEffects.Link;
    }
    else if ((dea.KeyState & 8) == 8) { // CTRL is pressed
     dea.Effect = DragDropEffects.Copy;
    }
    else if ((dea.KeyState & 4) == 4) { // SHIFT is pressed
     dea.Effect = DragDropEffects.None;
    }
    else {
     dea.Effect = DragDropEffects.Move;
    }
}


void pictureBox_DragDrop(object sender, DragEventArgs dea) {
    if (((IDataObject)dea.Data).GetDataPresent(DataFormats.Bitmap))
     (sender as PictureBox).Image = (Image)((IDataObject)dea.Data).GetData(DataFormats.Bitmap);
}

Any help would be greatly appreciated!

A: 

Just out of curiousity, in the DragDrop method, have you tried testing whether you can get the bitmap image out of the DragEventArgs at all? Without doing the sender cast? I'm wondering whether the picturebox object isn't serializable, which causes the issue when you try to use the sender in a different app domain...

genki
I tried creating a totally separate bitmap and used that instead. Same result. Possible to drag/drop internally, and not working with a separate application.To answer your question, yes, in the DragDrop event the image does come through as a bitmap. It's just that the application crashes with the aforementioned error.
Pedery
+7  A: 

After much gnashing of teeth and pulling of hair, I was able to come up with a workable solution. It seems there is some undocumented strangeness going on under the covers with .NET and its OLE drag and drop support. It appears to be trying to use .NET remoting when performing drag and drop between .NET applications, but is this documented anywhere? No, I don't think it is.

So the solution I came up with involves a helper class to help marshal the bitmap data between processes. First, here is the class.

[Serializable]
public class BitmapTransfer
{
    private byte[] buffer;
    private PixelFormat pixelFormat;
    private Size size;
    private float dpiX;
    private float dpiY;

    public BitmapTransfer(Bitmap source)
    {
        this.pixelFormat = source.PixelFormat;
        this.size = source.Size;
        this.dpiX = source.HorizontalResolution;
        this.dpiY = source.VerticalResolution;
        BitmapData bitmapData = source.LockBits(
            new Rectangle(new Point(0, 0), source.Size),
            ImageLockMode.ReadOnly, 
            source.PixelFormat);
        IntPtr ptr = bitmapData.Scan0;
        int bufferSize = bitmapData.Stride * bitmapData.Height;
        this.buffer = new byte[bufferSize];
        System.Runtime.InteropServices.Marshal.Copy(ptr, buffer, 0, bufferSize);
        source.UnlockBits(bitmapData);
    }

    public Bitmap ToBitmap()
    {
        Bitmap bitmap = new Bitmap(
            this.size.Width,
            this.size.Height,
            this.pixelFormat);
        bitmap.SetResolution(this.dpiX, this.dpiY);
        BitmapData bitmapData = bitmap.LockBits(
            new Rectangle(new Point(0, 0), bitmap.Size),
            ImageLockMode.WriteOnly, bitmap.PixelFormat);
        IntPtr ptr = bitmapData.Scan0;
        int bufferSize = bitmapData.Stride * bitmapData.Height;
        System.Runtime.InteropServices.Marshal.Copy(this.buffer, 0, ptr, bufferSize);
        bitmap.UnlockBits(bitmapData);
        return bitmap;
    }
}

To use the class in a manner that will support both .NET and unmanaged recipients of the bitmap, a DataObject class is used for the drag and drop operation as follows.

To start the drag operation:

DataObject dataObject = new DataObject();
dataObject.SetData(typeof(BitmapTransfer), 
  new BitmapTransfer((sender as PictureBox).Image as Bitmap));
dataObject.SetData(DataFormats.Bitmap, 
  (sender as PictureBox).Image as Bitmap);
(sender as PictureBox).DoDragDrop(dataObject, DragDropEffects.All);

To complete the operation:

if (dea.Data.GetDataPresent(typeof(BitmapTransfer)))
{
    BitmapTransfer bitmapTransfer = 
       (BitmapTransfer)dea.Data.GetData(typeof(BitmapTransfer));
    (sender as PictureBox).Image = bitmapTransfer.ToBitmap();
}
else if(dea.Data.GetDataPresent(DataFormats.Bitmap))
{
    Bitmap b = (Bitmap)dea.Data.GetData(DataFormats.Bitmap);
    (sender as PictureBox).Image = b;
}

The check for the customer BitmapTransfer is performed first so it takes precedence over the existence of a regular Bitmap in the data object. The BitmapTransfer class could be placed in a shared library for use with multiple applications. It must be marked serializable as shown for drag and drop between applications. I tested it with drag and drop of bitmaps within an application, between applications, and from a .NET application to Wordpad.

Hope this helps you out.

Michael McCloskey
Great answer. Very cool. +1
Judah Himango
Hi Michael!I like your approach. Thanks for the answer! This has bugged me for quite some time and your solution is a good solution to a recurring problem. However, I did find another solution which might be better (at least shorter) in the case of trasfering common clipboard formats. That solution is described below. In any case I want to give you the "accepted answer" cred since your solution is probably more adoptable to the general case. Please review my solution below for a different way to solve this problem.- Peder -
Pedery
Your find is very interesting and is probably the best solution for your case. I fully intend to dig into that article and experiment with this technique. It's a bit sad that such effort was necessary on both our parts to solve this. I cringe anytime I need to interact with the shell from .NET as the marriage is typically a rocky one. Thanks for the credit and even more for the additional information on this topic.
Michael McCloskey
Well, I've spent A LOT of time on this because I really wanted to get to the bottom of it. Examples I found on the web were either ugly workarounds or seriously complicated like in the article. I'm really surprised this is not embedded into the framework the way you'd expect or documented anywhere else. In any case, your approach might actually work better in my case because my intention was all along to pass custom objects back and forth. So I really appreciate you took the time to come up with your own approach to this enigma.
Pedery
+4  A: 

Following hours and hours of frustration with steam coming out of my ears, I finally arrived at a second solution to this problem. Exactly which solution is the most elegant is probably in the eyes of the beholder. I hope that Michael's and my solutions will both aid frustrated programmers and save them time when they embark on similar quests.

First of all, one thing that did strike me was that Wordpad was able to receive the drag/drop images just out of the box. Thus the packaging of the file was probably not the problem, but there was perhaps something fishy going on at the receiving end.

And fishy there was. It turns out there are seveal types of IDataObjects floating about the .Net framework. As Michael pointed out, OLE drag and drop support attempts to use .Net remoting when interacting between applications. This actually puts a System.Runtime.Remoting.Proxies.__TransparentProxy where the image is supposed to be. Clearly this is not (entirely) correct.

The following article gave me a few pointers in the right direction:

http://blogs.msdn.com/adamroot/archive/2008/02/01/shell-style-drag-and-drop-in-net-wpf-and-winforms.aspx

Windows Forms defaults to System.Windows.Forms.IDataObject. However, since we're dealing with different processes here, I decided to give System.Runtime.InteropServices.ComTypes.IDataObject a shot instead.

In the dragdrop event, the following code solves the problem:

const int CF_BITMAP = 2;

System.Runtime.InteropServices.ComTypes.FORMATETC formatEtc;
System.Runtime.InteropServices.ComTypes.STGMEDIUM stgMedium;

formatEtc = new System.Runtime.InteropServices.ComTypes.FORMATETC();
formatEtc.cfFormat = CF_BITMAP;
formatEtc.dwAspect = System.Runtime.InteropServices.ComTypes.DVASPECT.DVASPECT_CONTENT;
formatEtc.lindex = -1;
formatEtc.tymed = System.Runtime.InteropServices.ComTypes.TYMED.TYMED_GDI;

The two GetData functions only share the same name. One returns an object, the other is defined to return void and instead passes the info into the stgMedium out parameter:

(dea.Data as System.Runtime.InteropServices.ComTypes.IDataObject).GetData(ref formatEtc, out stgMedium);
Bitmap remotingImage = Bitmap.FromHbitmap(stgMedium.unionmember);

(sender as PictureBox).Image = remotingImage;

Finally, to avoid memory leaks, it's probably a good idea to call the OLE function ReleaseStgMedium:

ReleaseStgMedium(ref stgMedium);

That function can be included as follows:

[DllImport("ole32.dll")]
public static extern void ReleaseStgMedium([In, MarshalAs(UnmanagedType.Struct)] ref System.Runtime.InteropServices.ComTypes.STGMEDIUM pmedium);

...and this code seems to work perfectly with drag and drop operations (of bitmaps) between two applications. The code could easily be extended to other valid clipboard formats and probably custom clipboard formats too. Since nothing was done with the packaging part, you can still dragdrop an image to Wordpad, and since it accepts bitmap formats, you can also drag an image from Word into the application.

As a side note, dragging and dropping an image directly from IE does not even raise the DragDrop event. Strange.

Pedery
<<As a side note, dragging and dropping an image directly from IE does not even raise the DragDrop event.>> Which OS? On Vista+, there's work done to prevent content from the Internet zone being dropped to unsuspecting applications. You must register your application in the registry to accept drops out of IE.
EricLaw -MSFT-
+2  A: 

I recently came across this problem, and was using a custom format in the clipboard, making Interop a bit more difficult. Anyway, with a bit of light reflection I was able to get to the original System.Windows.Forms.DataObject, and then call the GetData and get my custom item out of it like normal.

var oleConverterType = Type.GetType("System.Windows.DataObject+OleConverter, PresentationCore, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
var oleConverter = typeof(System.Windows.DataObject).GetField("_innerData", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(e.Data);
var dataObject = (System.Windows.Forms.DataObject)oleConverterType.GetProperty("OleDataObject").GetValue(oleConverter, null);

var item = dataObject.GetData(this.Format);
thedesertfox
Interesting. Do you think your method would be "safe", meaning it wouldn't work on some computers and crach on others?
Pedery
The only potential problem would be getting the OleConverter type, which it should always be present with the PresentationCore, so it could be made safer by using a type you know will be in PresentationCore to get the fully qualified assembly name. But other than that, it should work without a problem.
thedesertfox
A: 

Do you have vb.net version of this code?

RShah
Nope, don't like VB :pBut seriously, you should be able to port it pretty easily. There are a total of three solutions to this problem above, and dozens of articles out there discussing "regular" drag/drop operations in VB. Good luck!
Pedery