Search code examples
c#.netwinformsgdi+system.drawing

Caching GDI+ objects in a winforms application: is it worth it and how to do it right?


For some of my winforms applications I need to create a whole bunch of GDI+ objects (brushes, pens, fonts, etc) and use them over and over again. I created a ghetto caching singleton to accomplish what I need, but the code smell is overwhelming...

public sealed class GraphicsPalette
{
    public static readonly GraphicsPalette Instance = new GraphicsPalette();

    static GraphicsPalette()
    {
    }

    private Dictionary<Color, Brush> solidBrushes;

    //multithreading
    private object brushLock;

    private GraphicsPalette()
    {
        solidBrushes = new Dictionary<Color, Brush>();

        brushLock = new object();
    }

    public Brush GetSolidBrush(Color color, int alpha)
    {
        return GetSolidBrush(Color.FromArgb(alpha, color));
    }

    public Brush GetSolidBrush(Color color)
    {
        if (!solidBrushes.ContainsKey(color))
        {
            lock (brushLock)
            {
                if (!solidBrushes.ContainsKey(color))
                {
                    Brush brush = new SolidBrush(color);
                    solidBrushes.Add(color, brush);
                    return brush;
                }
            }
        }
        return solidBrushes[color];
    }
}
  1. Is there a better way for me to reuse these GDI+ objects, as opposed to instantiating them all over again every time OnPaint() etc gets called?
  2. Will the GDI+ objects cause an unmanaged memory leak once the program terminates, or will the finalizer for each Brush object get called which will in turn release any unmanaged resources?

I apologize if this is a repeat, but I didn't find any similar questions.


Solution

  • There will not be memory leak but it’s better to release GDI+ objects when it makes sense for you. There are a limited amount of them in the operating system, so you might cause rendering issues in your and others applications. Another thing to be mentioned is inability of GDI+ objects (fonts, etc.) to be used by 2+ threads the same time (some difficult to reproduce exceptions might be thrown). You might be interested in some measurements of actual GDI+ objects creation time vs. possible exclusive locking delays. "premature optimization is the root of all evil" © Donald Knuth

    Actually it works for me to do some GDI+ objects caching: per painting cycle. Client code might look like this:

    class Visual 
    {
        public void Draw() 
        {
            using (new GraphicsPalette()) {
                DrawHeader();
                DrawFooter();
            }
        }
    
        private void DrawHeader() {
            var brush = GraphicsPalette.GetSolidBrush(Color.Green);
            ...   
        }
    
        public void DrawFooter() { 
            using (new GraphicsPalette()) { // ensures palette existence; does nothing if there is a palette on the stack
                var brush = GraphicsPalette.GetSolidBrush(Color.Green); // returns the same brush as in DrawHeader
                ...
            }
        }
    }
    

    So we need GraphicsPalette to ignore nested construction and return the same brush for a given thread. The suggested solution:

    public class GraphicsPalette : IDisposable 
    {
        [ThreadStatic]
        private static GraphicsPalette _current = null;
        private readonly Dictionary<Color, SolidBrush> _solidBrushes = new Dictionary<Color, SolidBrush>();
    
        public GraphicsPalette() 
        {
            if (_current == null)
                _current = this;
        }
    
        public void Dispose() 
        {
            if (_current == this)
                _current = null;
    
            foreach (var solidBrush in _solidBrushes.Values)
                solidBrush.Dispose();            
        }
    
        public static SolidBrush GetSolidBrush(Color color) 
        {
            if (!_current._solidBrushes.ContainsKey(color))
                _current._solidBrushes[color] = new SolidBrush(color);
    
            return _current._solidBrushes[color];
        }
    }