views:

874

answers:

2

I'm suffering from an OutOfMemoryException when obtaining an Image from an ImageList I've been unable to find an appropriate solution to the problem.

I've got a Custom ListView control, which has attached to it an Event for the drawing of ListViewItems. This then calls a static method which is designed to draw the item.

For a ListView of around 300 items, we're getting the memory jump up around 100Mb each time the ListView is scrolled. The offending code has been tracked down to the following:

Image image = item.ImageList.Images[item.ImageKey];
if (image != null)
{
    Size imageOffset = new Size((bounds.Width - image.Width) / 2, 2); 
    Point imagePosition = bounds.Location + imageOffset;
    graphics.DrawImageUnscaled(image, imagePosition);
}

It seems (certainly on WinXP) that the garbage collection isn't working correctly, causing the memory to spiral. We've tried adding an image.Dispose() directly after the block of code to fix the issue, but that doesn't have any effect.

The only solution I have managed to find so far, is at the end of the static method to call GC.Collect(). The problem with this however is that it then causes the ListView to re-paint itself slowly and you end up getting artifacts on the screen while it attempts to re-draw.

Has anyone else experienced this? Or knows of a workaround?

+2  A: 

Are you disposing graphics? Also, it you dispose your image like you mentioned then you would need to make sure it is taken out of the ImageList or you will cause more issues. What what format are the images?

In general when you get out of memory issues when images are involved, your issue will be either some method does not like some image format, or 9/10 times, you misunderstood the lifecycle of one of the graphic objects.

  • Check all your Graphics usage and put them in using blocks.
  • Check your Image life cycle and be careful with copying them, disposing them, closing underlying streams, etc.
  • Load up a memory manager (VS2008 has one built in) and see what is not getting cleaned up nicely.

EDIT:

Here is the best option I can find, use ImageList.Draw(graphics, x, y, width, height, index). This will use the internal handle instead of creating a copy of the image.

JasonRShaver
Thanks for the response. That's a good point about the Image disposing, I'll probably need to remove that line and just leave the garbage collection.I don't want to Dispose the graphics object, as I grab it from the EventArgs, and hence I don't know if it's going to be used elsewhere afterwards.Images vary, the time we spotted it was an icon file so we use an Icon.ToBitmap().Will try and track down the memory manager, that sounds very useful.
Ian
I've only had a brief attempt at using the memory tool. However it seems that everytime I call item.ImageList.Images[item.ImageKey] I get a new image out, rather than a reference to an existing one.For example I can do item.ImageList.Images[item.ImageKey].Dispose() and then call item.ImageList.Images[item.ImageKey] and still get out a value. So seems rather strange. As yet it hasn't helped me solve the issue though.
Ian
After reading the ImageCollection.Item properties on MSDN I've found that a copy is returned. Calling image.Dispose() is the correct approach in this case.I'm guessing though because there are too many images, that the drawing thread + events are consuming this memory, while the garbage collector waits for this processes to finish before triggering. If we have too many images though it falls over before this process can take place...
Ian
I am editing the above post with an idea that should help you...
JasonRShaver
I'm not sure if the above change works, however based on the fact that no one else offered anything and I can't really test the technique at the moment I'm just going to accept your answer to close off the thread...
Ian
It is one of those things where it *should* cut the problem in half, but that might not be enough I think...
JasonRShaver
A: 

I have managed to solve this issue in my application.

Jason has the answer, you have to make sure you use "using" blocks, or their equivalent.

I use VB, and the equivalent was to use Try... Catch... Finally whenever I created a new bitmap, calling BitMap.Dispose and setting the Bitmap = nothing in the "Finally" part.

This seems to be a really common problem, judging from the hours I have spent trying to Google this. The code below also allows any image to retain its aspect ratio when being reduced to a thumbnail, another issue that seems to be hard to Google!

Code:

Private Function AspectedImage(ByVal ImagePath As String, ByVal SizeWanted As Integer) As Image

    Dim myBitmap, WhiteSpace As System.Drawing.Bitmap
    Dim myGraphics As Graphics
    Dim myDestination As Rectangle
    Dim MaxDimension As Integer
    Dim ReductionRatio As Double

    Try

        'create an instance of bitmap based on a file
        myBitmap = New System.Drawing.Bitmap(ImagePath)



        'create a new square blank bitmap the right size
        If myBitmap.Height >= myBitmap.Width Then MaxDimension = myBitmap.Height Else MaxDimension = myBitmap.Width
        ReductionRatio = SizeWanted / MaxDimension
        WhiteSpace = New System.Drawing.Bitmap(SizeWanted, SizeWanted)

        'get the drawing surface of the new blank bitmap
        myGraphics = Graphics.FromImage(WhiteSpace)

        'find out if the photo is landscape or portrait
        Dim WhiteGap As Double

        If myBitmap.Height > myBitmap.Width Then 'portrait
            WhiteGap = ((myBitmap.Width - myBitmap.Height) / 2) * -1
            myDestination = New Rectangle(x:=CInt(WhiteGap * ReductionRatio), y:=0, Width:=Int(myBitmap.Width * ReductionRatio), Height:=Int(myBitmap.Height * ReductionRatio))
        Else 'landscape
            WhiteGap = ((myBitmap.Width - myBitmap.Height) / 2)
            'create a destination rectangle
            myDestination = New Rectangle(x:=0, y:=CInt(WhiteGap * ReductionRatio), Width:=Int(myBitmap.Width * ReductionRatio), Height:=Int(myBitmap.Height * ReductionRatio))
        End If

        'draw the image on the white square
        myGraphics.DrawImage(image:=myBitmap, rect:=myDestination)
        AspectedImage = WhiteSpace

    Catch ex As Exception
        myBitmap = New System.Drawing.Bitmap("")
        AspectedImage = New System.Drawing.Bitmap(4, 4)
        ImageBufferExceeded = True
        MsgBox("Image Buffer exceeded, too many images in memory. If the one(s) you want can't be seen, please restart the application and navigate straight to your images")
    Finally
        myBitmap.Dispose()
        myBitmap = Nothing
        WhiteSpace = Nothing
    End Try

End Function