在C#中使用本地HBitmap并保留alpha通道/透明度

3
假设我从本地Windows函数获取了一个HBITMAP对象/句柄。我可以使用Bitmap.FromHbitmap(nativeHBitmap)将其转换为托管位图,但是如果本地图像具有透明度信息(alpha通道),则此转换会丢失该信息。
关于这个问题,在Stack Overflow上有一些问题。使用这个问题的第一个答案(如何使用GDI+绘制ARGB位图?)中的信息,我编写了一段代码并尝试过它可以工作。
基本上,它获取本地HBitmap的宽度、高度和像素数据位置的指针,使用GetObjectBITMAP结构,然后调用托管位图构造函数:
Bitmap managedBitmap = new Bitmap(bitmapStruct.bmWidth, bitmapStruct.bmHeight,
    bitmapStruct.bmWidth * 4, PixelFormat.Format32bppArgb, bitmapStruct.bmBits);

据我理解(如果我有错,请纠正),这并不是将本地 HBitmap 的实际像素数据复制到托管位图中,而是仅将托管位图指向来自本地 HBitmap 的像素数据。
在此处,我不会在另一个 Graphics(DC)或另一个位图上绘制位图,以避免不必要的内存复制,特别是对于大型位图。
我可以简单地将此位图分配给 PictureBox 控件或 Form BackgroundImage 属性。它能够正确显示位图,并使用透明度。
当我不再使用该位图时,我确保 BackgroundImage 属性不再指向该位图,并且释放了托管位图和本地 HBitmap。
问题是:您能告诉我这个推理和代码是否正确吗?我希望不会出现一些意外的行为或错误。我也希望我正在正确释放所有的内存和对象。
    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);
}

像“请检查这段代码,它在我的电脑上运行正常…”这样的内容真的不应该出现在问题或主题标题中。 - Claus Jørgensen
你说得对,我改了标题。这是一个问题,但它也包含代码。 - TechAurelian
3个回答

14
以下代码对我有效,即使 HBITMAP 是图标或 BMP,当它是一个图标时不会翻转图像,并且也适用于不包含 Alpha 通道的位图:
    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;
    }

1
非常感谢您的出色回答!这个解决方案非常合适,谢谢。 - OptimizePrime
@DanielPeñalba,你能告诉我如何调用GetBitmapFromHBitmap吗?我猜我不能这样做GetBitmapFromHBitmap(New Bitmap("fileName").GetHbitmap()),因为那样会失去整个目的,对吧?那么从资源管理器中的文件,我能得到一个保留alpha通道的托管Bitmap对象吗? - test
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Daniel Peñalba
优秀的答案,提供了有用的可重复使用的方法。它帮助我正确地获取文件夹图标(调用IShellItemImageFactory.GetImage())并具有透明度。在此之前尝试过"Bitmap.MakeTransparent()",但不是一个好主意。 - ElektroStudios

2

没错,不会进行复制。这就是为什么MSDN Library的“备注”部分说:

调用方负责分配和释放由scan0参数指定的内存块,但是在相关位图被释放之前,不应该释放该内存。

如果像素数据被复制,这将不是一个问题。顺便说一下,通常这是一个难以处理的问题。您无法知道客户端代码何时调用了Dispose(),也没有办法拦截该调用。这使得无法使这样的位图行为类似于Bitmap的替代品。客户端代码必须意识到需要额外的工作。


谢谢您的好评。我制作了下面列出的方法的另一个版本。您能看一下吗?谢谢。 - TechAurelian
好的,没问题。我以为你不想复制。 - Hans Passant
问题在于,如果本地HBitmap是一个图标(例如通过IShellItemImageFactory :: GetImage与SIIGBF_ICONONLY返回的图标),则位图会倒置(上下翻转)。 因此,在这种情况下,简单的_graphics.DrawImage_无法工作。 - TechAurelian
听起来像是一个 bug。也许你可以使用 Bitmap.RotateFlip() 来修复它。 - Hans Passant
是的,我确实使用了Bitmap.RotateFlip(),它适用于图标,但IShellItemImageFactory::GetImage可以正确获取缩略图。那么我该如何检查返回的HBITMAP是否翻转(是否为图标)? - TechAurelian
这与原问题已经没有任何关系了。请开一个新的问题并适当标记。 - Hans Passant

1
在阅读了Hans Passant在他的回答中提出的好观点后,我改变了方法,立即将像素数据复制到托管位图中,并释放本地位图。
我创建了两个托管位图对象(但只有一个为实际像素数据分配内存),并使用graphics.DrawImage来复制图像。有更好的方法来完成这个任务吗?或者这样做已经足够好/快了吗?
    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;
    }

1
很好的回答,我已经使用了你在这个答案中提供的代码,它可以工作,但有时图像会翻转180º并旋转180º。你知道为什么吗?先感谢你。 - Daniel Peñalba
1
在使用完Graphics对象后,应该将其释放。最好将其包装在using语句中。 - Jelle Vergeer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接