1

Our application does some cursor manipulation to enable "relatively" nice drag drop animation on WinForms (at the time WPF wasn't an option). However when using the application over a RDP session it throws a generic GDI+ exception.

The method which throws this is this:

[DllImport("user32")]
private static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO pIconInfo);

[DllImport("user32.dll")]
private static extern IntPtr LoadCursorFromFile(string lpFileName);

[DllImport("user32.dll", SetLastError = true)]
public static extern bool DestroyIcon(IntPtr hIcon);

[DllImport("gdi32.dll", SetLastError = true)]
private static extern bool DeleteObject(IntPtr hObject);

public static Bitmap BitmapFromCursor(Cursor cur)
{
    ICONINFO iInfo;
    GetIconInfo(cur.Handle, out iInfo);

    Bitmap bmp = Bitmap.FromHbitmap(iInfo.hbmColor);
    DeleteObject(iInfo.hbmColor);
    DeleteObject(iInfo.hbmMask);

    BitmapData bmData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
    Bitmap dstBitmap = new Bitmap(bmData.Width, bmData.Height, bmData.Stride, PixelFormat.Format32bppArgb, bmData.Scan0);
    bmp.UnlockBits(bmData);

    return new Bitmap(dstBitmap);
}

Specifically the line:

Bitmap bmp = Bitmap.FromHbitmap(iInfo.hbmColor);

When debugging hbmColor is 0, which means when running over RDP the call to GetIconInfo doesn't return the required information.

I can check for 0 and handle the special case, but is there anything I can do to make this work over RDP as it would do normally?

Edit

Here's the ICONINFO structure:

[StructLayout(LayoutKind.Sequential)]
struct ICONINFO
{
     public bool fIcon;         // Specifies whether this structure defines an icon or a cursor. A value of TRUE specifies
                    // an icon; FALSE specifies a cursor.
     public Int32 xHotspot;     // Specifies the x-coordinate of a cursor's hot spot. If this structure defines an icon, the hot
                    // spot is always in the center of the icon, and this member is ignored.
     public Int32 yHotspot;     // Specifies the y-coordinate of the cursor's hot spot. If this structure defines an icon, the hot
                    // spot is always in the center of the icon, and this member is ignored.
     public IntPtr hbmMask;     // (HBITMAP) Specifies the icon bitmask bitmap. If this structure defines a black and white icon,
                    // this bitmask is formatted so that the upper half is the icon AND bitmask and the lower half is
                    // the icon XOR bitmask. Under this condition, the height should be an even multiple of two. If
                    // this structure defines a color icon, this mask only defines the AND bitmask of the icon.
     public IntPtr hbmColor;    // (HBITMAP) Handle to the icon color bitmap. This member can be optional if this
                    // structure defines a black and white icon. The AND bitmask of hbmMask is applied with the SRCAND
                    // flag to the destination; subsequently, the color bitmap is applied (using XOR) to the
                    // destination by using the SRCINVERT flag.
}        

From HABJAN's answer below I've added the comments from p/Invoke to the structure above. It looks like hbmMask contains the bitmap reference I'm after, but I'm afraid my bit manipulation skills are rather rusty. When p/Invoke says upper half / lower half - what is it inferring to?

Is it possible to get the black and white bitmap from this?

Marlon
  • 2,129
  • 3
  • 21
  • 40
  • Can you show your 'ICONINFO' structure definition? – HABJAN Jun 10 '14 at 10:11
  • Ignoring the return value of winapi functions is a standard mistake. It is *not* optional, you don't have the friendly .NET exceptions to keep you out of trouble. Throw a Win32Exception when GetIconInfo() returns *false* so you know why it is failing. And fix the declaration to add SetLastError = true. – Hans Passant Jun 10 '14 at 12:04
  • Cheers Hans, that is something I need to get more familiar with rather than just copying and pasting from p/Invoke. I'll keep that in mind next time.. – Marlon Jun 10 '14 at 14:09

2 Answers2

1

I think that this is due your RDP color depth. If your cursor is black and white only (via RDP), you will not get hbmColor value as this parameter is optional.

MSDN says:

hbmColor

Type: HBITMAP

Description: A handle to the icon color bitmap. This member can be optional if this structure defines a black and white icon. The AND bitmask of hbmMask is applied with the SRCAND flag to the destination; subsequently, the color bitmap is applied (using XOR) to the destination by using the SRCINVERT flag.

EDIT:

public static Bitmap BitmapFromCursor(Cursor cur)
{
    ICONINFO iInfo;
    GetIconInfo(cur.Handle, out iInfo);

    Bitmap bmpColor = null;

    if (iInfo.hbmColor != IntPtr.Zero) {
       bmpColor = Bitmap.FromHbitmap(iInfo.hbmColor);
    }
    else {
       bmpColor = new Bitmap(w,h);
       // fill bmpColor with white colour
    }

    Bitmap bmpMask = Bitmap.FromHbitmap(iInfo.hbmMask);
    DeleteObject(iInfo.hbmColor);
    DeleteObject(iInfo.hbmMask);

    // apply mask bitmap to color bitmap:
    // http://stackoverflow.com/questions/3654220/alpha-masking-in-c-sharp-system-drawing

    BitmapData bmData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);
    Bitmap dstBitmap = new Bitmap(bmData.Width, bmData.Height, bmData.Stride, PixelFormat.Format32bppArgb, bmData.Scan0);
    bmp.UnlockBits(bmData);

    return new Bitmap(dstBitmap);
}

... i did not test this code, it's just to give you a brief info what to do...

HABJAN
  • 9,212
  • 3
  • 35
  • 59
  • Hello, yes you are correct - I've amended my question with the extra info from p/Invoke - it looks like hbmMask contains what I need. I don't suppose you know if I can get a bitmap from this? – Marlon Jun 10 '14 at 10:51
  • @Marlon: a same you do for 'iInfo.hbmColor', you need to do for 'iInfo.hbmMask'. This way you will get 2 bitmaps, one color bitmap and the second mask bitmap. So, you need to check if the hbmColor == 0, if it is, create simple bitmap with the white background and apply mask over it. To apply mask you can check out this question: http://stackoverflow.com/questions/3654220/alpha-masking-in-c-sharp-system-drawing – HABJAN Jun 10 '14 at 11:09
  • @Marlon: I edited my answer with short sample how it should look like. – HABJAN Jun 10 '14 at 11:15
1

With the help of HABJAN I was able to come up with a method to do the job. The reason I'm writing the answer here is because the bitmap mask you get from the handle contains two masks, so you have to select which version you want (as per the documentation).

public static Bitmap GetBitmapFromMask(IntPtr maskH)
{
    using (var bothMasks = Bitmap.FromHbitmap(maskH))
    {
        int midY = bothMasks.Height / 2;
        using (var mask = bothMasks.Clone(new Rectangle(0, midY, bothMasks.Width, midY), bothMasks.PixelFormat))
        {
            using (var input = new Bitmap(mask.Width, mask.Height))
            {
                using (var g = Graphics.FromImage(input))
                {
                    using (var b = new SolidBrush(Color.FromArgb(255, 255, 255, 255)))
                        g.FillRectangle(b, 0, 0, input.Width, input.Height);
                }

                var output = new Bitmap(mask.Width, mask.Height, PixelFormat.Format32bppArgb);
                var rect = new Rectangle(0, 0, input.Width, input.Height);
                var bitsMask = mask.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
                var bitsInput = input.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
                var bitsOutput = output.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
                unsafe
                {
                    for (int y = 0; y < input.Height; y++)
                    {
                        byte* ptrMask = (byte*)bitsMask.Scan0 + y * bitsMask.Stride;
                        byte* ptrInput = (byte*)bitsInput.Scan0 + y * bitsInput.Stride;
                        byte* ptrOutput = (byte*)bitsOutput.Scan0 + y * bitsOutput.Stride;
                        for (int x = 0; x < input.Width; x++)
                        {
                            ptrOutput[4 * x] = ptrInput[4 * x];           // blue
                            ptrOutput[4 * x + 1] = ptrInput[4 * x + 1];   // green
                            ptrOutput[4 * x + 2] = ptrInput[4 * x + 2];   // red
                            ptrOutput[4 * x + 3] = ptrMask[4 * x];        // alpha
                        }
                    }
                }
                mask.UnlockBits(bitsMask);
                input.UnlockBits(bitsInput);
                output.UnlockBits(bitsOutput);
                return output;
            }
        }
    }
}

This is a basic copy of the answer linked by HABJAN - it doesn't seem to do either a logical AND or a logical XOR on the resulting bytes - none the less seems to do the required job.

Marlon
  • 2,129
  • 3
  • 21
  • 40