Используйте собственный HBitmap в C#, сохраняя альфа-канал / прозрачность

Допустим, я получил объект / дескриптор HBITMAP из встроенной функции Windows. Я могу преобразовать его в управляемое растровое изображение, используя Bitmap.FromHbitmap (nativeHBitmap), но если собственное изображение содержит информацию о прозрачности (альфа-канал), оно теряется при таком преобразовании.

Есть несколько вопросов по переполнению стека относительно этой проблемы. Используя информацию из первого ответа на этот вопрос ( Как нарисовать растровое изображение ARGB с помощью GDI +?), Я написал фрагмент кода, который я пробовал, и он работает.

Он в основном получает собственную ширину HBitmap, высоту и указатель на местоположение пиксельных данных, используя GetObject и структуру BITMAP, а затем вызывает управляемый конструктор Bitmap:

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

Как я понимаю (пожалуйста, исправьте меня, если я ошибаюсь), это не копирует фактические данные пикселей из собственного HBitmap в управляемое растровое изображение, оно просто указывает управляемое растровое изображение на данные пикселей из собственного HBitmap.

И я не рисую растровое изображение здесь на другой графике (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);
}

3 ответа

Решение

Да, копия не сделана. Вот почему в разделе "Примечания" библиотеки MSDN говорится:

Вызывающая сторона отвечает за выделение и освобождение блока памяти, указанного в параметре scan0, однако память не должна освобождаться до тех пор, пока не будет выпущено соответствующее растровое изображение.

Это не будет проблемой, если данные пикселей будут скопированы. Между прочим, обычно это сложная проблема. Вы не можете сказать, когда клиентский код называется Dispose(), нет способа перехватить этот вызов. Что делает невозможным заставить такое растровое изображение вести себя как замена растрового изображения. Код клиента должен знать, что требуется дополнительная работа.

Следующий код работал для меня, даже если HBITMAP это значок или BMP, он не переворачивает изображение, когда это значок, а также работает с растровыми изображениями, которые не содержат альфа-канал:

    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;
    }

После прочтения хороших замечаний, высказанных Хансом Пассантом в его ответе, я изменил метод, чтобы немедленно скопировать данные пикселей в управляемое растровое изображение и освободить исходное растровое изображение.

Я создаю два управляемых растровых объекта (но только один выделяет память для фактических данных пикселей) и использую 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;
    }
Другие вопросы по тегам