Using native GDI for text rendering in C#

To complete my previous post on text rendering in .NET I will present here the pitfalls I encountered migrating HTML Renderer to native GDI text rendering. The final result is ready to use NativeTextRenderer class with simple managed API that can be used for native GDI text rendering.

HDC

The first thing required for GDI native rendering is a handle to device context of the graphics object, HDC is used to set the font, color, clip range and text draw/measure GDI functions. HDC is received via Graphics.GetHdc and must be released via Graphics.ReleaseHdc, during the scope of a the method pair you can make calls only to GDI functions, calls to GDI+ methods of the Graphics object will fail with an ObjectBudy error.
To handle this the NativeTextRenderer calls GetHdc in the constructor and implements IDisposable interface to call ReleaseHdc in the dispose method those allowing the use of the using pattern.
 

Background mode

By default created HDC will use opaque background mode when rendering text, filling the rectangle with the current background color.
Because this is not the default behavior of managed draw methods the NativeTextRenderer, using SetBkMode function, sets the background mode to transparent on creation of the HDC.
 

Clip range

When HDC is created it doesn't inherit the clipping region set on the Graphics object therefor the text can be drawn outside of the clipping bounds.
To set the same clipping range NativeTextRenderer first gets the Graphics clip region using Graphics.Clip property before GetHdc is called and then sets the region on HDC using SelectClipRgn function.
 

Text color

Unlike managed text draw methods that receive the text color as parameter, GDI text draw functions require the text color to be set before calling text draw using SetTextColor. For simplicity NativeTextRenderer methods receive the color of the text for each text draw calling SetTextColor.
 

Fonts

As text color, GDI text rendering methods do not receive the font as one of the parameters but need it set before rendering functions call using SelectObject. The SelectObject function receives font handle that can be created from Font.ToHfont method and must be released using DeleteObject. Because fonts creation and deletion can be relatively expensive NativeTextRenderer caches the fonts in a static collection. For simplicity NativeTextRenderer methods receive the managed font object of the text for each text draw/measure getting native font handle and calling SelectObject.
 

Using NativeTextRenderer

To get the most performance out of NativeTextRenderer you should create/dispose it as little as possible so it is best to group all text rendering. Also reusing the same managed font object is advisable as its creation is also relatively expensive.
 

NativeTextRenderer class

For more convenient source viewer see: NativeTextRenderer source.

/// <summary>      
/// Wrapper for GDI  text rendering functions<br/>      
/// This class is  not thread-safe as GDI function should be called from the UI thread.      
///  </summary>      
public sealed class  NativeTextRenderer : IDisposable      
{      
    #region Fields and Consts      

    /// <summary>      
    /// used for <see  cref="MeasureString(string,System.Drawing.Font,float,out int,out  int)"/> calculation.      
    /// </summary>      
    private static readonly int[] _charFit =  new int[1];      

    /// <summary>      
    /// used for <see  cref="MeasureString(string,System.Drawing.Font,float,out int,out  int)"/> calculation.      
    /// </summary>      
    private static readonly int[] _charFitWidth  = new int[1000];      

    /// <summary>      
    /// cache of all the font used not to  create same font again and again      
    /// </summary>      
    private static readonly  Dictionary<string, Dictionary<float, Dictionary<FontStyle,  IntPtr>>> _fontsCache = new Dictionary<string, Dictionary<float,  Dictionary<FontStyle, IntPtr>>>(StringComparer.InvariantCultureIgnoreCase);      

    /// <summary>      
    /// The wrapped WinForms graphics object      
    /// </summary>      
    private readonly Graphics _g;      

    /// <summary>      
    /// the initialized HDC used      
    /// </summary>      
    private IntPtr _hdc;      

    #endregion      


    /// <summary>      
    /// Init.      
    /// </summary>      
    public NativeTextRenderer(Graphics g)      
    {      
        _g = g;      

        var clip = _g.Clip.GetHrgn(_g);      

        _hdc = _g.GetHdc();      
        SetBkMode(_hdc, 1);      

        SelectClipRgn(_hdc, clip);      

        DeleteObject(clip);      
    }      

    /// <summary>      
    /// Measure the width and height of string  <paramref name="str"/> when drawn on device context HDC      
    /// using the given font <paramref  name="font"/>.      
    /// </summary>      
    /// <param name="str">the  string to measure</param>      
    /// <param name="font">the  font to measure string with</param>      
    /// <returns>the size of the  string</returns>      
    public Size MeasureString(string str, Font  font)      
    {      
        SetFont(font);      

        var size = new Size();      
        GetTextExtentPoint32(_hdc, str,  str.Length, ref size);      
        return size;      
    }      

    /// <summary>      
    /// Measure the width and height of string  <paramref name="str"/> when drawn on device context HDC      
    /// using the given font <paramref  name="font"/>.<br/>      
    /// Restrict the width of the string and  get the number of characters able to fit in the restriction and      
    /// the width those characters take.      
    /// </summary>      
    /// <param name="str">the  string to measure</param>      
    /// <param name="font">the  font to measure string with</param>      
    /// <param  name="maxWidth">the max width to render the string  in</param>      
    /// <param  name="charFit">the number of characters that will fit under  <see cref="maxWidth"/> restriction</param>      
    /// <param  name="charFitWidth"></param>      
    /// <returns>the size of the  string</returns>      
    public Size MeasureString(string str, Font  font, float maxWidth, out int charFit, out int charFitWidth)      
    {      
        SetFont(font);      

        var size = new Size();      
        GetTextExtentExPoint(_hdc, str,  str.Length, (int)Math.Round(maxWidth), _charFit, _charFitWidth, ref size);      
        charFit = _charFit[0];      
        charFitWidth = charFit > 0 ?  _charFitWidth[charFit - 1] : 0;      
        return size;      
    }      

    /// <summary>      
    /// Draw the given string using the given  font and foreground color at given location.      
    /// </summary>      
    /// <param name="str">the  string to draw</param>      
    /// <param name="font">the  font to use to draw the string</param>      
    /// <param name="color">the  text color to set</param>      
    /// <param name="point">the  location to start string draw (top-left)</param>      
    public void DrawString(String str, Font  font, Color color, Point point)      
    {      
        SetFont(font);      
        SetTextColor(color);      

        TextOut(_hdc, point.X, point.Y, str,  str.Length);      
    }      

    /// <summary>      
    /// Draw the given string using the given  font and foreground color at given location.<br/>      
    /// See [http://msdn.microsoft.com/en-us/library/windows/desktop/dd162498(v=vs.85).aspx][15].      
    /// </summary>      
    /// <param name="str">the  string to draw</param>      
    /// <param name="font">the  font to use to draw the string</param>      
    /// <param name="color">the  text color to set</param>      
    /// <param name="rect">the  rectangle in which the text is to be formatted</param>      
    /// <param name="flags">The  method of formatting the text</param>      
    public void DrawString(String str, Font  font, Color color, Rectangle rect, TextFormatFlags flags)      
    {      
        SetFont(font);      
        SetTextColor(color);      

        var rect2 = new Rect(rect);      
        DrawText(_hdc, str, str.Length, ref  rect2, (uint)flags);      
    }      

    /// <summary>      
    /// Release current HDC to be able to use  <see cref="Graphics"/> methods.      
    /// </summary>      
    public void Dispose()      
    {      
        if (_hdc != IntPtr.Zero)      
        {      
            SelectClipRgn(_hdc, IntPtr.Zero);      
            _g.ReleaseHdc(_hdc);      
            _hdc = IntPtr.Zero;      
        }      
    }      


    #region Private methods      

    /// <summary>      
    /// Set a resource (e.g. a font) for the  specified device context.      
    /// </summary>      
    private void SetFont(Font font)      
    {      
        SelectObject(_hdc,  GetCachedHFont(font));      
    }      

    /// <summary>      
    /// Get cached unmanaged font handle for  given font.<br/>      
    /// </summary>      
    /// <param name="font">the  font to get unmanaged font handle for</param>      
    /// <returns>handle to unmanaged  font</returns>      
    private static IntPtr GetCachedHFont(Font  font)      
    {      
        IntPtr hfont = IntPtr.Zero;      
        Dictionary<float,  Dictionary<FontStyle, IntPtr>> dic1;      
        if (_fontsCache.TryGetValue(font.Name,  out dic1))      
        {      
            Dictionary<FontStyle, IntPtr>  dic2;      
            if (dic1.TryGetValue(font.Size, out  dic2))      
            {      
                dic2.TryGetValue(font.Style,  out hfont);      
            }      
            else      
            {      
                dic1[font.Size] = new  Dictionary<FontStyle, IntPtr>();      
            }      
        }      
        else      
        {      
            _fontsCache[font.Name] = new  Dictionary<float, Dictionary<FontStyle, IntPtr>>();      
            _fontsCache[font.Name][font.Size] =  new Dictionary<FontStyle, IntPtr>();      
        }      

        if (hfont == IntPtr.Zero)      
        {      
            _fontsCache[font.Name][font.Size][font.Style] = hfont = font.ToHfont();      
        }      

        return hfont;      
    }      

    /// <summary>      
    /// Set the text color of the device  context.      
    /// </summary>      
    private void SetTextColor(Color color)      
    {      
        int rgb = ( color.B & 0xFF )  << 16 | ( color.G & 0xFF ) << 8 | color.R;      
        SetTextColor(_hdc, rgb);      
    }      

    [DllImport("gdi32.dll")]      
    private static extern int SetBkMode(IntPtr  hdc, int mode);      

    [DllImport("gdi32.dll")]      
    private static extern int  SelectObject(IntPtr hdc, IntPtr hgdiObj);      

    [DllImport("gdi32.dll")]      
    private static extern int  SetTextColor(IntPtr hdc, int color);      

    [DllImport("gdi32.dll",  EntryPoint = "GetTextExtentPoint32W")]      
    private static extern int  GetTextExtentPoint32(IntPtr hdc, [MarshalAs(UnmanagedType.LPWStr)] string str,  int len, ref Size size);      

    [DllImport("gdi32.dll",  EntryPoint = "GetTextExtentExPointW")]      
    private static extern bool  GetTextExtentExPoint(IntPtr hDc, [MarshalAs(UnmanagedType.LPWStr)]string str,  int nLength, int nMaxExtent, int[] lpnFit, int[] alpDx, ref Size size);      

    [DllImport("gdi32.dll",  EntryPoint = "TextOutW")]      
    private static extern bool TextOut(IntPtr  hdc, int x, int y, [MarshalAs(UnmanagedType.LPWStr)] string str, int len);      

    [DllImport("user32.dll",  EntryPoint = "DrawTextW")]      
    private static extern int DrawText(IntPtr  hdc, [MarshalAs(UnmanagedType.LPWStr)] string str, int len, ref Rect rect, uint  uFormat);      

    [DllImport("gdi32.dll")]      
    private static extern int  SelectClipRgn(IntPtr hdc, IntPtr hrgn);      

    [DllImport("gdi32.dll")]      
    private static extern bool  DeleteObject(IntPtr hObject);      

    // ReSharper disable NotAccessedField.Local      
    private struct Rect      
    {      
        private int _left;      
        private int _top;      
        private int _right;      
        private int _bottom;      

        public Rect(Rectangle r)      
        {      
            _left = r.Left;      
            _top = r.Top;      
            _bottom = r.Bottom;      
            _right = r.Right;      
        }      
    }      
    // ReSharper restore NotAccessedField.Local      

    #endregion      
}      

/// <summary>      
/// See [http://msdn.microsoft.com/en-us/library/windows/desktop/dd162498(v=vs.85).aspx][15]      
///  </summary>      
[Flags]      
public enum  TextFormatFlags : uint      
{      
    Default = 0x00000000,      
    Center = 0x00000001,      
    Right = 0x00000002,      
    VCenter = 0x00000004,      
    Bottom = 0x00000008,      
    WordBreak = 0x00000010,      
    SingleLine = 0x00000020,      
    ExpandTabs = 0x00000040,      
    TabStop = 0x00000080,      
    NoClip = 0x00000100,      
    ExternalLeading = 0x00000200,      
    CalcRect = 0x00000400,      
    NoPrefix = 0x00000800,      
    Internal = 0x00001000,      
    EditControl = 0x00002000,      
    PathEllipsis = 0x00004000,      
    EndEllipsis = 0x00008000,      
    ModifyString = 0x00010000,      
    RtlReading = 0x00020000,      
    WordEllipsis = 0x00040000,      
    NoFullWidthCharBreak = 0x00080000,      
    HidePrefix = 0x00100000,      
    ProfixOnly = 0x00200000,      
}      

10 comments on “Using native GDI for text rendering in C#

  1. […] using p/invoke. It’s much more complicated than using TextRenderer so I will dedicate my next post to the details and present a utility class to simplify […]

  2. […] changing HTML Renderer to GDI text rendering (see previous post and this one) I have encountered another issue: GDI doesn’t support alpha channel, which means that GDI is […]

  3. […] For better performance use native call to GDI functions, see Using native GDI for text rendering in C# . […]

  4. […] changing HTML Renderer to GDI text rendering (see previous post and this one) I have encountered another issue: GDI doesn't support alpha channel, which means that GDI is […]

  5. shane says:

    Thanks for the great post Arthur. I used your code as a drop in replacement for the .NET version and saw a dramatic improvement in my text rendering performance.

    A small comment, which you are welcome to ignore:

    I ran fxcop over my entire code base and it issued a portability warning on your code. I looked into it a little and it turns out it is because of this declaration

    [DllImport(“gdi32.dll”)]
    private static extern int SelectObject(IntPtr hdc, IntPtr hgdiObj);

    According to http://www.pinvoke.net/default.aspx/gdi32.selectobject this should look like

    [DllImport(“gdi32.dll”, EntryPoint = “SelectObject”)]
    public static extern IntPtr SelectObject([In] IntPtr hdc, [In] IntPtr hgdiobj);

    The key difference that fxcop picked up being the return value. Apparently, because int is 32 bit and IntPtr is system word size the behavior may not be as expected on a 64 bit system where the declaration doesn’t technically match.

    A more concerning discrepancy is in the BitBlt method where your version defines dwRop as a 64 bit integer whereas pinvoke says it should be a 32 bit integer.

    For my code I’ve simply updated to the code from pinvoke and fxcop is now happy. I figured I’d do you the courtesy of posting here in case you wished to update your code.

    Thanks again

  6. Spike says:

    With a few additions this might also be able to render OpenType fonts as well. Something sadly missing and becoming increasingly difficult to cope without as font studios have moved away from truetype.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s