Use native HBitmap in C# while preserving alpha channel/transparency
Asked Answered
G

3

3

Let's say I get a HBITMAP object/handle from a native Windows function. I can convert it to a managed bitmap using Bitmap.FromHbitmap(nativeHBitmap), but if the native image has transparency information (alpha channel), it is lost by this conversion.

There are a few questions on Stack Overflow regarding this issue. Using information from the first answer of this question (How to draw ARGB bitmap using GDI+?), I wrote a piece of code that I've tried and it works.

It basically gets the native HBitmap width, height and the pointer to the location of the pixel data using GetObject and the BITMAP structure, and then calls the managed Bitmap constructor:

Bitmap managedBitmap = new Bitmap(bitmapStruct.bmWidth, bitmapStruct.bmHeight,
    bitmapStruct.bmWidth * 4, PixelFormat.Format32bppArgb, bitmapStruct.bmBits);

As I understand (please correct me if I'm wrong), this does not copy the actual pixel data from the native HBitmap to the managed bitmap, it simply points the managed bitmap to the pixel data from the native HBitmap.

And I don't draw the bitmap here on another Graphics (DC) or on another bitmap, to avoid unnecessary memory copying, especially for large bitmaps.

I can simply assign this bitmap to a PictureBox control or the the Form BackgroundImage property. And it works, the bitmap is displayed correctly, using transparency.

When I no longer use the bitmap, I make sure the BackgroundImage property is no longer pointing to the bitmap, and I dispose both the managed bitmap and the native HBitmap.

The Question: Can you tell me if this reasoning and code seems correct. I hope I will not get some unexpected behaviors or errors. And I hope I'm freeing all the memory and objects correctly.

    private void Example()
    {
        IntPtr nativeHBitmap = IntPtr.Zero;

        /* Get the native HBitmap object from a Windows function here */

        // Create the BITMAP structure and get info from our nativeHBitmap
        NativeMethods.BITMAP bitmapStruct = new NativeMethods.BITMAP();
        NativeMethods.GetObjectBitmap(nativeHBitmap, Marshal.SizeOf(bitmapStruct), ref bitmapStruct);

        // Create the managed bitmap using the pointer to the pixel data of the native HBitmap
        Bitmap managedBitmap = new Bitmap(
            bitmapStruct.bmWidth, bitmapStruct.bmHeight, bitmapStruct.bmWidth * 4, PixelFormat.Format32bppArgb, bitmapStruct.bmBits);

        // Show the bitmap
        this.BackgroundImage = managedBitmap;

        /* Run the program, use the image */
        MessageBox.Show("running...");

        // When the image is no longer needed, dispose both the managed Bitmap object and the native HBitmap
        this.BackgroundImage = null;
        managedBitmap.Dispose();
        NativeMethods.DeleteObject(nativeHBitmap);
    }

internal static class NativeMethods
{
    [StructLayout(LayoutKind.Sequential)]
    public struct BITMAP
    {
        public int bmType;
        public int bmWidth;
        public int bmHeight;
        public int bmWidthBytes;
        public ushort bmPlanes;
        public ushort bmBitsPixel;
        public IntPtr bmBits;
    }

    [DllImport("gdi32", CharSet = CharSet.Auto, EntryPoint = "GetObject")]
    public static extern int GetObjectBitmap(IntPtr hObject, int nCount, ref BITMAP lpObject);

    [DllImport("gdi32.dll")]
    internal static extern bool DeleteObject(IntPtr hObject);
}
Grandniece answered 7/1, 2011 at 15:54 Comment(2)
Stuff like "Please check this code, it works on my computer…" really don't belong to questions, or topic titles.Hendry
You are right, I changed the title. It is a question, but it also has code in it.Grandniece
J
2

Right, no copy is made. Which is why the Remarks section of the MSDN Library says:

The caller is responsible for allocating and freeing the block of memory specified by the scan0 parameter, however, the memory should not be released until the related Bitmap is released.

This wouldn't be a problem if the pixel data was copied. Incidentally, this is normally a difficult problem to deal with. You can't tell when the client code called Dispose(), there's no way to intercept that call. Which makes it impossible to make such a bitmap behave like a replacement for Bitmap. The client code has to be aware that additional work is needed.

Judaize answered 7/1, 2011 at 16:50 Comment(6)
Thank you for your good remarks. I made another version of the method listed bellow. Can you take a look at it? ThanksGrandniece
Well, that's fine. I thought you didn't want to copy.Judaize
A problem is that if the native HBitmap is an icon (for example returned by IShellItemImageFactory::GetImage with SIIGBF_ICONONLY), the bitmap comes inverted (up side down). So a simple graphics.DrawImage does not work in this case.Grandniece
Sounds like a bug. You could use Bitmap.RotateFlip() to fix it perhaps.Judaize
Yes, I did use Bitmap.RotateFlip() and it works for icons, but IShellItemImageFactory::GetImage gets thumbnails correctly. So how can I check if the returned HBITMAP is inverted or not (an icon or not)?Grandniece
This just doesn't have anything to do anymore with the original question. Please start a new one and tag it appropriately.Judaize
F
14

The following code worked for me even if the HBITMAP is an icon or bmp, it doesn't flip the image when it's an icon, and also works with bitmaps that don't contain Alpha channel:

    private static Bitmap GetBitmapFromHBitmap(IntPtr nativeHBitmap)
    {
        Bitmap bmp = Bitmap.FromHbitmap(nativeHBitmap);

        if (Bitmap.GetPixelFormatSize(bmp.PixelFormat) < 32)
            return bmp;

        BitmapData bmpData;

        if (IsAlphaBitmap(bmp, out bmpData))
            return GetlAlphaBitmapFromBitmapData(bmpData);

        return bmp;
    }

    private static Bitmap GetlAlphaBitmapFromBitmapData(BitmapData bmpData)
    {
        return new Bitmap(
                bmpData.Width,
                bmpData.Height,
                bmpData.Stride,
                PixelFormat.Format32bppArgb,
                bmpData.Scan0);
    }

    private static bool IsAlphaBitmap(Bitmap bmp, out BitmapData bmpData)
    {
        Rectangle bmBounds = new Rectangle(0, 0, bmp.Width, bmp.Height);

        bmpData = bmp.LockBits(bmBounds, ImageLockMode.ReadOnly, bmp.PixelFormat);

        try
        {
            for (int y = 0; y <= bmpData.Height - 1; y++)
            {
                for (int x = 0; x <= bmpData.Width - 1; x++)
                {
                    Color pixelColor = Color.FromArgb(
                        Marshal.ReadInt32(bmpData.Scan0, (bmpData.Stride * y) + (4 * x)));

                    if (pixelColor.A > 0 & pixelColor.A < 255)
                    {
                        return true;
                    }
                }
            }
        }
        finally
        {
            bmp.UnlockBits(bmpData);
        }

        return false;
    }
Fenian answered 15/2, 2012 at 9:53 Comment(4)
+1 for an awesome answer! This solution fits nicely, Thank you.Remission
@DanielPeñalba Can you tell me how you would call GetBitmapFromHBitmap? I'm assuming I can't do GetBitmapFromHBitmap(New Bitmap("fileName").GetHbitmap()) as that would defeat the whole purpose, right? So from a file in explorer, would I get a managed Bitmap object with the alpha channel preserved?Bainbridge
@test: This code is intended to be called having a (native) image handle. Don't use it for managed bitmaps.Africander
Excellent answer with useful reusable methods. It served me to get folder icons ( calling IShellItemImageFactory.GetImage() ) with proper transparency. Tried "Bitmap.MakeTransparent()" before this but was not a good idea.Interfere
J
2

Right, no copy is made. Which is why the Remarks section of the MSDN Library says:

The caller is responsible for allocating and freeing the block of memory specified by the scan0 parameter, however, the memory should not be released until the related Bitmap is released.

This wouldn't be a problem if the pixel data was copied. Incidentally, this is normally a difficult problem to deal with. You can't tell when the client code called Dispose(), there's no way to intercept that call. Which makes it impossible to make such a bitmap behave like a replacement for Bitmap. The client code has to be aware that additional work is needed.

Judaize answered 7/1, 2011 at 16:50 Comment(6)
Thank you for your good remarks. I made another version of the method listed bellow. Can you take a look at it? ThanksGrandniece
Well, that's fine. I thought you didn't want to copy.Judaize
A problem is that if the native HBitmap is an icon (for example returned by IShellItemImageFactory::GetImage with SIIGBF_ICONONLY), the bitmap comes inverted (up side down). So a simple graphics.DrawImage does not work in this case.Grandniece
Sounds like a bug. You could use Bitmap.RotateFlip() to fix it perhaps.Judaize
Yes, I did use Bitmap.RotateFlip() and it works for icons, but IShellItemImageFactory::GetImage gets thumbnails correctly. So how can I check if the returned HBITMAP is inverted or not (an icon or not)?Grandniece
This just doesn't have anything to do anymore with the original question. Please start a new one and tag it appropriately.Judaize
G
1

After reading the good points made by Hans Passant in his answer, I changed the method to immediately copy the pixel data into the managed bitmap, and free the native bitmap.

I'm creating two managed bitmap objects (but only one allocates memory for the actual pixel data), and use graphics.DrawImage to copy the image. Is there a better way to accomplish this? Or is this good/fast enough?

    public static Bitmap CopyHBitmapToBitmap(IntPtr nativeHBitmap)
    {
        // Get width, height and the address of the pixel data for the native HBitmap
        NativeMethods.BITMAP bitmapStruct = new NativeMethods.BITMAP();
        NativeMethods.GetObjectBitmap(nativeHBitmap, Marshal.SizeOf(bitmapStruct), ref bitmapStruct);

        // Create a managed bitmap that has its pixel data pointing to the pixel data of the native HBitmap
        // No memory is allocated for its pixel data
        Bitmap managedBitmapPointer = new Bitmap(
            bitmapStruct.bmWidth, bitmapStruct.bmHeight, bitmapStruct.bmWidth * 4, PixelFormat.Format32bppArgb, bitmapStruct.bmBits);

        // Create a managed bitmap and allocate memory for pixel data
        Bitmap managedBitmapReal = new Bitmap(bitmapStruct.bmWidth, bitmapStruct.bmHeight, PixelFormat.Format32bppArgb);

        // Copy the pixels of the native HBitmap into the canvas of the managed bitmap
        Graphics graphics = Graphics.FromImage(managedBitmapReal);
        graphics.DrawImage(managedBitmapPointer, 0, 0);

        // Delete the native HBitmap object and free memory
        NativeMethods.DeleteObject(nativeHBitmap);

        // Return the managed bitmap, clone of the native HBitmap, with correct transparency
        return managedBitmapReal;
    }
Grandniece answered 7/1, 2011 at 18:0 Comment(2)
Great answer, I have used your code provided in this answer and it works, but sometimes the image is flipped 180º and rotated 180º. Do you know why? Thanks in advance.Africander
You should dispose the Graphics object after you are done with it. Preferably, wrap it in a using statement.Cummings

© 2022 - 2024 — McMap. All rights reserved.