Search code examples
c#winformsgdi+alphablendingmouse-cursor

Windows Forms: Making a cursor bitmap partially transparent


I want to use partially transparent images in drag/drop operations. This is all set up and works fine, but the actual transformation to transparency has a weird side effect. For some reason, the pixels seem to be blended against a black background.

The following image describes the problem:

Transparency problem

Figure a) is the original bitmap.

Figure b) is what is produced after alpha blending has been performed. Obviously this is a lot darker than the intended 50% alpha filter intended.

Figure c) is the desired effect, image a) with 50% transparency (added to the composition with a drawing program).

The code I use to produce the trasparent image is the following:

Bitmap bmpNew = new Bitmap(bmpOriginal.Width, bmpOriginal.Height);
Graphics g = Graphics.FromImage(bmpNew);

// Making the bitmap 50% transparent:
float[][] ptsArray ={ 
    new float[] {1, 0, 0, 0, 0},        // Red
    new float[] {0, 1, 0, 0, 0},        // Green
    new float[] {0, 0, 1, 0, 0},        // Blue
    new float[] {0, 0, 0, 0.5f, 0},     // Alpha
    new float[] {0, 0, 0, 0, 1}         // Brightness
};
ColorMatrix clrMatrix = new ColorMatrix(ptsArray);
ImageAttributes imgAttributes = new ImageAttributes();
imgAttributes.SetColorMatrix(clrMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
g.DrawImage(bmpOriginal, new Rectangle(0, 0, bmpOriginal.Width, bmpOriginal.Height), 0, 0, bmpOriginal.Width, bmpOriginal.Height, GraphicsUnit.Pixel, imgAttributes);
Cursors.Default.Draw(g, new Rectangle(bmpOriginal.Width / 2 - 8, bmpOriginal.Height / 2 - 8, 32, 32));
g.Dispose();
imgAttributes.Dispose();
return bmpNew;

Does anyone know why the alpha blending does not work?

Update I:

For clarity, the code does work if I'm alphablending on top of a drawn surface. The problem is that I want to create a completely semitransparent image from an existing image and use this as a dynamic cursor during drag/drop operations. Even skipping the above and only painting a filled rectangle of color 88ffffff yields a dark grey color. Something fishy is going on with the icon.

Update II:

Since I've reseached a whole lot and believe this has got something to do with the Cursor creation, I'm gonna include that code below too. If I GetPixel-sample the bitmap just before the CreateIconIndirect call, the four color values seem to be intact. Thus I have a feeling the culprits might be the hbmColor or the hbmMask members of the IconInfo structure.

Here's the IconInfo structure:

public struct IconInfo {    // http://msdn.microsoft.com/en-us/library/ms648052(VS.85).aspx
    public bool fIcon;      // Icon or cursor. True = Icon, False = Cursor
    public int xHotspot;
    public int yHotspot;
    public IntPtr hbmMask;  // 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; // 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. 

}

And here is the code that actually creates the Cursor:

    public static Cursor CreateCursor(Bitmap bmp, int xHotSpot, int yHotSpot) {
        IconInfo iconInfo = new IconInfo();
        GetIconInfo(bmp.GetHicon(), ref iconInfo);
        iconInfo.hbmColor = (IntPtr)0;
        iconInfo.hbmMask = bmp.GetHbitmap();
        iconInfo.xHotspot = xHotSpot;
        iconInfo.yHotspot = yHotSpot;
        iconInfo.fIcon = false;

        return new Cursor(CreateIconIndirect(ref iconInfo));
    }

The two external functions are defined as follows:

    [DllImport("user32.dll", EntryPoint = "CreateIconIndirect")]
    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);

Solution

  • GDI+ has a number of problems related to alpha blending when doing interop with GDI (and Win32). In this case, the call to bmp.GetHbitmap() will blend your image with a black background. An article on CodeProject gives more detail on the problem, and a solution that was used for adding images to an image list.

    You should be able to use similar code to get the HBITMAP to use for the mask:

    [DllImport("kernel32.dll")]
    public static extern bool RtlMoveMemory(IntPtr dest, IntPtr source, int dwcount);
    [DllImport("gdi32.dll")]
    public static extern IntPtr CreateDIBSection(IntPtr hdc, [In, MarshalAs(UnmanagedType.LPStruct)]BITMAPINFO pbmi, uint iUsage, out IntPtr ppvBits, IntPtr hSection, uint dwOffset);
    
    public static IntPtr GetBlendedHBitmap(Bitmap bitmap)
    {
        BITMAPINFO bitmapInfo = new BITMAPINFO();
        bitmapInfo.biSize = 40;
        bitmapInfo.biBitCount = 32;
        bitmapInfo.biPlanes = 1;
    
        bitmapInfo.biWidth = bitmap.Width;
        bitmapInfo.biHeight = -bitmap.Height;
    
        IntPtr pixelData;
        IntPtr hBitmap = CreateDIBSection(
            IntPtr.Zero, bitmapInfo, 0, out pixelData, IntPtr.Zero, 0);
    
        Rectangle bounds = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
        BitmapData bitmapData = bitmap.LockBits(
            bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb );
        RtlMoveMemory(
            pixelData, bitmapData.Scan0, bitmap.Height * bitmapData.Stride);
    
        bitmap.UnlockBits(bitmapData);
        return hBitmap;
    }