Search code examples
c#.netvb.netmemory-leaksbitblt

VB.NET/C#; Using BitBlt; Using same code in both, memory leak appearing in VB.NET but not C#


Looking to use BitBlt for an application, I found a C# reference at BitBlt code not working and converted it to VB.net. I mainly use VB.net which is why I converted it, and trying to use it as such. It works fine in C#, but in VB.net it has a memory leak and I'm not sure how to fix it.

Code:

C# version (modified from above source a little). Open new project, add 1 button and 1 picturebox, modify lstPics.Add():

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        [DllImport("gdi32.dll", EntryPoint = "SelectObject")]
        public static extern System.IntPtr SelectObject(
        [In()] System.IntPtr hdc,
        [In()] System.IntPtr h);

        [DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool DeleteObject(
            [In()] System.IntPtr ho);

        [DllImport("gdi32.dll", EntryPoint = "BitBlt")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool BitBlt(
            [In()] System.IntPtr hdc, int x, int y, int cx, int cy,
            [In()] System.IntPtr hdcSrc, int x1, int y1, uint rop);

        public Form1()

        {
            InitializeComponent();
        }

        public Int16 lstLoc = 0;

        private void button1_Click(object sender, EventArgs e)
        {
            List<string> lstPics = new List<string>();
            lstPics.Add("C:\\1.jpg");
            lstPics.Add("C:\\2.jpg");
            lstPics.Add("C:\\3.jpg");
            if ((lstLoc == lstPics.Count))
            {
                lstLoc = 0;
            }

            string strLoc = lstPics[lstLoc];
            lstLoc++;

            using (Bitmap bmp = (Bitmap)Bitmap.FromFile(strLoc))
            using (Graphics grDest = Graphics.FromHwnd(pictureBox1.Handle))
            using (Graphics grSrc = Graphics.FromImage(bmp))
            {
                IntPtr hdcDest = IntPtr.Zero;
                IntPtr hdcSrc = IntPtr.Zero;
                IntPtr hBitmap = IntPtr.Zero;
                IntPtr hOldObject = IntPtr.Zero;

                try
                {
                    hdcDest = grDest.GetHdc();
                    hdcSrc = grSrc.GetHdc();
                    hBitmap = bmp.GetHbitmap();

                    hOldObject = SelectObject(hdcSrc, hBitmap);
                    if (hOldObject == IntPtr.Zero)
                        throw new Win32Exception();

                    if (!BitBlt(hdcDest, 0, 0, pictureBox1.Width, pictureBox1.Height,
                        hdcSrc, 0, 0, 0x00CC0020U))
                        throw new Win32Exception();
                }
                finally
                {
                    if (hOldObject != IntPtr.Zero) SelectObject(hdcSrc, hOldObject);
                    if (hBitmap != IntPtr.Zero) DeleteObject(hBitmap);
                    if (hdcDest != IntPtr.Zero) grDest.ReleaseHdc(hdcDest);
                    if (hdcSrc != IntPtr.Zero) grSrc.ReleaseHdc(hdcSrc);
                }
            }
        }
    }
}

VB.net version (converted by me). Open new project, add 1 button and 1 picturebox, modify lstPics.Add():

Imports System.ComponentModel

Public Class Form1
    Public Declare Function SelectObject Lib "gdi32.dll" Alias "SelectObject" (ByVal hdc As System.IntPtr, ByVal h As System.IntPtr) As System.IntPtr
    Public Declare Function DeleteObject Lib "gdi32.dll" Alias "DeleteObject" (ByVal ho As System.IntPtr) As Boolean
    Public Declare Function BitBlt Lib "gdi32.dll" Alias "BitBlt" (ByVal hdc As System.IntPtr, ByVal x As Integer, ByVal y As Integer, ByVal cx As Integer, ByVal cy As Integer, ByVal hdcSrc As System.IntPtr, ByVal x1 As Integer, ByVal y1 As Integer, ByVal rop As UInteger) As Boolean

    Public lstLoc As Int16 = 0

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim mil1 As Int64 = 0
        Dim mil2 As Int64 = 0
        Dim mil3 As Int64 = 0

        Dim lstPics As New List(Of String)
        lstPics.Add("C:\1.jpg")
        lstPics.Add("C:\2.jpg")
        lstPics.Add("C:\3.jpg")

        If lstLoc = lstPics.Count Then lstLoc = 0
        Dim strLoc As String = lstPics(lstLoc)
        lstLoc += 1

        Using bmp As Bitmap = Bitmap.FromFile(strLoc),
            grDest As Graphics = Graphics.FromHwnd(PictureBox1.Handle),
            grSrc As Graphics = Graphics.FromImage(bmp)

            Dim hdcDest As IntPtr = IntPtr.Zero
            Dim hdcSrc As IntPtr = IntPtr.Zero
            Dim hBitmap As IntPtr = IntPtr.Zero
            Dim hOldObject As IntPtr = IntPtr.Zero

            Try
                hdcDest = grDest.GetHdc()
                hdcSrc = grSrc.GetHdc()
                hBitmap = bmp.GetHbitmap()

                hOldObject = SelectObject(hdcSrc, bmp.GetHbitmap)

                If (hOldObject = IntPtr.Zero) Then Throw New Win32Exception()

                If Not BitBlt(hdcDest, 0, 0, PictureBox1.Width, PictureBox1.Height, hdcSrc, 0, 0, 13369376) Then Throw New Win32Exception()

            Catch ex As Exception
                MessageBox.Show(ex.Message.ToString & vbNewLine & vbNewLine & ex.ToString)

            Finally
                If (hOldObject <> IntPtr.Zero) Then SelectObject(hdcSrc, hOldObject)
                If (hBitmap <> IntPtr.Zero) Then DeleteObject(hBitmap)
                If (hdcDest <> IntPtr.Zero) Then grDest.ReleaseHdc(hdcDest)
                If (hdcSrc <> IntPtr.Zero) Then grSrc.ReleaseHdc(hdcSrc)

            End Try

        End Using

    End Sub

End Class

When you push the button and cycle through different pictures, the C# version will spike in memory usage but then go back down. The VB.net version however will keep rising in memory usage. Where is this leak coming from, and why does it only happen in VB.net?

I know I have other options available, such as DrawImage or displaying directly in the picturebox. I wanted to use BitBlt for the speed. And wanted to test it out and get it working before putting it into main application (which handles images).

Thanks for any help.

And bonus question, is there a way to get memory usage down in the above code (mainly when it spikes). Just wanna make sure I'm not being wasteful. Thanks.


Solution

  • The VB version calls bmp.GetHbitmap() twice:

    hBitmap = bmp.GetHbitmap()
    hOldObject = SelectObject(hdcSrc, bmp.GetHbitmap)
    '                                      ^^^ Here it is again
    

    While the C# version only calls it once:

    hBitmap = bmp.GetHbitmap();
    hOldObject = SelectObject(hdcSrc, hBitmap);
    //                                ^^^ uses the handle from the previous line
    

    Note these excerpts from the documentation on GetHbitmap:

    Creates a GDI bitmap object from this Bitmap.
    ...

    Remarks:

    You are responsible for calling the GDI DeleteObject method to free the memory used by the GDI bitmap object.

    So the GetHbitmap() method is confusingly named, in that it doesn't just give you the handle that's already there, but actually creates a new GDI resource you must clean up. To make the VB code equivalent to the C#, do this:

    hBitmap = bmp.GetHbitmap()
    hOldObject = SelectObject(hdcSrc, hBitmap)